From f53dbe9be4ff405ef09992440d73839acc866a39 Mon Sep 17 00:00:00 2001 From: Thomas Kolb Date: Sun, 19 Jan 2020 20:10:07 +0100 Subject: [PATCH 1/5] First version of collectd plugin --- README.md | 48 +++++++++++++++- collectd/collectd_sem6000.py | 108 +++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 1 deletion(-) create mode 100755 collectd/collectd_sem6000.py diff --git a/README.md b/README.md index 9929704..5333571 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,42 @@ $ 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" + + + Address "ab:cd:ef:13:37:42" + SocketName "ASecondSocket" + + # ... + +``` + +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..76f19bb --- /dev/null +++ b/collectd/collectd_sem6000.py @@ -0,0 +1,108 @@ +#!/usr/bin/env python3 +# coding: utf-8 +# vim: noet ts=2 sw=2 sts=2 + +import os +import collectd + +from sem6000 import SEMSocket +import bluepy + +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 'address' not in config.keys(): + collectd.error('sem6000: address must be set') + return + + if 'socketname' not in config.keys(): + config['socketname'] = config['address'].replace(':', '') + + instances.append( {'config': config, 'socket': None} ) + +def read_func(): + global instances + + for inst in instances: + config = inst['config'] + + 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, bluepy.btle.BTLEDisconnectError, BrokenPipeError) as e: + collectd.warning("sem6000: Exception caught: {}".format(e)) + collectd.warning("sem6000: Restarting on next cycle...") + + 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)) + + 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) -- 2.45.2 From de3b7026a821aa158a2a83d13af3767928db7893 Mon Sep 17 00:00:00 2001 From: Thomas Kolb Date: Mon, 20 Jan 2020 12:08:44 +0100 Subject: [PATCH 2/5] Improved handling of unavailable devices --- README.md | 10 ++++++++++ collectd/collectd_sem6000.py | 38 +++++++++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5333571..fc50c6c 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,8 @@ Add or adjust the configuration for your collectd’s Python plugin as follows: Address "12:34:56:78:90:ab" SocketName "FirstSocket" + ReadTimeout 30 + SuspendTime 300 Address "ab:cd:ef:13:37:42" @@ -49,6 +51,14 @@ Add or adjust the configuration for your collectd’s Python plugin as follows: ``` +`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 index 76f19bb..40d2048 100755 --- a/collectd/collectd_sem6000.py +++ b/collectd/collectd_sem6000.py @@ -3,6 +3,7 @@ # vim: noet ts=2 sw=2 sts=2 import os +import time import collectd from sem6000 import SEMSocket @@ -25,6 +26,12 @@ def config_func(cfg): 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 @@ -32,7 +39,19 @@ def config_func(cfg): if 'socketname' not in config.keys(): config['socketname'] = config['address'].replace(':', '') - instances.append( {'config': config, 'socket': None} ) + 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 @@ -40,6 +59,14 @@ def read_func(): 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'])) @@ -52,6 +79,13 @@ def read_func(): 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 @@ -61,6 +95,8 @@ def read_func(): 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' -- 2.45.2 From 861079e997b9409a973a3a69840a974d585c9cd6 Mon Sep 17 00:00:00 2001 From: Thomas Kolb Date: Tue, 21 Jan 2020 19:55:09 +0100 Subject: [PATCH 3/5] collectd: clarify bluepy import --- collectd/collectd_sem6000.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/collectd/collectd_sem6000.py b/collectd/collectd_sem6000.py index 40d2048..397a382 100755 --- a/collectd/collectd_sem6000.py +++ b/collectd/collectd_sem6000.py @@ -7,7 +7,7 @@ import time import collectd from sem6000 import SEMSocket -import bluepy +from bluepy.btle import BTLEDisconnectError instances = [] @@ -75,7 +75,7 @@ def read_func(): collectd.info("sem6000: Connected.") inst['socket'].getStatus() - except (SEMSocket.NotConnectedException, bluepy.btle.BTLEDisconnectError, BrokenPipeError) as e: + except (SEMSocket.NotConnectedException, BTLEDisconnectError, BrokenPipeError) as e: collectd.warning("sem6000: Exception caught: {}".format(e)) collectd.warning("sem6000: Restarting on next cycle...") -- 2.45.2 From a0874625c7d6d2bb1d0594145f2a4a6d351de458 Mon Sep 17 00:00:00 2001 From: Thomas Kolb Date: Tue, 28 Jan 2020 15:54:36 +0100 Subject: [PATCH 4/5] fix: use function parameter in changePassword() --- sem6000.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sem6000.py b/sem6000.py index df90f0a..2472775 100644 --- a/sem6000.py +++ b/sem6000.py @@ -63,7 +63,7 @@ class SEMSocket(): payload.append(0x00) payload.append(0x01) for i in range(4): - payload.append(int(self.newPassword[i])) + payload.append(int(newPassword[i])) for i in range(4): payload.append(int(self.password[i])) self.password = newPassword -- 2.45.2 From a5312baef381aef04249ccf33b66c84c1d9111f3 Mon Sep 17 00:00:00 2001 From: sqozz Date: Fri, 17 Jan 2020 22:20:41 +0100 Subject: [PATCH 5/5] Add function to sync the time with the plug --- sem6000.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/sem6000.py b/sem6000.py index 2472775..3e952c7 100644 --- a/sem6000.py +++ b/sem6000.py @@ -1,5 +1,6 @@ from bluepy import btle import time +import datetime import uuid class SEMSocket(): @@ -42,6 +43,17 @@ class SEMSocket(): msg = self.BTLEMessage(self, cmd, payload) msg.send() + def syncTime(self): + #15, 12, 1, 0, SECOND, MINUTE, HOUR_OF_DAY, DAY_OF_MONTH, MONTH (+1), int(YEAR/256), YEAR%256, 0, 0, CHKSUM, 255, 255 + now = datetime.datetime.now() + cmd = bytearray([0x01]) + payload = bytearray([0x00]) + payload += bytearray([now.second, now.minute, now.hour]) + payload += bytearray([now.day, now.month, int(now.year/256), now.year%256]) + payload += bytearray([0x00, 0x00]) + msg = self.BTLEMessage(self, cmd, payload) + msg.send() + def login(self, password): self.password = password cmd = bytearray([0x17]) @@ -191,6 +203,8 @@ class SEMSocket(): print("Checksum error!") else: print("Unknown error:", data) + elif message_type == 0x01: + print("Time synced") elif message_type == 0x03: #switch toggle print("Switch toggled") self.__btle_device.getStatus() -- 2.45.2