Collectd Plugin #13
48
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
|
$ git clone … sem6000
|
||||||
$ cd sem6000
|
$ cd sem6000
|
||||||
|
@ -5,4 +13,42 @@ $ 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
|
||||||
|
|||||||
|
```
|
||||||
|
|
||||||
|
Add or adjust the configuration for your collectd’s 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"
|
||||||
|
</Module>
|
||||||
|
<Module collectd_sem6000>
|
||||||
|
Address "ab:cd:ef:13:37:42"
|
||||||
|
SocketName "ASecondSocket"
|
||||||
|
</Module>
|
||||||
|
# ...
|
||||||
|
</Plugin>
|
||||||
|
```
|
||||||
|
|
||||||
|
Make sure that everything listed in `requirements.txt` is available to the user
|
||||||
|
running collectd.
|
||||||
|
|
||||||
|
|
108
collectd/collectd_sem6000.py
Executable file
|
@ -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
|
||||||
|
|
||||||
sqozz
commented
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.
cfr34k
commented
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 '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:
|
||||||
sqozz
commented
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.
cfr34k
commented
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 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.
|
|||||||
|
inst['socket'].disconnect()
|
||||||
|
|
||||||
|
instances = []
|
||||||
|
|
||||||
|
collectd.register_config(config_func)
|
||||||
|
collectd.register_init(init_func)
|
||||||
|
collectd.register_read(read_func)
|
||||||
|
collectd.register_shutdown(shutdown_func)
|
did you want to indicate root rights for these commands (because of the preceding
#
)? If not, I'd prefer$
as shell indicator :)Yes, the
#
should indicate required root access.