Collectd Plugin #13

Merged
sqozz merged 3 commits from cfr34k/sem6000:collectd into master 2020-05-02 19:26:20 +02:00
2 changed files with 201 additions and 1 deletions

View file

@ -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 $ git clone … sem6000
$ cd sem6000 $ cd sem6000
@ -5,4 +13,52 @@ $ virtualenv -p python3 python3_venv
$ . ./python3_venv/bin/activate $ . ./python3_venv/bin/activate
$ pip3 install -r requirements.txt $ pip3 install -r requirements.txt
$ python3 example.py $ python3 example.py
``` ```
## 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
Review

did you want to indicate root rights for these commands (because of the preceding #)? If not, I'd prefer $ as shell indicator :)

did you want to indicate root rights for these commands (because of the preceding `#`)? If not, I'd prefer `$` as shell indicator :)
Review

Yes, the # should indicate required root access.

Yes, the `#` should indicate required root access.
```
Add or adjust the configuration for your collectds Python plugin as follows:
```
<Plugin python>
ModulePath "/usr/local/share/collectd/python"
LogTraces true
Interactive false
Import "collectd_sem6000"
<Module collectd_sem6000>
Address "12:34:56:78:90:ab"
SocketName "FirstSocket"
ReadTimeout 30
SuspendTime 300
</Module>
<Module collectd_sem6000>
Address "ab:cd:ef:13:37:42"
SocketName "ASecondSocket"
</Module>
# ...
</Plugin>
```
`ReadTimeout` and `SuspendTime` control whats 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.

144
collectd/collectd_sem6000.py Executable file
View file

@ -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
Outdated
Review

maybe just import the exception here? I don't know if it makes any performance difference but at least it would be less confusing why bluepy is needed in here.

maybe just import the exception here? I don't know if it makes any performance difference but at least it would be less confusing why bluepy is needed in here.

Sounds reasonable. I'll do that!

Sounds reasonable. I'll do that!
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']))
Review

I'd prefer to use the BT mac instead of a string as plugin name here. There's a good chance we can read the name of the socket over BT and expose it later with this plugin which would allow a mapping even if the name changes.

I'd prefer to use the BT mac instead of a string as plugin name here. There's a good chance we can read the name of the socket over BT and expose it later with this plugin which would allow a mapping even if the name changes.
Review

Are you sure that the name is actually stored on the socket? My feeling is that that’s only a mapping in the app.

I prefer to have a string in the plugin name, because it indicates the purpose of the measurements (which can change as devices are reused). If you don't like it, just copy the address into SocketName 😉 in the collectd config.

Are you sure that the name is actually stored on the socket? My feeling is that that’s only a mapping in the app. I prefer to have a string in the plugin name, because it indicates the purpose of the measurements (which can change as devices are reused). If you don't like it, just copy the address into `SocketName` :wink: in the collectd config.
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)