diff --git a/README.md b/README.md index 9929704..fc50c6c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,11 @@ +# SEM6000 Python Library + +SEM6000 is a energy meter and power switch with Bluetooth 4.0. + +This library provides a Python module for these devices + +## Run the example code + ``` $ git clone … sem6000 $ cd sem6000 @@ -5,4 +13,52 @@ $ virtualenv -p python3 python3_venv $ . ./python3_venv/bin/activate $ pip3 install -r requirements.txt $ python3 example.py -``` \ No newline at end of file +``` + +## Collectd Plugin + +You can find a Plugin for [collectd](https://collectd.org) in the `collectd` +subdirectory. + +Installation procedure (the target directory may be changed of course): + +```shell +# mkdir -p /usr/local/lib/collectd/python +# cp collectd/collectd_sem6000.py /usr/local/lib/collectd/python +# cp sem6000.py /usr/local/lib/collectd/python +``` + +Add or adjust the configuration for your collectd’s Python plugin as follows: + +``` + + ModulePath "/usr/local/share/collectd/python" + LogTraces true + Interactive false + Import "collectd_sem6000" + + + Address "12:34:56:78:90:ab" + SocketName "FirstSocket" + ReadTimeout 30 + SuspendTime 300 + + + Address "ab:cd:ef:13:37:42" + SocketName "ASecondSocket" + + # ... + +``` + +`ReadTimeout` and `SuspendTime` control what’s happening when a device is +unavailable. If no value could be retrieved for `ReadTimeout` seconds, the +plugin does not retry for `SuspendTime` seconds. After that, normal operation +is resumed. This procedure ensures that an unreachable device does not block +other devices (too often) in the current single-threaded architecture. + +If not specified, `ReadTimeout` is 30 seconds and `SuspendTime` is 5 minutes. + +Make sure that everything listed in `requirements.txt` is available to the user +running collectd. + diff --git a/collectd/collectd_sem6000.py b/collectd/collectd_sem6000.py new file mode 100755 index 0000000..397a382 --- /dev/null +++ b/collectd/collectd_sem6000.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +# coding: utf-8 +# vim: noet ts=2 sw=2 sts=2 + +import os +import time +import collectd + +from sem6000 import SEMSocket +from bluepy.btle import BTLEDisconnectError + +instances = [] + +def init_func(): + pass + +def config_func(cfg): + global instances + + config = {} + + for node in cfg.children: + key = node.key.lower() + value = node.values[0] + + if key in ['address', 'socketname']: + config[key] = value + + if key == 'readtimeout': + config['readtimeout'] = int(value) + + if key == 'suspendtime': + config['suspendtime'] = int(value) + + if 'address' not in config.keys(): + collectd.error('sem6000: address must be set') + return + + if 'socketname' not in config.keys(): + config['socketname'] = config['address'].replace(':', '') + + if 'readtimeout' not in config.keys(): + config['readtimeout'] = 30 + + if 'suspendtime' not in config.keys(): + config['suspendtime'] = 300 + + instances.append( { + 'config': config, + 'socket': None, + 'suspended': False, + 'lastsuccess': 0, + 'resumetime': 0 + } ) + +def read_func(): + global instances + + for inst in instances: + config = inst['config'] + + if inst['suspended']: + if time.time() < inst['resumetime']: + continue + else: + collectd.info("sem6000: Device {} waking up.".format(config['address'])) + inst['suspended'] = False + inst['lastsuccess'] = time.time() + + try: + if inst['socket'] == None: + collectd.info("sem6000: Connecting to {}...".format(config['address'])) + + inst['socket'] = SEMSocket(config['address']) + collectd.info("sem6000: Connected.") + + inst['socket'].getStatus() + except (SEMSocket.NotConnectedException, BTLEDisconnectError, BrokenPipeError) as e: + collectd.warning("sem6000: Exception caught: {}".format(e)) + collectd.warning("sem6000: Restarting on next cycle...") + + if inst['lastsuccess'] < time.time() - config['readtimeout']: + collectd.error("sem6000: no successful communication with {} for {:.1f} seconds. Suspending device for {:.1f} seconds.".format( + config['address'], config['readtimeout'], config['suspendtime'])) + + inst['suspended'] = True + inst['resumetime'] = time.time() + config['suspendtime'] + + if inst['socket'] != None: + inst['socket'].disconnect() + inst['socket'] = None + + socket = inst['socket'] + + if socket != None and socket.voltage != 0: + collectd.debug("Uploading values for {}".format(socket.mac_address)) + + inst['lastsuccess'] = time.time() + + val = collectd.Values(plugin = 'sem6000-{}'.format(config['socketname'])) + + val.type = 'voltage' + val.type_instance = 'grid' + val.values = [ socket.voltage ] + val.dispatch() + + val.type = 'current' + val.type_instance = 'load' + val.values = [ socket.current ] + val.dispatch() + + val.type = 'power' + val.type_instance = 'real_power' + val.values = [ socket.power ] + val.dispatch() + + val.type = 'gauge' + val.type_instance = 'power_factor' + val.values = [ socket.power_factor ] + val.dispatch() + + val.type = 'gauge' + val.type_instance = 'load_on' + val.values = [ socket.powered ] + val.dispatch() + + val.type = 'frequency' + val.type_instance = 'grid' + val.values = [ socket.frequency ] + val.dispatch() + +def shutdown_func(): + global instances + + for inst in instances: + if inst['socket'] != None: + inst['socket'].disconnect() + + instances = [] + +collectd.register_config(config_func) +collectd.register_init(init_func) +collectd.register_read(read_func) +collectd.register_shutdown(shutdown_func)