├── src ├── goodwe │ └── __init__.py ├── smalibrary │ ├── __init__.py │ └── SMABluetoothPacket.py ├── Cargo.py ├── emonhub_coder.py ├── interfacers │ ├── __init__.py │ ├── tmp │ │ ├── EmonFroniusModbusTcpInterfacer.py │ │ └── EmonHubSmilicsInterfacer.py │ ├── EmonHubSerialInterfacer.py │ ├── EmonHubTx3eInterfacer.py │ ├── EmonHubTeslaPowerWallInterfacer.py │ ├── EmonHubRedisInterfacer.py │ ├── EmonHubPulseCounterInterfacer.py │ ├── EmonHubDigitalInputInterfacer.py │ ├── EmonHubRFM69LPLInterfacer.py │ ├── EmonHubGraphiteInterfacer.py │ ├── EmonHubGoodWeInterfacer.py │ ├── EmonHubTemplateInterfacer.py │ ├── EmonHubSunampInterfacer.py │ ├── EmonHubBleInterfacer.py │ ├── EmonHubPacketGenInterfacer.py │ ├── EmonHubDS18B20Interfacer.py │ ├── EmonHubInfluxInterfacer.py │ ├── EmonHubSocketInterfacer.py │ ├── EmonHubModbusRenogyInterfacer.py │ └── EmonHubRF69Interfacer.py ├── emonhub_auto_conf.py ├── emonhub_buffer.py └── emonhub_setup.py ├── version.txt ├── .gitattributes ├── .gitignore ├── docs ├── img │ ├── sds011.jpg │ ├── emonhubconf.png │ ├── emonhublog.png │ ├── mbus_reader.png │ ├── direct_pulse.jpeg │ ├── mbus_emoncms.png │ ├── samsung-ashp.jpg │ ├── sdm120_emoncms.png │ ├── sdm120_modbus.png │ ├── sds011_emoncms.png │ ├── sdm120-230-ob415.png │ └── samsung-ashp-emoncms.png ├── index.md ├── troubleshooting.md └── overview.md ├── service ├── emonhub.default.conf ├── emonhub.service.bullseye └── emonhub.service ├── conf ├── interfacer_examples │ ├── directserial-serialtx3e │ │ └── readme.md │ ├── RF69 │ │ ├── RF69.emonhub.conf │ │ └── readme.md │ ├── Socket │ │ ├── socket.emonhub.conf │ │ └── readme.md │ ├── OEM │ │ ├── oem.emonhub.conf │ │ └── readme.md │ ├── GoodWe │ │ ├── goodwe.emonhub.conf │ │ └── readme.md │ ├── SDS011 │ │ ├── sds011.emonhub.conf │ │ └── readme.md │ ├── Pulse │ │ ├── pulse.emonhub.conf │ │ └── readme.md │ ├── Redis │ │ ├── redis.emonhub.conf │ │ └── readme.md │ ├── PowerWall │ │ ├── powerwall.emonhub.conf │ │ └── readme.md │ ├── samsung-ashp │ │ ├── readme.md │ │ └── samsung-ashp.emonhub.conf │ ├── MBUS │ │ ├── mbus.emonhub.conf │ │ └── readme.md │ ├── Influx │ │ ├── influx.emonhub.conf │ │ └── readme.md │ ├── DS18B20 │ │ ├── sds011.emonhub.conf │ │ └── readme.md │ ├── Emoncms │ │ ├── emoncms.emonhub.conf │ │ └── readme.md │ ├── graphite │ │ ├── graphite.emonhub.conf │ │ └── readme.md │ ├── SDM630 │ │ └── sdm630.emonhub.conf │ ├── rayleigh-ri-d35-100 │ │ └── rayleigh-ri-d35-100.emonhub.conf │ ├── smasolar │ │ ├── smasolar.emonhub.conf │ │ └── readme.md │ ├── JaguarLandRover │ │ ├── jlr.emonhub.conf │ │ └── readme.md │ ├── RFM2Pi │ │ ├── rfm2pi.emonhub.conf │ │ └── readme.md │ ├── bmw │ │ ├── bmw.emonhub.conf │ │ └── readme.md │ ├── vedirect │ │ ├── mppt.vedirect.emonhub.conf │ │ ├── readme.md │ │ └── bmv700.vedirect.emonhub.conf │ ├── SDM120 │ │ ├── sdm120.emonhub.conf │ │ └── readme.md │ ├── MQTT │ │ ├── mqtt.emonhub.conf │ │ └── readme.md │ ├── Renogy │ │ └── Renogy.emonhub.conf │ ├── smilices │ │ ├── readme.md │ │ └── smilics.emonhub.conf │ ├── directserial │ │ └── readme.md │ └── modbus │ │ ├── readme.md │ │ └── modbusTCP.emonhub.conf ├── nodes │ ├── 5 │ ├── 6 │ ├── 7 │ ├── 8 │ ├── 9 │ ├── 10 │ ├── 11 │ ├── 12 │ ├── 13 │ ├── 14 │ ├── 15 │ ├── 16 │ ├── 19 │ ├── 20 │ ├── 21 │ ├── 22 │ ├── 23 │ ├── 24 │ ├── 25 │ ├── 26 │ ├── Readme.md │ └── emonpi_auto_add_nodes.sh ├── default │ └── emonhub ├── default.emonhub.conf ├── emonpi2.default.emonhub.conf └── available.conf ├── module.json ├── examples ├── control_sender.py └── mqtt_reader.py └── README.md /src/goodwe/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /version.txt: -------------------------------------------------------------------------------- 1 | 2.7.6 2 | -------------------------------------------------------------------------------- /src/smalibrary/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | README.md merge=ours 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | README.md 2 | *.pyc 3 | *.swp 4 | *.idea 5 | *~ 6 | .vscode/settings.json 7 | -------------------------------------------------------------------------------- /docs/img/sds011.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njh/emonhub/master/docs/img/sds011.jpg -------------------------------------------------------------------------------- /docs/img/emonhubconf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njh/emonhub/master/docs/img/emonhubconf.png -------------------------------------------------------------------------------- /docs/img/emonhublog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njh/emonhub/master/docs/img/emonhublog.png -------------------------------------------------------------------------------- /docs/img/mbus_reader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njh/emonhub/master/docs/img/mbus_reader.png -------------------------------------------------------------------------------- /docs/img/direct_pulse.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njh/emonhub/master/docs/img/direct_pulse.jpeg -------------------------------------------------------------------------------- /docs/img/mbus_emoncms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njh/emonhub/master/docs/img/mbus_emoncms.png -------------------------------------------------------------------------------- /docs/img/samsung-ashp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njh/emonhub/master/docs/img/samsung-ashp.jpg -------------------------------------------------------------------------------- /docs/img/sdm120_emoncms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njh/emonhub/master/docs/img/sdm120_emoncms.png -------------------------------------------------------------------------------- /docs/img/sdm120_modbus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njh/emonhub/master/docs/img/sdm120_modbus.png -------------------------------------------------------------------------------- /docs/img/sds011_emoncms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njh/emonhub/master/docs/img/sds011_emoncms.png -------------------------------------------------------------------------------- /docs/img/sdm120-230-ob415.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njh/emonhub/master/docs/img/sdm120-230-ob415.png -------------------------------------------------------------------------------- /service/emonhub.default.conf: -------------------------------------------------------------------------------- 1 | ExecStart=/usr/share/emonhub/emonhub.py --config-file=/home/pi/data/emonhub.conf 2 | -------------------------------------------------------------------------------- /docs/img/samsung-ashp-emoncms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/njh/emonhub/master/docs/img/samsung-ashp-emoncms.png -------------------------------------------------------------------------------- /conf/interfacer_examples/directserial-serialtx3e/readme.md: -------------------------------------------------------------------------------- 1 | ## Deprecated 2 | 3 | 4 | Replaced with OEM Interfacer 5 | -------------------------------------------------------------------------------- /module.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "Emonhub", 3 | "version" : "2.7.6", 4 | "location" : "/opt/openenergymonitor", 5 | "branches_available": ["stable","master"], 6 | "requires": [] 7 | } 8 | -------------------------------------------------------------------------------- /conf/interfacer_examples/RF69/RF69.emonhub.conf: -------------------------------------------------------------------------------- 1 | [[SPI]] 2 | Type = EmonHubRF69Interfacer 3 | [[[init_settings]]] 4 | nodeid = 5 5 | group = 210 6 | [[[runtimesettings]]] 7 | pubchannels = ToEmonCMS, 8 | -------------------------------------------------------------------------------- /conf/interfacer_examples/Socket/socket.emonhub.conf: -------------------------------------------------------------------------------- 1 | [[mysocketlistener]] 2 | Type = EmonHubSocketInterfacer 3 | [[[init_settings]]] 4 | port_nb = 8080 5 | [[[runtimesettings]]] 6 | pubchannels = ToEmonCMS, 7 | -------------------------------------------------------------------------------- /conf/nodes/Readme.md: -------------------------------------------------------------------------------- 1 | Script for automatically adding node decoders to emonhub.conf without over-writing existing nodes 2 | 3 | Called by emonpi [emonhub update script](https://github.com/openenergymonitor/emonpi/blob/master/emonhubupdate) 4 | -------------------------------------------------------------------------------- /conf/interfacer_examples/OEM/oem.emonhub.conf: -------------------------------------------------------------------------------- 1 | [[OEM]] 2 | Type = EmonHubOEMInterfacer 3 | [[[init_settings]]] 4 | com_port = /dev/ttyAMA0 5 | com_baud = 115200 6 | [[[runtimesettings]]] 7 | pubchannels = ToEmonCMS, 8 | -------------------------------------------------------------------------------- /conf/interfacer_examples/GoodWe/goodwe.emonhub.conf: -------------------------------------------------------------------------------- 1 | [[GoodWe]] 2 | Type = EmonHubGoodWeInterfacer 3 | [[[init_settings]]] 4 | [[[runtimesettings]]] 5 | pubchannels = ToEmonCMS, 6 | name = goodwe 7 | ip = 192.168.0.100 8 | readinterval = 10 -------------------------------------------------------------------------------- /conf/nodes/6: -------------------------------------------------------------------------------- 1 | [[6]] 2 | nodename = emonTxShield 3 | firmware =emonTxShield 4 | hardware = emonTxShield 5 | [[[rx]]] 6 | names = power1, power2, power3, power4, Vrms 7 | datacode = h 8 | scales = 1,1,1,1,0.01 9 | units =W,W,W,W,V 10 | -------------------------------------------------------------------------------- /conf/interfacer_examples/SDS011/sds011.emonhub.conf: -------------------------------------------------------------------------------- 1 | [[SDS011]] 2 | Type = EmonHubSDS011Interfacer 3 | [[[init_settings]]] 4 | com_port = /dev/ttyUSB0 5 | [[[runtimesettings]]] 6 | readinterval = 5 7 | nodename = SDS011 8 | pubchannels = ToEmonCMS, 9 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # emonHub user guide 2 | 3 | ```{toctree} 4 | --- 5 | maxdepth: 2 6 | --- 7 | Overview 8 | Configuration 9 | Default Configuration 10 | Interfacers 11 | Troubleshooting 12 | ``` 13 | -------------------------------------------------------------------------------- /conf/nodes/11: -------------------------------------------------------------------------------- 1 | [[11]] 2 | nodename = 3phase 3 | [[[rx]]] 4 | names = powerL1, powerL2, powerL3, power4, Vrms, temp1, temp2, temp3, temp4, temp5, temp6, pulse 5 | datacodes = h,h,h,h,h,h,h,h,h,h,h,L 6 | scales = 1,1,1,1,0.01,0.1,0.1,0.1,0.1,0.1,0.1,1 7 | units =W,W,W,W,V,C,C,C,C,C,C,p 8 | -------------------------------------------------------------------------------- /conf/interfacer_examples/Pulse/pulse.emonhub.conf: -------------------------------------------------------------------------------- 1 | [[pulse]] 2 | Type = EmonHubPulseCounterInterfacer 3 | [[[init_settings]]] 4 | pulse_pin = 15 5 | # bouncetime = 2 6 | # rate_limit = 2 7 | [[[runtimesettings]]] 8 | pubchannels = ToEmonCMS, 9 | nodeoffset = 3 10 | -------------------------------------------------------------------------------- /conf/interfacer_examples/Redis/redis.emonhub.conf: -------------------------------------------------------------------------------- 1 | [[Redis]] 2 | Type = EmonHubRedisInterfacer 3 | [[[init_settings]]] 4 | redis_host = localhost 5 | redis_port = 6379 6 | redis_db = 0 7 | [[[runtimesettings]]] 8 | subchannels = ToEmonCMS, 9 | prefix = "emonhub:" 10 | -------------------------------------------------------------------------------- /conf/nodes/12: -------------------------------------------------------------------------------- 1 | [[12]] 2 | nodename = 3phase2 3 | [[[rx]]] 4 | names = powerL1, powerL2, powerL3, power4, Vrms, temp1, temp2, temp3, temp4, temp5, temp6, pulse 5 | datacodes = h,h,h,h,h,h,h,h,h,h,h,L 6 | scales = 1,1,1,1,0.01,0.1,0.1,0.1,0.1,0.1,0.1,1 7 | units =W,W,W,W,V,C,C,C,C,C,C,p 8 | -------------------------------------------------------------------------------- /conf/nodes/13: -------------------------------------------------------------------------------- 1 | [[13]] 2 | nodename = 3phase3 3 | [[[rx]]] 4 | names = powerL1, powerL2, powerL3, power4, Vrms, temp1, temp2, temp3, temp4, temp5, temp6, pulse 5 | datacodes = h,h,h,h,h,h,h,h,h,h,h,L 6 | scales = 1,1,1,1,0.01,0.1,0.1,0.1,0.1,0.1,0.1,1 7 | units = W,W,W,W,V,C,C,C,C,C,C,p 8 | -------------------------------------------------------------------------------- /conf/nodes/14: -------------------------------------------------------------------------------- 1 | [[14]] 2 | nodename = 3phase4 3 | [[[rx]]] 4 | names = powerL1, powerL2, powerL3, power4, Vrms, temp1, temp2, temp3, temp4, temp5, temp6, pulse 5 | datacodes = h,h,h,h,h,h,h,h,h,h,h,L 6 | scales = 1,1,1,1,0.01,0.1,0.1,0.1,0.1,0.1,0.1,1 7 | units = W,W,W,W,V,C,C,C,C,C,C,p 8 | -------------------------------------------------------------------------------- /conf/interfacer_examples/PowerWall/powerwall.emonhub.conf: -------------------------------------------------------------------------------- 1 | [[PowerWall]] 2 | Type = EmonHubTeslaPowerWallInterfacer 3 | [[[init_settings]]] 4 | [[[runtimesettings]]] 5 | pubchannels = ToEmonCMS, 6 | name = powerwall 7 | url = http://POWERWALL-IP/api/system_status/soe 8 | readinterval = 10 9 | -------------------------------------------------------------------------------- /conf/interfacer_examples/samsung-ashp/readme.md: -------------------------------------------------------------------------------- 1 | ### SAMSUNG ASHP MIB19N Modbus 2 | 3 | Read data from a Samsung Heat Pump or HVAC unit using the [MIM-B19N Modbus module](https://www.samsung.com/uk/support/model/MIM-B19N/). Tested on AE050RXYDEG-EU Gen6 ASHP. Should work for all Samsung HVAC units. 4 | 5 | see `.conf` for example config 6 | -------------------------------------------------------------------------------- /conf/nodes/15: -------------------------------------------------------------------------------- 1 | [[15]] 2 | nodename = emontx3cm15 3 | [[[rx]]] 4 | names = MSG, Vrms, P1, P2, P3, P4, E1, E2, E3, E4, T1, T2, T3, pulse 5 | datacodes = L,h,h,h,h,h,L,L,L,L,h,h,h,L 6 | scales = 1,0.01,1,1,1,1,1,1,1,1,0.01,0.01,0.01,1 7 | units = n,V,W,W,W,W,Wh,Wh,Wh,Wh,C,C,C,p 8 | whitening = 1 9 | -------------------------------------------------------------------------------- /conf/nodes/16: -------------------------------------------------------------------------------- 1 | [[16]] 2 | nodename = emontx3cm16 3 | [[[rx]]] 4 | names = MSG, Vrms, P1, P2, P3, P4, E1, E2, E3, E4, T1, T2, T3, pulse 5 | datacodes = L,h,h,h,h,h,L,L,L,L,h,h,h,L 6 | scales = 1,0.01,1,1,1,1,1,1,1,1,0.01,0.01,0.01,1 7 | units = n,V,W,W,W,W,Wh,Wh,Wh,Wh,C,C,C,p 8 | whitening = 1 9 | -------------------------------------------------------------------------------- /conf/nodes/19: -------------------------------------------------------------------------------- 1 | [[19]] 2 | nodename = emonTH_1 3 | firmware = emonTH_DHT22_DS18B20_RFM69CW 4 | hardware = emonTH_(Node_ID_Switch_DIP1:OFF_DIP2:OFF) 5 | [[[rx]]] 6 | names = temperature, external temperature, humidity, battery 7 | datacode = h 8 | scales = 0.1,0.1,0.1,0.1 9 | units = C,C,%,V 10 | -------------------------------------------------------------------------------- /conf/nodes/20: -------------------------------------------------------------------------------- 1 | [[20]] 2 | nodename = emonTH_2 3 | firmware = emonTH_DHT22_DS18B20_RFM69CW 4 | hardware = emonTH_(Node_ID_Switch_DIP1:ON_DIP2:OFF) 5 | [[[rx]]] 6 | names = temperature, external temperature, humidity, battery 7 | datacode = h 8 | scales = 0.1,0.1,0.1,0.1 9 | units = C,C,%,V 10 | -------------------------------------------------------------------------------- /conf/nodes/21: -------------------------------------------------------------------------------- 1 | [[21]] 2 | nodename = emonTH_3 3 | firmware = emonTH_DHT22_DS18B20_RFM69CW 4 | hardware = emonTH_(Node_ID_Switch_DIP1:OFF_DIP2:ON) 5 | [[[rx]]] 6 | names = temperature, external temperature, humidity, battery 7 | datacode = h 8 | scales = 0.1,0.1,0.1,0.1 9 | units = C,C,%,V 10 | -------------------------------------------------------------------------------- /conf/nodes/22: -------------------------------------------------------------------------------- 1 | [[22]] 2 | nodename = emonTH_4 3 | firmware = V1_5_emonTH_DHT22_DS18B20_RFM69CW 4 | hardware = emonTH_(Node_ID_Switch_DIP1:ON_DIP2:ON) 5 | [[[rx]]] 6 | names = temperature, external temperature, humidity, battery 7 | datacode = h 8 | scales = 0.1,0.1,0.1,0.1 9 | units = C,C,%,V 10 | -------------------------------------------------------------------------------- /conf/interfacer_examples/MBUS/mbus.emonhub.conf: -------------------------------------------------------------------------------- 1 | [[MBUS]] 2 | Type = EmonHubMBUSInterfacer 3 | [[[init_settings]]] 4 | device = /dev/ttyUSB0 5 | baud = 4800 6 | [[[runtimesettings]]] 7 | pubchannels = ToEmonCMS, 8 | address = 100 9 | pages = 3,1 10 | read_interval = 10 11 | nodename = MBUS 12 | -------------------------------------------------------------------------------- /conf/interfacer_examples/Influx/influx.emonhub.conf: -------------------------------------------------------------------------------- 1 | [[MBUS]] 2 | Type = EmonHubMBUSInterfacer 3 | [[[init_settings]]] 4 | device = /dev/ttyUSB0 5 | baud = 4800 6 | [[[runtimesettings]]] 7 | pubchannels = ToEmonCMS, 8 | address = 100 9 | pages = 3,1 10 | read_interval = 10 11 | nodename = MBUS 12 | -------------------------------------------------------------------------------- /conf/nodes/23: -------------------------------------------------------------------------------- 1 | [[23]] 2 | nodename = emonTH_5 3 | firmware = V2.x_emonTH_DHT22_DS18B20_RFM69CW_Pulse 4 | hardware = emonTH_(Node_ID_Switch_DIP1:OFF_DIP2:OFF) 5 | [[[rx]]] 6 | names = temperature, external temperature, humidity, battery, pulseCount 7 | datacodes = h,h,h,h,L 8 | scales = 0.1,0.1,0.1,0.1,1 9 | units = C,C,%,V,p 10 | -------------------------------------------------------------------------------- /conf/nodes/24: -------------------------------------------------------------------------------- 1 | [[24]] 2 | nodename = emonTH_6 3 | firmware = V2.x_emonTH_DHT22_DS18B20_RFM69CW_Pulse 4 | hardware = emonTH_(Node_ID_Switch_DIP1:ON_DIP2:OFF) 5 | [[[rx]]] 6 | names = temperature, external temperature, humidity, battery, pulseCount 7 | datacodes = h,h,h,h,L 8 | scales = 0.1,0.1,0.1,0.1,1 9 | units = C,C,%,V,p 10 | -------------------------------------------------------------------------------- /conf/nodes/25: -------------------------------------------------------------------------------- 1 | [[25]] 2 | nodename = emonTH_7 3 | firmware = V2.x_emonTH_DHT22_DS18B20_RFM69CW_Pulse 4 | hardware = emonTH_(Node_ID_Switch_DIP1:OFF_DIP2:ON) 5 | [[[rx]]] 6 | names = temperature, external temperature, humidity, battery, pulseCount 7 | datacodes = h,h,h,h,L 8 | scales = 0.1,0.1,0.1,0.1,1 9 | units = C,C,%,V,p 10 | -------------------------------------------------------------------------------- /conf/nodes/26: -------------------------------------------------------------------------------- 1 | [[26]] 2 | nodename = emonTH_8 3 | firmware = V2.x_emonTH_DHT22_DS18B20_RFM69CW_Pulse 4 | hardware = emonTH_(Node_ID_Switch_DIP1:ON_DIP2:ON) 5 | [[[rx]]] 6 | names = temperature, external temperature, humidity, battery, pulseCount 7 | datacodes = h,h,h,h,L 8 | scales = 0.1,0.1,0.1,0.1,1 9 | units = C,C,%,V,p 10 | -------------------------------------------------------------------------------- /conf/interfacer_examples/DS18B20/sds011.emonhub.conf: -------------------------------------------------------------------------------- 1 | [[DS18B20]] 2 | Type = EmonHubDS18B20Interfacer 3 | [[[init_settings]]] 4 | [[[runtimesettings]]] 5 | pubchannels = ToEmonCMS, 6 | read_interval = 10 7 | nodename = sensors 8 | # ids = 28-000008e2db06, 28-000009770529, 28-0000096a49b4 9 | # names = ambient, cyl_bot, cyl_top 10 | -------------------------------------------------------------------------------- /conf/interfacer_examples/Emoncms/emoncms.emonhub.conf: -------------------------------------------------------------------------------- 1 | [[emoncmsorg]] 2 | Type = EmonHubEmoncmsHTTPInterfacer 3 | [[[init_settings]]] 4 | [[[runtimesettings]]] 5 | pubchannels = ToRFM12, 6 | subchannels = ToEmonCMS, 7 | url = https://emoncms.org 8 | apikey = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 9 | senddata = 1 10 | sendstatus = 1 11 | -------------------------------------------------------------------------------- /conf/nodes/5: -------------------------------------------------------------------------------- 1 | [[5]] 2 | nodename = emonPi 3 | firmware = emonPi_RFM69CW_RF12Demo_DiscreteSampling.ino 4 | hardware = emonpi 5 | [[[rx]]] 6 | names = power1,power2,power1_plus_power2,Vrms,T1,T2,T3,T4,T5,T6,pulseCount 7 | datacodes = h, h, h, h, h, h, h, h, h, h, L 8 | scales = 1,1,1,0.01,0.1,0.1,0.1,0.1,0.1,0.1,1 9 | units = W,W,W,V,C,C,C,C,C,C,p 10 | -------------------------------------------------------------------------------- /conf/nodes/7: -------------------------------------------------------------------------------- 1 | [[7]] 2 | nodename = emonTx_4 3 | firmware =V2_3_emonTxV3_4_DiscreteSampling 4 | hardware = emonTx_(NodeID_DIP_Switch1:OFF) 5 | [[[rx]]] 6 | names = power1, power2, power3, power4, Vrms, temp1, temp2, temp3, temp4, temp5, temp6, pulse 7 | datacodes = h,h,h,h,h,h,h,h,h,h,h,L 8 | scales = 1,1,1,1,0.01,0.1,0.1, 0.1,0.1,0.1,0.1,1 9 | units =W,W,W,W,V,C,C,C,C,C,C,p 10 | -------------------------------------------------------------------------------- /conf/nodes/8: -------------------------------------------------------------------------------- 1 | [[8]] 2 | nodename = emonTx_3 3 | firmware =V2_3_emonTxV3_4_DiscreteSampling 4 | hardware = emonTx_(NodeID_DIP_Switch1:OFF) 5 | [[[rx]]] 6 | names = power1, power2, power3, power4, Vrms, temp1, temp2, temp3, temp4, temp5, temp6, pulse 7 | datacodes = h,h,h,h,h,h,h,h,h,h,h,L 8 | scales = 1,1,1,1,0.01,0.1,0.1, 0.1,0.1,0.1,0.1,1 9 | units =W,W,W,W,V,C,C,C,C,C,C,p 10 | -------------------------------------------------------------------------------- /conf/interfacer_examples/graphite/graphite.emonhub.conf: -------------------------------------------------------------------------------- 1 | [[Graphite]] 2 | Type = EmonHubGraphiteInterfacer 3 | [[[init_settings]]] 4 | [[[runtimesettings]]] 5 | pubchannels = ToRFM12, 6 | subchannels = ToEmonCMS, 7 | graphite_host = graphite.example.com, 8 | graphite_port = 2003, 9 | senddata = 1, 10 | interval = 30, 11 | prefix = emonpi 12 | -------------------------------------------------------------------------------- /conf/interfacer_examples/Redis/readme.md: -------------------------------------------------------------------------------- 1 | ### Redis 2 | 3 | Writes latest sensor values (or emonhub cargo objects) to redis keys. Makes it easy to then pick up the values in a separate python script used for automation. 4 | 5 | [[Redis]] 6 | Type = EmonHubRedisInterfacer 7 | [[[init_settings]]] 8 | redis_host = localhost 9 | redis_port = 6379 10 | redis_db = 0 11 | [[[runtimesettings]]] 12 | subchannels = ToEmonCMS, 13 | prefix = "emonhub:" 14 | -------------------------------------------------------------------------------- /examples/control_sender.py: -------------------------------------------------------------------------------- 1 | import time 2 | import paho.mqtt.client as mqtt 3 | 4 | client = mqtt.Client() 5 | client.connect("127.0.0.1", 1883, 60) 6 | 7 | while True: 8 | topic = "emonhub/tx/30/values" 9 | payload = "1,1850" 10 | client.publish(topic, payload=payload, qos=0, retain=False) 11 | # client.loop() 12 | time.sleep(5.0) 13 | payload = "0,1850" 14 | client.publish(topic, payload=payload, qos=0, retain=False) 15 | # client.loop() 16 | time.sleep(5.0) 17 | -------------------------------------------------------------------------------- /examples/mqtt_reader.py: -------------------------------------------------------------------------------- 1 | import time 2 | import paho.mqtt.client as mqtt 3 | 4 | def on_connect(client, userdata, rc): 5 | print("Connected with result code "+str(rc)) 6 | client.subscribe("emonhub/#") 7 | 8 | def on_message(client, userdata, msg): 9 | print(msg.topic+" "+str(msg.payload)) 10 | 11 | client = mqtt.Client() 12 | client.on_connect = on_connect 13 | client.on_message = on_message 14 | client.connect("127.0.0.1", 1883, 60) 15 | 16 | while True: 17 | client.loop() 18 | time.sleep(0.1) 19 | -------------------------------------------------------------------------------- /conf/interfacer_examples/RF69/readme.md: -------------------------------------------------------------------------------- 1 | ### RF69 Interfacer 2 | 3 | Read data directly from a RFM69cw module on a RaspberryPi: 4 | 5 | ```text 6 | [[SPI]] 7 | Type = EmonHubRF69Interfacer 8 | [[[init_settings]]] 9 | nodeid = 5 10 | group = 210 11 | [[[runtimesettings]]] 12 | pubchannels = ToEmonCMS, 13 | ``` 14 | 15 | Steps to get working: 16 | 17 | 1. Enable SPI in raspi-config: 18 | 2. sudo adduser emonhub spi 19 | 3. sudo apt-get install python3-spidev (may just upgrade an existing package) 20 | -------------------------------------------------------------------------------- /conf/default/emonhub: -------------------------------------------------------------------------------- 1 | ## emonHub settings 2 | 3 | # Edit this to configure the parameters used in 4 | # the /etc/init.d/emonhub script. 5 | 6 | # This file should be deployed to /etc/default/emonhub 7 | # unless you have edited the init.d file to give an 8 | # alternate SYSCONF_PATH 9 | 10 | # Specify the directory in which emonhub.py is found: 11 | EMONHUB_PATH=/usr/share/emonhub/ 12 | 13 | # Specify the full config file path: 14 | EMONHUB_CONFIG=/home/pi/data/emonhub.conf 15 | 16 | # Specify the full log file path: 17 | EMONHUB_LOG=/var/log/emonhub/emonhub.log 18 | -------------------------------------------------------------------------------- /conf/interfacer_examples/SDS011/readme.md: -------------------------------------------------------------------------------- 1 | ### SDS011 Air Quality Sensor 2 | 3 | Read data from the SDS011 particulate matter sensor. Updated implementation puts sensor to sleep between readings. 4 | 5 | **readinterval:** Interval between readings in minutes, it is recommended to read every 5 minutes to preserve sensor lifespan. 6 | 7 | ```text 8 | [[SDS011]] 9 | Type = EmonHubSDS011Interfacer 10 | [[[init_settings]]] 11 | com_port = /dev/ttyUSB0 12 | [[[runtimesettings]]] 13 | readinterval = 5 14 | nodename = SDS011 15 | pubchannels = ToEmonCMS, 16 | ``` 17 | -------------------------------------------------------------------------------- /conf/interfacer_examples/PowerWall/readme.md: -------------------------------------------------------------------------------- 1 | ### Tesla Power Wall Interfacer 2 | 3 | This interfacer fetches the state of charge of a Tesla Power Wall on the local network. Enter your PowerWall IP-address or hostname in the URL section of the following emonhub.conf configuration: 4 | 5 | ```text 6 | [[PowerWall]] 7 | Type = EmonHubTeslaPowerWallInterfacer 8 | [[[init_settings]]] 9 | [[[runtimesettings]]] 10 | pubchannels = ToEmonCMS, 11 | name = powerwall 12 | url = http://POWERWALL-IP/api/system_status/soe 13 | readinterval = 10 14 | ``` 15 | -------------------------------------------------------------------------------- /conf/interfacer_examples/GoodWe/readme.md: -------------------------------------------------------------------------------- 1 | ### Goodwe Interfacer 2 | 3 | This interfacer fetches the state of charge of a GoodWe ET inverter on the local network. Enter your GoodWe Wifi IP-address ip section of the following emonhub.conf configuration: 4 | 5 | ```text 6 | [[GoodWe]] 7 | Type = EmonHubGoodWeInterfacer 8 | [[[init_settings]]] 9 | [[[runtimesettings]]] 10 | pubchannels = ToEmonCMS, 11 | name = goodwe 12 | ip = 192.168.0.100 13 | port = 8899 14 | readinterval = 10 15 | retries = 3 16 | timeout = 2 17 | ``` 18 | -------------------------------------------------------------------------------- /conf/interfacer_examples/SDM630/sdm630.emonhub.conf: -------------------------------------------------------------------------------- 1 | [[SDM630]] 2 | Type = EmonHubMinimalModbusInterfacer 3 | [[[init_settings]]] 4 | device = /dev/ttyUSB0 5 | baud = 9600 6 | [[[runtimesettings]]] 7 | pubchannels = ToEmonCMS, 8 | read_interval = 10 9 | nodename = SDM630 10 | [[[[meters]]]] 11 | [[[[[electric]]]]] 12 | address = 1 13 | registers = 0,2,4,52,12,14,16,72,90,92,94,74,68,68,99,6,8,10 14 | names = V1,V2,V3,P_total,P1,P2,P3,EI_total,EI1,EI2,EI3,EE_total,EE1,EE2,EE3,I1,I2,I3 15 | precision = 2,2,2,1,1,1,1,3,3,3,3,3,3,3,3,3,3,3 16 | -------------------------------------------------------------------------------- /conf/interfacer_examples/rayleigh-ri-d35-100/rayleigh-ri-d35-100.emonhub.conf: -------------------------------------------------------------------------------- 1 | [[modbus]] 2 | Type = EmonHubMinimalModbusInterfacer 3 | [[[init_settings]]] 4 | device = /dev/ttyUSB0 5 | baud = 9600 6 | [[[runtimesettings]]] 7 | pubchannels = ToEmonCMS, 8 | read_interval = 10 9 | nodename = electricity 10 | # prefix = ri_ 11 | [[[[meters]]]] 12 | [[[[[ri-d35-100]]]]] 13 | address = 1 14 | registers = 3,5,15,19,23,21,27 15 | names = EI,EE,P,VA,I,V,FR,PF 16 | precision = 1,1,1,2,2,1,2 17 | scales = 1,1,1000,1,1,1,1 18 | 19 | -------------------------------------------------------------------------------- /conf/interfacer_examples/smasolar/smasolar.emonhub.conf: -------------------------------------------------------------------------------- 1 | ####################################################################### 2 | ####################### emonhub.conf ######################### 3 | ####################################################################### 4 | 5 | [interfacers] 6 | 7 | ### This interfacer manages connections for EmonHubSMASolarInterfacer 8 | [[SMASolar]] 9 | Type = EmonHubSMASolarInterfacer 10 | [[[init_settings]]] 11 | inverteraddress= 00:80:25:1D:AC:53 12 | inverterpincode = 0000 13 | timeinverval = 5 14 | nodeid = 29 15 | packettrace = 0 16 | [[[runtimesettings]]] 17 | pubchannels = ToEmonCMS, 18 | -------------------------------------------------------------------------------- /service/emonhub.service.bullseye: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=emonHub data multiplexer 3 | # The config file lives in /etc/emonhub/emonhub.conf 4 | # The log file lives in /var/log/emonhub/emonhub.log 5 | After= network.target 6 | 7 | [Service] 8 | Type=exec 9 | ExecStart=/usr/local/bin/emonhub/emonhub.py --config-file=/etc/emonhub/emonhub.conf --logfile=/var/log/emonhub/emonhub.log 10 | User=pi 11 | Environment='USER=pi' 12 | Environment='LOG_PATH=/var/log/emonhub' 13 | PermissionsStartOnly=true 14 | ExecStartPre=/bin/mkdir -p ${LOG_PATH} 15 | ExecStartPre=/bin/chown ${USER} ${LOG_PATH} 16 | 17 | Restart=always 18 | RestartSec=5 19 | 20 | SyslogIdentifier=emonhub 21 | 22 | [Install] 23 | WantedBy=multi-user.target 24 | -------------------------------------------------------------------------------- /conf/interfacer_examples/JaguarLandRover/jlr.emonhub.conf: -------------------------------------------------------------------------------- 1 | ####################################################################### 2 | ####################### emonhub.conf ######################### 3 | ####################################################################### 4 | 5 | [interfacers] 6 | 7 | ### Retrieves data from the Jaguar Land Rover API for electric car monitoring 8 | [[JaguarLandRover]] 9 | Type = EmonHubJaguarLandRoverInterfacer 10 | [[[init_settings]]] 11 | timeinverval = 600 12 | duringchargetimeinterval = 60 13 | nodeid = 28 14 | jlrusername = USERNAMEGOESHERE 15 | jlrpassword = PASSWORDGOESHERE 16 | [[[runtimesettings]]] 17 | pubchannels = ToEmonCMS, 18 | -------------------------------------------------------------------------------- /conf/interfacer_examples/RFM2Pi/rfm2pi.emonhub.conf: -------------------------------------------------------------------------------- 1 | [[RFM2Pi]] 2 | Type = EmonHubJeeInterfacer 3 | [[[init_settings]]] 4 | com_port = /dev/ttyAMA0 5 | com_baud = 38400 # 9600 for old RFM12Pi 6 | [[[runtimesettings]]] 7 | pubchannels = ToEmonCMS, 8 | subchannels = ToRFM12, 9 | 10 | group = 210 11 | frequency = 433 12 | baseid = 5 # emonPi / emonBase nodeID 13 | quiet = true # Report incomplete RF packets (not implemented on emonPi) 14 | calibration = 230V # (UK/EU: 230V, US: 110V) 15 | # interval = 0 # Interval to transmit time to emonGLCD (seconds) 16 | -------------------------------------------------------------------------------- /conf/nodes/10: -------------------------------------------------------------------------------- 1 | [[10]] 2 | nodename = emonTx_1 3 | firmware =V1_6_emonTxV3_4_DiscreteSampling 4 | hardware = emonTx_(NodeID_DIP_Switch1:OFF) 5 | [[[rx]]] 6 | names = power1, power2, power3, power4, Vrms, temp1, temp2, temp3, temp4, temp5, temp6, pulse #Firmware V1.6 7 | #names = power1, power2, power3, power4, Vrms, temp #Firmware =>$emonhub_location 30 | cat $path/$var >> $emonhub_location 31 | echo "Added node $var to emonhub.conf" 32 | fi 33 | fi 34 | done 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # emonHub 2 | 3 | EmonHub is a piece of software running on the emonPi and emonBase that can read/subscribe or send/publish data to and from a multitude of services. It is primarily used as the bridge between the OpenEnergyMonitor monitoring hardware and the Emoncms software but it can also be used to read in data from a number of other sources, providing an easy way to interface with a wider range of sensors. 4 | 5 | ## Documentation 6 | 7 | - [Overview](https://docs.openenergymonitor.org/emonhub/overview.html) 8 | - [Configuration](https://docs.openenergymonitor.org/emonhub/configuration.html) 9 | - [Interfacers](https://docs.openenergymonitor.org/emonhub/emonhub-interfacers.html) 10 | 11 | Alternatively view the documentation directly on github here: [emonhub/docs](docs). 12 | 13 | ## Licence 14 | 15 | GNU Affero General Public License 16 | -------------------------------------------------------------------------------- /conf/interfacer_examples/vedirect/mppt.vedirect.emonhub.conf: -------------------------------------------------------------------------------- 1 | [[VEDirect]] 2 | Type = EmonHubVEDirectInterfacer 3 | [[[init_settings]]] 4 | com_port = /dev/ttyUSB0 5 | com_baud = 19200 6 | toextract = V,VPV,PPV,I,IL,LOAD,Relay,H19,H20,H21,H22,H23,ERR,CS,FW,PID,HSDS 7 | poll_interval = 10 8 | [[[runtimesettings]]] 9 | nodeoffset = 9 #make sure this matches with nodename below 10 | pubchannels = ToEmonCMS, 11 | subchannels = ToBlueSolarMPTT, 12 | basetopic = emonhub/ 13 | 14 | [nodes] 15 | 16 | [[9]] 17 | nodename = BlueSolarMPTT 18 | [[[rx]]] 19 | names = V,VPV,PPV,I,IL,LOAD,Relay,H19,H20,H21,H22,H23,ERR,CS,FW,PID,HSDS 20 | datacode = 0 21 | scales = 0.001,0.001,0.001,0.001,1,1,1,1,0.001,1,0.001,1,1,1,1,1,1,1 22 | units = V,V,W,A,A,1,1,kWh,kWh,W,kWh,W,1,1,1,1,1,1 23 | -------------------------------------------------------------------------------- /conf/interfacer_examples/SDM120/sdm120.emonhub.conf: -------------------------------------------------------------------------------- 1 | [[SDM120]] 2 | Type = EmonHubMinimalModbusInterfacer 3 | [[[init_settings]]] 4 | device = /dev/ttyUSB0 5 | baud = 2400 6 | parity = none 7 | datatype = float 8 | [[[runtimesettings]]] 9 | pubchannels = ToEmonCMS, 10 | read_interval = 10 11 | nodename = sdm120 12 | # prefix = sdm_ 13 | [[[[meters]]]] 14 | [[[[[sdm120a]]]]] 15 | address = 1 16 | registers = 0,6,12,18,30,70,72,74,76 17 | names = V,I,P,VA,PF,FR,EI,EE,RI 18 | precision = 2,3,1,1,3,3,3,3,3 19 | [[[[[sdm120b]]]]] 20 | address = 2 21 | registers = 0,6,12,18,30,70,72,74,76 22 | names = V,I,P,VA,PF,FR,EI,EE,RI 23 | precision = 2,3,1,1,3,3,3,3,3 24 | -------------------------------------------------------------------------------- /conf/interfacer_examples/MQTT/mqtt.emonhub.conf: -------------------------------------------------------------------------------- 1 | [[MQTT]] 2 | 3 | Type = EmonHubMqttInterfacer 4 | [[[init_settings]]] 5 | mqtt_host = 127.0.0.1 6 | mqtt_port = 1883 7 | mqtt_user = emonpi 8 | mqtt_passwd = emonpimqtt2016 9 | 10 | [[[runtimesettings]]] 11 | # pubchannels = ToRFM12, 12 | subchannels = ToEmonCMS, 13 | 14 | # emonhub/rx/10/values format 15 | # Use with emoncms Nodes module 16 | node_format_enable = 0 17 | node_format_basetopic = emonhub/ 18 | 19 | # emon/emontx/power1 format - use with Emoncms MQTT input 20 | # http://github.com/emoncms/emoncms/blob/master/docs/RaspberryPi/MQTT.md 21 | nodevar_format_enable = 1 22 | nodevar_format_basetopic = emon/ 23 | 24 | # Single JSON payload published - use with Emoncms MQTT 25 | node_JSON_enable = 0 26 | node_JSON_basetopic = emon/ 27 | -------------------------------------------------------------------------------- /conf/interfacer_examples/Pulse/readme.md: -------------------------------------------------------------------------------- 1 | ### Direct Pulse counting 2 | 3 | This EmonHub interfacer can be used to read directly from pulse counter connected to a GPIO pin on the RaspberryPi. 4 | 5 | - **pulse_pin:** Pi GPIO pin number must be specified. Create a second interfacer for more than one pulse sensor 6 | - **Rate_limit:** The rate in seconds at which the interfacer will pass data to emonhub for sending on. Too short and pulses will be missed. Pulses are accumulated in this period. 7 | - **nodeoffset:** Default NodeID is 0. Use nodeoffset to set NodeID 8 | 9 | Example Pulse counting EmonHub configuration: 10 | 11 | [[pulse]] 12 | Type = EmonHubPulseCounterInterfacer 13 | [[[init_settings]]] 14 | pulse_pin = 15 15 | # bouncetime = 2 16 | # rate_limit = 2 17 | [[[runtimesettings]]] 18 | pubchannels = ToEmonCMS, 19 | nodeoffset = 3 20 | 21 | -------------------------------------------------------------------------------- /src/Cargo.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | class EmonHubCargo: 4 | uri = 0 5 | 6 | # The class "constructor" - It's actually an initializer 7 | def __init__(self, timestamp, target, nodeid, nodename, names, realdata, rssi, rawdata): 8 | EmonHubCargo.uri += 1 9 | self.uri = EmonHubCargo.uri 10 | self.timestamp = float(timestamp) 11 | self.target = int(target) 12 | self.nodeid = int(nodeid) 13 | self.nodename = nodename 14 | self.names = names 15 | self.realdata = realdata 16 | self.rssi = int(rssi) 17 | 18 | # self.datacodes = [] 19 | # self.datacode = "" 20 | # self.scale = 0 21 | # self.scales = [] 22 | self.rawdata = rawdata 23 | self.encoded = {} 24 | # self.realdatacodes = [] 25 | 26 | def new_cargo(rawdata="", nodename=False, names=[], realdata=[], nodeid=0, timestamp=0.0, target=0, rssi=0.0): 27 | return EmonHubCargo(timestamp or time.time(), target, nodeid, nodename, names, realdata, rssi, rawdata) 28 | -------------------------------------------------------------------------------- /conf/interfacer_examples/samsung-ashp/samsung-ashp.emonhub.conf: -------------------------------------------------------------------------------- 1 | [[SAMSUNG-ASHP-MIB19N]] 2 | Type = EmonHubMinimalModbusInterfacer 3 | [[[init_settings]]] 4 | device = /dev/ttyUSB0 5 | baud = 9600 6 | parity = even 7 | datatype = int 8 | [[[runtimesettings]]] 9 | pubchannels = ToEmonCMS, 10 | read_interval = 20 11 | nodename = samsung-ashp 12 | # prefix = sdm_ 13 | [[[[meters]]]] 14 | [[[[[ashp]]]]] 15 | device_type = samsung 16 | address = 1 17 | registers = 75,74,72,65,66,68,52,59,58,2,79,87,5,89,4,85,88 18 | names = dhw_temp,dhw_target,dhw_status,return_temp,flow_temp,flow_target,heating_status,indoor_temp,indoor_target, defrost_status,away_status,flow_rate,outdoor_temp,3_way_valve, compressor_freq, dhw_boost_heater_status, compressor_freq_percentage 19 | scales = 0.1,0.1,1,0.1,0.1,0.1,1,0.1,0.1,1,1,0.1,0.1,1,1,1,1 20 | precision = 2,2,1,2,2,2,1,2,2,1,1,2,2,1,1,1,1 21 | -------------------------------------------------------------------------------- /conf/interfacer_examples/OEM/readme.md: -------------------------------------------------------------------------------- 1 | ### OEM Interfacer 2 | 3 | Replaces EmonHubJeeInterfacer with a more flexible implementation that can accept a range of different formats from connected devices, including OpenEnergyMonitor devices. Here are a number of supported data formats: 4 | 5 | - Decimal space seperate representation of RFM binary data e.g OK 5 0 0 0 0 (-0) 6 | - KEY:VALUE format e.g power1:100,power2:200 7 | - JSON format e.g {"power1":100,"power2":200} 8 | 9 | Example configuration: 10 | 11 | ```text 12 | [[OEM]] 13 | Type = EmonHubOEMInterfacer 14 | [[[init_settings]]] 15 | com_port = /dev/ttyAMA0 16 | com_baud = 115200 17 | [[[runtimesettings]]] 18 | pubchannels = ToEmonCMS, 19 | 20 | ``` 21 | 22 | ``` 23 | [[usbdata]] 24 | Type = EmonHubOEMInterfacer 25 | [[[init_settings]]] 26 | com_port = /dev/ttyUSB0 27 | com_baud = 115200 28 | [[[runtimesettings]]] 29 | pubchannels = ToEmonCMS, 30 | subchannels = ToRFM12, 31 | ``` 32 | -------------------------------------------------------------------------------- /conf/interfacer_examples/Renogy/Renogy.emonhub.conf: -------------------------------------------------------------------------------- 1 | ### You will need to install pip: sudo apt-get install python-pip 2 | ### And you'll need pymodbus installed: pip install -U pymodbus 3 | 4 | #Configuration is for Renogy Rover on USB0 5 | [[Renogy]] 6 | Type = EmonHubModbusRenogyInterfacer 7 | [[[init_settings]]] 8 | com_port = /dev/ttyUSB0 9 | com_baud = 9600 10 | toextract = BatteryPercent,Charging_Stage,BatteryTemp_F,SolarVoltage,SolarCurrent,SolarPower,PowerGenToday 11 | poll_interval = 30 # More fields can be found in datalist.py 12 | [[[runtimesettings]]] 13 | nodeoffset = 28 #make sure this matches with nodename below 14 | pubchannels = ToEmonCMS, 15 | subchannels = ToRenogy, 16 | basetopic = emonhub/ 17 | 18 | [[28]] 19 | nodename = Renogy 20 | [[[rx]]] 21 | names = BatteryPercent,Charging_Stage,BatteryTemp_F,SolarVoltage,SolarCurrent,SolarPower,PowerGenToday 22 | datacode = 0 23 | scales = 1, 1, 1, 0.1,0.01,1,1 24 | units = %, s, s, V, A,W,W 25 | -------------------------------------------------------------------------------- /src/emonhub_coder.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | # Initialize nodes data 4 | # FIXME this shouldn't live here 5 | nodelist = {} 6 | 7 | 8 | def check_datacode(datacode): 9 | # Ensure little-endian & standard sizes used 10 | e = '<' 11 | try: 12 | return struct.calcsize(e + datacode) 13 | except struct.error: 14 | return False 15 | 16 | 17 | def decode(datacode, frame): 18 | # Ensure little-endian & standard sizes used 19 | e = '<' 20 | 21 | # set the base data type to bytes 22 | b = 'B' 23 | 24 | # get data size from data code 25 | s = int(check_datacode(datacode)) 26 | 27 | result = struct.unpack(e + datacode[0], struct.pack(e + b*s, *frame)) 28 | return result[0] 29 | 30 | def encode(datacode, value): 31 | # Ensure little-endian & standard sizes used 32 | e = '<' 33 | 34 | # set the base data type to bytes 35 | b = 'B' 36 | 37 | # get data size from data code 38 | s = int(check_datacode(datacode)) 39 | 40 | #value = 60 41 | #datacode = "b" 42 | return struct.unpack(e + b*s, struct.pack(e + datacode, value)) 43 | -------------------------------------------------------------------------------- /conf/interfacer_examples/Influx/readme.md: -------------------------------------------------------------------------------- 1 | ### Influx Writer 2 | 3 | The influx writer uses the 1.0 api to write in bulk (100) every influx_interval in seconds. 4 | 5 | 6 | - **influx_interval:** The default interval is 30 seconds. For visualization only this should be enough. If you want more details, you can lower this value, but this will put more stress on your system. 7 | - **influx_host:** The host the influx service runs. Defaults to localhost. 8 | - **influx_port:** The port the influx service runs. Defaults to 8086. 9 | - **influx_user:** The user for posting data into the influx db. Defaults to emoncms. 10 | - **influx_passwd:** The password for posting data into the influx db. Defaults to emoncmspw. 11 | - **influx_db:** The database where your timeseries will be stored in the influx db. Defaults to emoncms. 12 | 13 | 14 | 15 | ```text 16 | [[Influx]] 17 | loglevel = DEBUG 18 | Type = EmonHubInfluxInterfacer 19 | [[[init_settings]]] 20 | influx_port = 8086 21 | influx_host = localhost 22 | influx_user = grafana 23 | influx_passwd = samplepw 24 | influx_db = home 25 | 26 | [[[runtimesettings]]] 27 | subchannels = ToEmonCMS, 28 | ``` 29 | -------------------------------------------------------------------------------- /src/interfacers/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = [ 2 | "EmonHubPulseCounterInterfacer", 3 | "EmonHubDigitalInputInterfacer", 4 | "EmonHubSocketInterfacer", 5 | "EmonHubSerialInterfacer", 6 | "EmonHubJeeInterfacer", 7 | "EmonHubOEMInterfacer", 8 | "EmonHubSunampInterfacer", 9 | "EmonHubRF69Interfacer", 10 | "EmonHubRFM69LPLInterfacer", 11 | "EmonHubPacketGenInterfacer", 12 | "EmonHubEmoncmsHTTPInterfacer", 13 | "EmonHubMqttInterfacer", 14 | "EmonHubTx3eInterfacer", 15 | "EmonHubVEDirectInterfacer", 16 | # "EmonHubSmilicsInterfacer", 17 | "EmonHubSMASolarInterfacer", 18 | "EmonHubGraphiteInterfacer", 19 | "EmonHubBMWInterfacer", 20 | "EmonHubJaguarLandRoverInterfacer", 21 | "EmonModbusTcpInterfacer", 22 | "EmonHubTeslaPowerWallInterfacer", 23 | "EmonHubSDS011Interfacer", 24 | "EmonHubModbusRenogyInterfacer", 25 | "EmonHubTemplateInterfacer", 26 | "EmonHubDS18B20Interfacer", 27 | "EmonHubRedisInterfacer", 28 | "EmonHubSDM120Interfacer", 29 | "EmonHubMBUSInterfacer", 30 | "EmonHubMinimalModbusInterfacer", 31 | "EmonHubBleInterfacer", 32 | "EmonHubGoodWeInterfacer", 33 | "EmonHubInfluxInterfacer" 34 | #"EmonFroniusModbusTcpInterfacer" 35 | ] 36 | -------------------------------------------------------------------------------- /conf/interfacer_examples/RFM2Pi/readme.md: -------------------------------------------------------------------------------- 1 | ### [[RFM2Pi]] 2 | 3 | The `[[RFM2Pi]]` interfacer section contains the settings to read from RFM69Pi / emonPi boards via GPIO internal serial port `/dev/ttyAMA0`. The default serial baud on all emonPi and RFM69Pi is `38400`. Older RFM12Pi boards using `9600` baud. 4 | 5 | The frequency and network group must match the hardware and other nodes on the network. 6 | 7 | The `calibration` config is used to set the calibration of the emonPi when using USA AC-AC adapters 110V. Set `calibration = 110V` when using USA AC-AC adapter. 8 | 9 | ```text 10 | [[RFM2Pi]] 11 | Type = EmonHubJeeInterfacer 12 | [[[init_settings]]] 13 | com_port = /dev/ttyAMA0 14 | com_baud = 38400 # 9600 for old RFM12Pi 15 | [[[runtimesettings]]] 16 | pubchannels = ToEmonCMS, 17 | subchannels = ToRFM12, 18 | 19 | group = 210 20 | frequency = 433 21 | baseid = 5 # emonPi / emonBase nodeID 22 | quiet = true # Report incomplete RF packets (not implemented on emonPi) 23 | calibration = 230V # (UK/EU: 230V, US: 110V) 24 | # interval = 0 # Interval to transmit time to emonGLCD (seconds) 25 | ``` 26 | -------------------------------------------------------------------------------- /conf/interfacer_examples/smilices/readme.md: -------------------------------------------------------------------------------- 1 | #Smilics interface# 2 | 3 | This interface starts a http server and listens for GET requests from Smilics products like the Wibeee 4 | 5 | ##Usage and configuration## 6 | There is a sample smilics.emonhub.conf file located in this directory. 7 | This is preconfigured to listen on port 8080 8 | 9 | ###Sample interfacer config within emonhub.conf ### 10 | # Sample configuration for Smilics products 11 | [[SMILICS_INTERFACE]] 12 | Type = EmonHubSmilicsInterfacer 13 | [[[init_settings]]] 14 | port = 8080 15 | [[[runtimesettings]]] 16 | pubchannels = ToEmonCMS 17 | subchannels = ToSmilics 18 | 19 | 20 | ### Sample Node declaration in emonhub.conf### 21 | Using the Wibeee mac-address, without colons, as node id. 22 | 23 | [[121111111111]] 24 | nodename = SMILICS01 25 | firmware =V120 26 | hardware = Smilics Wibeee 27 | [[[rx]]] 28 | names = power1, power2, power3, power_total, wh1, wh2, wh3, wh_total 29 | datacodes = h, h, h, h, h, h, h, h 30 | scales = 1, 1, 1, 1, 1, 1, 1, 1 31 | units = W, W, W, W, Wh, Wh, Wh, Wh 32 | 33 | 34 | With this config in place, you simply need to restart emonhub on our emonpi by ssh'ing into it and typing 35 | 36 | $>sudo service emonhub restart 37 | -------------------------------------------------------------------------------- /conf/interfacer_examples/MBUS/readme.md: -------------------------------------------------------------------------------- 1 | ### MBUS Reader for Electric and Heat meters 2 | 3 | Many electricity and heat meters are available with meter bus (MBUS) outputs. Using an MBUS to USB converter (coming soon), these can be read from an emonPi or emonBase. For heat pumps, this provides a convenient way of monitoring the heat output, flow temperature, return temperature, flow rate and cumulative heat energy provided by the system. 4 | 5 | - **baud:** The MBUS baud rate is typically 2400 or 4800. It is usually possible to check the baud rate of the meter using the meter configuration interface. 6 | - **read_interval:** Interval between readings in seconds. 7 | 8 | List attached meters as shown in the example below. 9 | 10 | - **address:** The address of the meter is also usually possible to find via the meter configuration interface. If in doubt try 0 or 254. 11 | - **type:** Available options include: standard, qalcosonic_e3, sontex531, sdm120 12 | 13 | ```text 14 | [[MBUS]] 15 | Type = EmonHubMBUSInterfacer 16 | [[[init_settings]]] 17 | device = /dev/ttyAMA0 18 | baud = 2400 19 | [[[runtimesettings]]] 20 | pubchannels = ToEmonCMS, 21 | read_interval = 10 22 | validate_checksum = False 23 | nodename = MBUS 24 | [[[[meters]]]] 25 | [[[[[sdm120]]]]] 26 | address = 1 27 | type = sdm120 28 | [[[[[qalcosonic]]]]] 29 | address = 2 30 | type = qalcosonic_e3 31 | ``` 32 | -------------------------------------------------------------------------------- /conf/interfacer_examples/Socket/readme.md: -------------------------------------------------------------------------------- 1 | ### Socket Interfacer 2 | 3 | The EmonHub socket interfacer is particularly useful for inputing data from a range of sources. e.g a script monitoring server status where you wish to post the result to both a local instance of emoncms and a remote instance of emoncms alongside other data from other sources such as rfm node data. 4 | 5 | As an example, the following python script will post a single line of data values on node 98 to an emonhub instance running locally and listening on port 8080: 6 | 7 | ```python 8 | import socket, time 9 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 10 | s.connect(('localhost', 8080)) 11 | s.sendall('98 3.8 1.6 5.2 80.3\r\n') 12 | ``` 13 | 14 | The following emonhub.conf interfacer definition will listen on the choosen socket and forward the data on the ToEmonCMS channel: 15 | 16 | ```text 17 | [[mysocketlistener]] 18 | Type = EmonHubSocketInterfacer 19 | [[[init_settings]]] 20 | port_nb = 8080 21 | [[[runtimesettings]]] 22 | pubchannels = ToEmonCMS, 23 | ``` 24 | 25 | **Timestamped data** 26 | 27 | To set a timestamp for the posted data add the timestamped property to the emonhub.conf runtimesettings section: 28 | 29 | ```text 30 | [[[runtimesettings]]] 31 | pubchannels = ToEmonCMS, 32 | timestamped = True 33 | ``` 34 | 35 | The python client example needs to include the timestamp e.g: 36 | 37 | ```python 38 | s.sendall(str(time.time())+' 98 3.8 1.6 5.2 80.3\r\n') 39 | ``` 40 | -------------------------------------------------------------------------------- /conf/interfacer_examples/directserial/readme.md: -------------------------------------------------------------------------------- 1 | Emonhub can read from a serial device directly e.g. `/dev/ttyUSB0` 2 | 3 | ## Data Format 4 | 5 | Data should be printed to serial (integer only) with space sperators in the format: 6 | 7 | `NODEID VAR1 VAR2 VAR3` ....and so on 8 | 9 | Here is an example of printing data from an Arduino sketch: 10 | 11 | 12 | ``` 13 | Serial.print(nodeID); Serial.print(' '); 14 | Serial.print(realPower1); Serial.print(' '); 15 | Serial.print(realPower2); Serial.print(' '); 16 | Serial.print(realPower3); Serial.print(' '); 17 | Serial.print(realPower4); Serial.print(' '); 18 | Serial.print(Vrms); Serial.println(); 19 | ``` 20 | 21 | ## Example emonhub Config 22 | 23 | In the `[interfacers]` section: 24 | 25 | ``` 26 | [[SerialDirect]] 27 | Type = EmonHubSerialInterfacer 28 | [[[init_settings]]] 29 | com_port = /dev/ttyUSB0 # or /dev/ttyAMA0 or/dev/ttyACM0 etc 30 | com_baud = 9600 # to match the baud of the connected device 31 | [[[runtimesettings]]] 32 | pubchannels = ToEmonCMS, 33 | ``` 34 | In the `[nodes]` section: 35 | 36 | ``` 37 | [[99]] 38 | nodename = my-serial-device 39 | [[[rx]]] 40 | names = power1, power2, power3, power4, vrms 41 | datacode = 0 # not essential as "0" is default datacode for serial interfacer 42 | scale = 1 # not essential as "1" is default scale for serial interfacer 43 | units =W,W,W,W,V 44 | ``` 45 | ## Debugging 46 | 47 | - Ensure the data received on the serial port is 100% numerical characters with no rogue spaces. Non-numerical characters could result in `Thread is dead` error. 48 | -------------------------------------------------------------------------------- /conf/interfacer_examples/DS18B20/readme.md: -------------------------------------------------------------------------------- 1 | ### Direct DS18B20 temperature sensing 2 | 3 | This EmonHub interfacer can be used to read directly from DS18B20 temperature sensors connected to the GPIO pins on the RaspberryPi. At present a couple of manual setup steps are required to enable DS18B20 temperature sensing before using this EmonHub interfacer. 4 | 5 | **Manual RaspberryPi configuration:** 6 | 7 | 1\. SSH into your RaspberryPi, open /boot/config.txt in an editor: 8 | 9 | sudo nano /boot/config.txt 10 | 11 | 2\. Add the following to the end of the file: 12 | 13 | dtoverlay=w1-gpio 14 | 15 | 3\. Exit and reboot the Pi 16 | 17 | sudo reboot 18 | 19 | 4\. SSH back in again and run the following to enable the required modules: 20 | 21 | sudo modprobe w1-gpio 22 | sudo modprobe w1-therm 23 | 24 | **Configuring the Interfacer:** 25 | 26 | Login to the local copy of Emoncms running on the emonPi/emonBase and navigate to Setup > EmonHub. Click on 'Edit Config' and add the following config in the interfacers section to enable reading from the temperature sensors. 27 | 28 | - **read_interval:** Interval between readings in seconds. 29 | - **ids:** This can be used to link specific sensors addresses to input names listed under the names property. 30 | - **names:** Names associated with sensor id's, ordered by index. 31 | 32 | Example DS18B20 EmonHub configuration: 33 | 34 | [[DS18B20]] 35 | Type = EmonHubDS18B20Interfacer 36 | [[[init_settings]]] 37 | [[[runtimesettings]]] 38 | pubchannels = ToEmonCMS, 39 | read_interval = 10 40 | nodename = sensors 41 | # ids = 28-000008e2db06, 28-000009770529, 28-0000096a49b4 42 | # names = ambient, cyl_bot, cyl_top 43 | 44 | -------------------------------------------------------------------------------- /conf/interfacer_examples/JaguarLandRover/readme.md: -------------------------------------------------------------------------------- 1 | # Jaguar Land Rover Interfacer # 2 | 3 | This interfacer collects data from Jagular Land Rover's InControl API. Data collected on vehicle state of charge is suitable for use with OpenEVSE via MQTT. 4 | 5 | Tested with Range Rover Velar PHEV. 6 | 7 | ## Readings ## 8 | 9 | The following values (in order) are determined from the Jaguar Land Rover API. 10 | 11 | * ODOMETER_MILES 12 | * EV_STATE_OF_CHARGE 13 | * EV_RANGE_ON_BATTERY_MILES 14 | * EV_RANGE_ON_BATTERY_KM 15 | * EV_CHARGING_RATE_SOC_PER_HOUR 16 | * EV_SECONDS_TO_FULLY_CHARGED 17 | * EV_CHARGING_STATUS (1 or 0) 18 | 19 | ## Sample config for emonhub.conf ## 20 | 21 | Sample configuration, add these settings under the [interfacers] tag. Changing username and password to match those for your account on https://incontrol.landrover.com/ 22 | 23 | ``` 24 | [[JaguarLandRover]] 25 | Type = EmonHubJaguarLandRoverInterfacer 26 | [[[init_settings]]] 27 | timeinverval = 600 28 | duringchargetimeinterval = 60 29 | nodeid = 28 30 | jlrusername = USERNAMEGOESHERE 31 | jlrpassword = PASSWORDGOESHERE 32 | [[[runtimesettings]]] 33 | pubchannels = ToEmonCMS, 34 | ``` 35 | 36 | ## Settings ## 37 | 38 | ### timeinverval ### 39 | Interval between taking readings from API. Normally 10 minutes (600 seconds) 40 | 41 | ### duringchargetimeinterval ### 42 | When charging the API updates more frequently, so update every 1 minute when charging is detected (60 seconds) 43 | 44 | ### nodeid ### 45 | The emonHub/emonCMS nodeId to use 46 | 47 | ### jlrusername ### 48 | Username as used in the https://incontrol.landrover.com/ site 49 | 50 | ### jlrpassword ### 51 | Password as used in the https://incontrol.landrover.com/ site 52 | -------------------------------------------------------------------------------- /conf/interfacer_examples/SDM120/readme.md: -------------------------------------------------------------------------------- 1 | ### SDM120-Modbus 2 | 3 | The SDM120-Modbus single phase electricity meter provides MID certified electricity monitoring up to 45A, ideal for monitoring the electricity supply of heat pumps and EV chargers. A USB to RS485 converter is needed to read from the modbus output of the meter such as: https://www.amazon.co.uk/gp/product/B07SD65BVF. The SDM120 meter comes in a number of different variants, be sure to order the version with a modbus output. 4 | 5 | **read_interval:** Interval between readings in seconds 6 | 7 | ## Single SDM120 Meter 8 | ``` 9 | [[SDM120]] 10 | Type = EmonHubMinimalModbusInterfacer 11 | [[[init_settings]]] 12 | device = /dev/ttyUSB0 13 | baud = 2400 14 | [[[runtimesettings]]] 15 | pubchannels = ToEmonCMS, 16 | read_interval = 10 17 | nodename = sdm120 18 | # prefix = sdm_ 19 | [[[[meters]]]] 20 | [[[[[sdm120]]]]] 21 | address = 1 22 | registers = 0,6,12,18,30,70,72,74,76 23 | names = V,I,P,VA,PF,FR,EI,EE,RI 24 | precision = 2,3,1,1,3,3,3,3,3 25 | ``` 26 | 27 | ## Multiple SDM120 Meters 28 | 29 | ``` 30 | [[SDM120]] 31 | Type = EmonHubMinimalModbusInterfacer 32 | [[[init_settings]]] 33 | device = /dev/ttyUSB0 34 | baud = 2400 35 | [[[runtimesettings]]] 36 | pubchannels = ToEmonCMS, 37 | read_interval = 10 38 | nodename = sdm120 39 | # prefix = sdm_ 40 | [[[[meters]]]] 41 | [[[[[sdm120a]]]]] 42 | address = 1 43 | registers = 0,6,12,18,30,70,72,74,76 44 | names = V,I,P,VA,PF,FR,EI,EE,RI 45 | precision = 2,3,1,1,3,3,3,3,3 46 | [[[[[sdm120b]]]]] 47 | address = 2 48 | registers = 0,6,12,18,30,70,72,74,76 49 | names = V,I,P,VA,PF,FR,EI,EE,RI 50 | precision = 2,3,1,1,3,3,3,3,3 51 | ``` 52 | -------------------------------------------------------------------------------- /conf/interfacer_examples/graphite/readme.md: -------------------------------------------------------------------------------- 1 | # Graphite interfacer 2 | 3 | Graphite is an enterprise-ready timeseries database capable of collecting millions of metrics per minute. 4 | It has a powerful API and function set for querying and manipulating the data. 5 | For details on graphite see 6 | 7 | It is frequently paired with [Grafana](http://grafana.org/) for dashboards and visualizations 8 | 9 | 10 | ## Usage and configuration 11 | Simply configure interface with host, port and interval of your graphite installation. 12 | Metrics get sent to a path similar to the nodevar format as MQTT interface. 13 | 14 | Ex: `.emonPi.power1` 15 | 16 | ### Parameters 17 | 18 | * `pubchannels` and `subchannels` 19 | 20 | Same usage as other interfacers 21 | 22 | * `graphite_host` 23 | 24 | Host or IP of your graphite server 25 | 26 | * `graphite_port` 27 | 28 | Graphite tcp metrics port. (Default: 2003) 29 | 30 | * `senddata` 31 | 32 | Set to 0 to disable sending metrics. (Default: 1) 33 | 34 | * `interval` 35 | 36 | Frequency, in seconds, to send metrics. (Default: 30) 37 | (Should be set the same in your graphite storage scheme) 38 | 39 | * `prefix` 40 | 41 | Prefix for graphite storage path. (Default: emonpi) 42 | 43 | ### Sample interfacer config within emonhub.conf 44 | 45 | ``` 46 | [[Graphite]] 47 | Type = EmonHubGraphiteInterfacer 48 | [[[init_settings]]] 49 | [[[runtimesettings]]] 50 | pubchannels = ToRFM12, 51 | subchannels = ToEmonCMS, 52 | graphite_host = graphite.example.com, 53 | graphite_port = 2003, 54 | senddata = 1, 55 | interval = 30, 56 | prefix = emonpi 57 | ``` 58 | 59 | With this config in place now you simply need to restart emonhub on your emonpi by ssh'ing into it and typing 60 | 61 | $> sudo service emonhub restart 62 | 63 | If there are any problems you can debug by looking inside /var/emonhub/emonhub.log 64 | -------------------------------------------------------------------------------- /conf/interfacer_examples/bmw/readme.md: -------------------------------------------------------------------------------- 1 | # BMW Connected Drive Interface # 2 | 3 | This is an interface between BMW Connected Drive cars (https://www.bmw-connecteddrive.co.uk) and emonCMS. 4 | 5 | Tested with BMW i3 electric car. This input emulates the same API calls that the www.bmw-connecteddrive.co.uk site uses. 6 | 7 | ## Readings ## 8 | 9 | The following values are extracted from the BMW API. 10 | 11 | * battery_size_max 12 | * beMaxRangeElectricKm 13 | * beMaxRangeElectricMile 14 | * beRemainingRangeElectricKm 15 | * beRemainingRangeElectricMile 16 | * beRemainingRangeFuelKm 17 | * beRemainingRangeFuelMile 18 | * fuelPercent 19 | * kombi_current_remaining_range_fuel 20 | * chargingLevelHv 21 | * mileage 22 | * remaining_fuel 23 | * soc_hv_percent 24 | * ChargingActive 25 | * rssi 26 | 27 | ## Sample config for emonhub.conf ## 28 | 29 | Sample configuration, add these settings under the [interfacers] tag. Changing username and password to match those on https://www.bmw-connecteddrive.co.uk 30 | 31 | ``` 32 | ### This interfacer manages communication to BMW API for electric car monitoring 33 | [[BMWi3]] 34 | Type = EmonHubBMWInterfacer 35 | [[[init_settings]]] 36 | timeinverval =600 37 | duringchargetimeinterval=60 38 | nodeid = 28 39 | tempcredentialfile = /tmp/bmwcredentials.json 40 | bmwapiusername = USERNAMEGOESHERE 41 | bmwapipassword = PASSWORDGOESHERE 42 | [[[runtimesettings]]] 43 | pubchannels = ToEmonCMS, 44 | 45 | ``` 46 | 47 | ## Settings ## 48 | 49 | ### timeinverval ### 50 | Interval between taking readings from API. Normally 10 minutes (600 seconds) - the car only updates every few hours so dont flood BMW servers 51 | 52 | ### duringchargetimeinterval ### 53 | When charging the API updates more frequently, so update every 1 minute when charging is detected (60 seconds) 54 | 55 | ### nodeid ### 56 | The emonHub/emonCMS nodeId to use 57 | 58 | ### tempcredentialfile ### 59 | File where temporary access credentials are persisted across emonHub restarts. 60 | 61 | ### bmwapiusername ### 62 | Username as used in the https://www.bmw-connecteddrive.co.uk site 63 | 64 | ### bmwapipassword ### 65 | Password as used in the https://www.bmw-connecteddrive.co.uk site 66 | -------------------------------------------------------------------------------- /conf/interfacer_examples/smilices/smilics.emonhub.conf: -------------------------------------------------------------------------------- 1 | ####################################################################### 2 | ####################### emonhub.conf ######################### 3 | ####################################################################### 4 | 5 | ## **LEGACY CONFIG: For use with 17thJune2015 emonPi/emonBase image and older ** 6 | ## (check image version by looking for file in /boot) 7 | ## Uses old CSV MQTT topic structure compatible with Emoncms Nodes 8 | ## Does not use MQTT server authentication 9 | 10 | ### emonHub configuration file, for info see documentation: 11 | ### https://docs.openenergymonitor.org/emonhub/configuration.html 12 | ####################################################################### 13 | ####################### emonHub settings ####################### 14 | ####################################################################### 15 | 16 | [hub] 17 | ### loglevel must be one of DEBUG, INFO, WARNING, ERROR, and CRITICAL 18 | loglevel = DEBUG #(default:WARNING) 19 | 20 | ####################################################################### 21 | ####################### Interfacers ####################### 22 | ####################################################################### 23 | 24 | [interfacers] 25 | ### This interfacer manages the RFM12Pi/RFM69Pi/emonPi module 26 | 27 | # Sample configuration for Smilics products 28 | [[SMILICS_INTERFACE]] 29 | Type = EmonHubSmilicsInterfacer 30 | [[[init_settings]]] 31 | port = 8080 32 | [[[runtimesettings]]] 33 | pubchannels = ToEmonCMS 34 | subchannels = ToSmilics 35 | 36 | 37 | ####################################################################### 38 | ####################### Nodes ####################### 39 | ####################################################################### 40 | 41 | [nodes] 42 | 43 | ## See config user guide: https://docs.openenergymonitor.org/emonhub/configuration.html 44 | 45 | 46 | [[121111111111]] 47 | nodename = SMILICS01 48 | firmware =V120 49 | hardware = Smilics Wibeee 50 | [[[rx]]] 51 | names = power1, power2, power3, power_total, wh1, wh2, wh3, wh_total 52 | datacodes = h, h, h, h, h, h, h, h 53 | scales = 1, 1, 1, 1, 1, 1, 1, 1 54 | units = W, W, W, W, Wh, Wh, Wh, Wh 55 | -------------------------------------------------------------------------------- /src/interfacers/tmp/EmonFroniusModbusTcpInterfacer.py: -------------------------------------------------------------------------------- 1 | from pymodbus.constants import Endian 2 | from pymodbus.payload import BinaryPayloadDecoder 3 | from . import EmonModbusTcpInterfacer as EmonModbusTcpInterfacer 4 | 5 | """class EmonModbusTcpInterfacer 6 | Monitors Solar Inverter using modbus tcp 7 | """ 8 | 9 | class EmonFroniusModbusTcpInterfacer(EmonModbusTcpInterfacer): 10 | 11 | def __init__(self, name, modbus_IP='192.168.1.10', modbus_port=502): 12 | """Initialize Interfacer 13 | com_port (string): path to COM port 14 | """ 15 | 16 | # Initialization 17 | super().__init__(name) 18 | 19 | # Connection opened by parent class INIT 20 | # Retrieve Fronius specific inverter info if connection successful 21 | self._log.debug("Fronius args: " + str(modbus_IP) + " - " + str(modbus_port)) 22 | self._log.debug("EmonFroniusModbusTcpInterfacer: Init") 23 | if self._modcon: 24 | # Display device firmware version and current settings 25 | self.info = ["", ""] 26 | #self._log.info("Modtcp Connected") 27 | r2 = self._con.read_holding_registers(40005 - 1, 4, unit=1) 28 | r3 = self._con.read_holding_registers(40021 - 1, 4, unit=1) 29 | invBrand = BinaryPayloadDecoder.fromRegisters(r2.registers, endian=Endian.Big) 30 | invModel = BinaryPayloadDecoder.fromRegisters(r3.registers, endian=Endian.Big) 31 | self._log.info(self.name + " Inverter: " + invBrand.decode_string(8) + " " + invModel.decode_string(8)) 32 | swDM = self._con.read_holding_registers(40037 - 1, 8, unit=1) 33 | swInv = self._con.read_holding_registers(40045 - 1, 8, unit=1) 34 | swDMdecode = BinaryPayloadDecoder.fromRegisters(swDM.registers, endian=Endian.Big) 35 | swInvdecode = BinaryPayloadDecoder.fromRegisters(swInv.registers, endian=Endian.Big) 36 | self._log.info(self.name + " SW Versions: Datamanager " + swDMdecode.decode_string(16) + "- Inverter " + swInvdecode.decode_string(16)) 37 | r1 = self._con.read_holding_registers(40070 - 1, 1, unit=1) 38 | ssModel = BinaryPayloadDecoder.fromRegisters(r1.registers, endian=Endian.Big) 39 | self._log.info(self.name + " SunSpec Model: " + str(ssModel.decode_16bit_uint())) 40 | -------------------------------------------------------------------------------- /conf/interfacer_examples/Emoncms/readme.md: -------------------------------------------------------------------------------- 1 | ### [[emoncmsorg]] 2 | 3 | The EmonHubEmoncmsHTTPInterfacer configuration that is used for sending data to emoncms.org (or any instance of emoncms). If you wish to use emoncms.org the only change to make here is to replace the blank apikey with your write apikey from emoncms.org found on the user account page. See [Setup Guide > Setup > Remote logging](https://guide.openenergymonitor.org/setup/remote). 4 | 5 | ```text 6 | [[emoncmsorg]] 7 | Type = EmonHubEmoncmsHTTPInterfacer 8 | [[[init_settings]]] 9 | [[[runtimesettings]]] 10 | pubchannels = ToRFM12, 11 | subchannels = ToEmonCMS, 12 | url = https://emoncms.org 13 | apikey = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 14 | senddata = 1 15 | sendstatus = 1 16 | sendnames = 1 17 | compress = 1 18 | ``` 19 | 20 | `sendstatus` - It is possible to the EmonHubEmoncmsHTTPInterfacer to send a 'ping' to the destination emoncms that can be picked up by the myip module which will then list the source IP address. This can be useful for remote login to a home emonpi if port forwarding is enabled on your router. 21 | 22 | `senddata` - If you only want to send the ping request, and no data, to emoncms.org set this to 0 23 | 24 | `sendnames` - sends input names in addition to values, makes sure compress is also enabled. 25 | 26 | `compress` - compress data, particularly important if sendnames is enabled as this effectively removes the overhead of adding in the names to every packet. Compress is enabled automatically if sendnames is enabled. 27 | 28 | You can create more than one of these sections to send data to multiple emoncms instances. For example, if you wanted to send to an emoncms running at emoncms.example.com (or on a local LAN) you would add the following underneath the `emoncmsorg` section described above: 29 | 30 | ```text 31 | [[emoncmsexample]] 32 | Type = EmonHubEmoncmsHTTPInterfacer 33 | [[[init_settings]]] 34 | [[[runtimesettings]]] 35 | pubchannels = ToRFM12, 36 | subchannels = ToEmonCMS, 37 | url = https://emoncms.example.com 38 | apikey = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 39 | senddata = 1 40 | sendstatus = 1 41 | sendnames = 1 42 | compress = 1 43 | ``` 44 | 45 | This time, the API key will be the API key from your account at emoncms.example.com. 46 | -------------------------------------------------------------------------------- /conf/interfacer_examples/smasolar/readme.md: -------------------------------------------------------------------------------- 1 | # SMA Solar Bluetooth Interface # 2 | 3 | This is an interface between SMA Solar inverters (http://www.sma-uk.com/) and emonCMS. 4 | 5 | emonHub is used to communicate over bluetooth to the solar inverter and retrieve various generation values, which are posted into emonCMS. 6 | 7 | Currently tested on SMA models: 8 | 9 | * SB3000 10 | * SB3000HF 11 | 12 | 13 | ## Installation ## 14 | 15 | Note that you will need to have a bluetooth USB device installed - recommend a class 1 device with a longer range if your inverter is more than 10 metres away. 16 | 17 | On emonPI, you will need to install the bluetooth software library, using the commands 18 | 19 | ``` 20 | rpi-rw 21 | sudo aptitude install bluez python-bluetooth 22 | 23 | sudo service bluetooth start 24 | sudo service bluetooth status 25 | sudo hciconfig hci0 up 26 | ``` 27 | 28 | 29 | ### Finding inverter bluetooth address ### 30 | 31 | Run the command "hcitool scan", which should list the bluetooth devices/addresses it can see. 32 | ``` 33 | Scanning ... 34 | 00:80:25:1D:AC:53 SMA001d SN: 2120051742 SN2120051742 35 | ``` 36 | 37 | ## Sample config for emonhub.conf ## 38 | 39 | Sample configuration for SMA Solar interface, add these settings under the [interfacers] tag. 40 | ``` 41 | [[SMASolar]] 42 | Type = EmonHubSMASolarInterfacer 43 | [[[init_settings]]] 44 | inverteraddress= 00:80:25:1D:AC:53 45 | inverterpincode = 0000 46 | timeinverval = 5 47 | nodeid = 29 48 | packettrace = 0 49 | [[[runtimesettings]]] 50 | pubchannels = ToEmonCMS, 51 | ``` 52 | 53 | ## Setting s## 54 | 55 | ### inverteraddress ### 56 | Specify the bluetooth address of the solar inverter for instance 00:80:25:1D:AC:53 57 | 58 | ### inverterpincode ### 59 | Security PIN code, normally defaults to "0000" unless you have changed it. 60 | 61 | ### timeinverval ### 62 | Time in seconds between samples, defaults to 10 seconds 63 | 64 | ### nodeid ### 65 | Starting node id number to assign to inputs into emonCMS, defaults to 29 for the first inverter, 30 for second, 31 for third etc. 66 | 67 | Note, you do *NOT* have to manually enter the nodes into the "[nodes]" section of the config file. 68 | 69 | ### packettrace ### 70 | If needed, set to 1 to enable debug logging of the bluetooth communication packets. You will also need to set the debug level for emonHub to DEBUG. 71 | -------------------------------------------------------------------------------- /conf/interfacer_examples/vedirect/readme.md: -------------------------------------------------------------------------------- 1 | # Interfacer for Victron VE.Direct Protocol 2 | 3 | The VE.Direct protocol is as binary/ASCII [protocol](https://www.victronenergy.com/live/vedirect_protocol:faq) created and used by [Victron Energy](https://www.victronenergy.com/) for communication between and with their products. 4 | 5 | This interfacer provides support for reading data from any Victron product that can use the VE.Direct protocol for inter-device communication. Currently this includes the BMV600, BMV700, Blue Solar MPPT, and Phoenix ranges. 6 | 7 | Example configurations are provided for the BMV700 battery monitor and Blue Solar MPPT charge controller. 8 | 9 | A VE.Direct to USB converter is available from Victron which would allow direct connection to a emonPi/Raspberry Pi or laptop. 10 | 11 | ## Usage and configuration 12 | 13 | Each supported product has it's own set of data that can be read over VE.Direct called 'fields' . The full list of available fields can be found in the VE.Direct Protocol white paper found [here](https://www.victronenergy.com/support-and-downloads/whitepapers). This information can be used to adapt the provided configurations for your device. 14 | 15 | ### Sample interfacer config for a BMV700 16 | # Sample configuration for Victron Product with VEDirect connection over USB 17 | # Configuration is for BMV 700 18 | [[VEDirect]] 19 | Type = EmonHubVEDirectInterfacer 20 | [[[init_settings]]] 21 | com_port = /dev/ttyUSB0 # Where to find our device 22 | com_baud = 19200 # Baud rate needed to decode 23 | toextract = SOC,CE,TTG,V,I,Relay,Alarm 24 | poll_interval = 10 # How often to get data in seconds 25 | [[[runtimesettings]]] 26 | nodeoffset = 9 #make sure this matches with nodename below 27 | pubchannels = ToEmonCMS, 28 | subchannels = ToBMV, 29 | basetopic = emonhub/ 30 | 31 | 32 | # Followed by a corresponding Node declaration 33 | 34 | [[9]] # This node name should be consistent with the nodeoffset parameter above 35 | nodename = VictronBMV700 36 | [[[rx]]] 37 | names = SOC,CE,TTG,V,I,Relay,Alarm # Make sure this matches 'toextract' in interfacer definition above 38 | datacode = 0 #no need to decode values 39 | scales = 0.1,1,1,0.001,1,1,1 # Some scaling necassary 40 | units = %,Ah,s,V,A,S,S 41 | 42 | 43 | With this config in place you just need to restart emonhub on your emonPi by rebooting it or ssh'ing into it and typing 44 | 45 | $>sudo service emonhub restart 46 | 47 | If there are any problems you can debug by looking inside /var/emonhub/emonhub.log. 48 | -------------------------------------------------------------------------------- /docs/troubleshooting.md: -------------------------------------------------------------------------------- 1 | --- 2 | github_url: "https://github.com/openenergymonitor/emonhub/blob/master/docs/troubleshooting.md" 3 | --- 4 | # emonHub Troubleshooting 5 | 6 | ```{admonition} emonCMS inputs not updating? 7 | emonHub is a good place to look first. Check the emonHub log and configuration. See below for more details. 8 | ``` 9 | 10 | ```{admonition} Unknown nodes keep appearing? 11 | **Turn off autoconf** at the top of emonhub.conf (set autoconf = 0) and **restart emonHub**. Remove the unknown nodes, keep only the nodes that you wish to keep. 12 | 13 | Unknown nodes can also be cleared in emoncms with the following URL: 14 | 15 | https://emoncms.org/device/clean.json 16 | http://emonpi.local/device/clean.json 17 | 18 | ``` 19 | 20 | ## View the emonHub log 21 | 22 | The emonHub log is a useful place to look if you are trying to troubleshoot problems with inputs not updating in emoncms. If `loglevel = DEBUG` is set in the `[hub]` section of the emonHub configuration file, you should see a stream of activity in the emonhub log. 23 | 24 | To access the emonHub log from within emonCMS running on the emonPi/emonBase/RaspberryPi. Navigate to Setup > EmonHub. 25 | 26 | ![emonhublog.png](img/emonhublog.png) 27 | 28 | Alternatively the emonHub log can be viewed via command line: 29 | 30 | tail -f /var/log/emonhub/emonhub.log -n1000 31 | 32 | ### Making sense of the log 33 | 34 | These messages indicate that a new frame of data is being received, via the interfacer named SPI and on node 17 in this case with the values as indicated. The frame is being sent to the internal emonHub channel `ToEmonCMS`: 35 | 36 | ``` 37 | 2022-12-01 09:50:53,993 INFO SPI Packet received 52 bytes 38 | 2022-12-01 09:50:53,994 DEBUG SPI 36 NEW FRAME : 39 | 2022-12-01 09:50:53,995 DEBUG SPI 36 Timestamp : 1669888253.994002 40 | 2022-12-01 09:50:53,996 DEBUG SPI 36 From Node : 17 41 | 2022-12-01 09:50:53,996 DEBUG SPI 36 Values : [3, 240, 11, 11, 11, 5, 5, 5, 0, 0, 0, 0, 0, 0, 19.12, 300, 300, 0, -2, -100.0] 42 | 2022-12-01 09:50:53,996 DEBUG SPI 36 RSSI : -44 43 | 2022-12-01 09:50:53,997 DEBUG SPI 36 Sent to channel(start)' : ToEmonCMS 44 | 2022-12-01 09:50:53,997 DEBUG SPI 36 Sent to channel(end)' : ToEmonCMS 45 | ``` 46 | 47 | In the standard emonSD configuration, data frames received and passed on to the `ToEmonCMS` channel are then published via MQTT. You should see a series of lines that look something like this: 48 | 49 | 2022-12-01 09:51:03,218 DEBUG MQTT Publishing: emon/emonTx4_17/MSG 1 50 | 51 | emonCMS is seperately subscribed to the `emon/` MQTT channel and will show these messages as emoncms inputs. 52 | -------------------------------------------------------------------------------- /conf/interfacer_examples/vedirect/bmv700.vedirect.emonhub.conf: -------------------------------------------------------------------------------- 1 | ####################################################################### 2 | ####################### emonhub.conf ######################### 3 | ####################################################################### 4 | 5 | ## **LEGACY CONFIG: For use with 17thJune2015 emonPi/emonBase image and older ** 6 | ## (check image version by looking for file in /boot) 7 | ## Uses old CSV MQTT topic structure compatible with Emoncms Nodes 8 | ## Does not use MQTT server authentication 9 | 10 | ### emonHub configuration file, for info see documentation: 11 | ### https://docs.openenergymonitor.org/emonhub/configuration.html 12 | ####################################################################### 13 | ####################### emonHub settings ####################### 14 | ####################################################################### 15 | 16 | [hub] 17 | ### loglevel must be one of DEBUG, INFO, WARNING, ERROR, and CRITICAL 18 | loglevel = DEBUG #(default:WARNING) 19 | 20 | ####################################################################### 21 | ####################### Interfacers ####################### 22 | ####################################################################### 23 | 24 | [interfacers] 25 | ### This interfacer manages the RFM12Pi/RFM69Pi/emonPi module 26 | 27 | # Sample configuration for Victron Product with VEDirect connection over USB 28 | # Configuration is for BMV 700 29 | [[VEDirect]] 30 | Type = EmonHubVEDirectInterfacer 31 | [[[init_settings]]] 32 | com_port = /dev/ttyUSB0 33 | com_baud = 19200 34 | toextract = SOC,CE,TTG,V,I,Relay,Alarm # These are the fields we wish to extract 35 | poll_interval = 10 # More fields can be found in datalist.py 36 | [[[runtimesettings]]] 37 | nodeoffset = 9 #make sure this matches with nodename below 38 | pubchannels = ToEmonCMS, 39 | subchannels = ToBMV, 40 | basetopic = emonhub/ 41 | 42 | 43 | ####################################################################### 44 | ####################### Nodes ####################### 45 | ####################################################################### 46 | 47 | [nodes] 48 | 49 | ## See config user guide: https://docs.openenergymonitor.org/emonhub/configuration.html 50 | 51 | 52 | [[9]] 53 | nodename = emonDC 54 | firmware =V1_6_emonTxV3_4_DiscreteSampling 55 | hardware = emonTx_(NodeID_DIP_Switch1:ON) 56 | [[[rx]]] 57 | names = SOC,CE,TTG,V,I,Relay,Alarm # Make sure this matches 'toextract' in interfacer 58 | datacode = 0 59 | scales = 0.1,1,1,0.001,1,1,1 60 | units =%,Ah,s,V,A,S,S #FirmwareV1.6 61 | -------------------------------------------------------------------------------- /conf/interfacer_examples/modbus/readme.md: -------------------------------------------------------------------------------- 1 | # Modbus interface 2 | 3 | This interface starts a modbus TCP connection and retrieves register information for publishing via mqtt to emonCMS 4 | For a FRONIUS Inverter specific config to log inver `model/brand/software` 5 | versions on init to the log file change the Type setting to EmonFroniusModbusTcpInterfacer 6 | 7 | ## Usage and configuration 8 | 9 | There is a sample modbusTCP.emonhub.conf file located in this directory. 10 | The rType and datacodes must match. Use the table below to assist. 11 | 12 | ### Sample interfacer config within emonhub.conf 13 | 14 | Sample configuration for modbus TCP clients 15 | 16 | ``` 17 | [[ModbusTCP]] 18 | # this interfacer retrieves register information from modbusTCP clients 19 | # retrieve register information from modbus TCP documentation for your inverter. 20 | Type = EmonModbusTcpInterfacer 21 | [[[init_settings]]] 22 | modbus_IP = 192.168.1.10 # ip address of client to retrieve data from 23 | modbus_port = 502 # Portclient listens on 24 | [[[runtimesettings]]] 25 | # List of starting registers for items listed above 26 | register = 40118,40092,40102,502,40285,40305,40284,40304,40086,40088,40090 27 | # nodeid used to match with node definition in nodes section below. Can be set to any integer value not previously used. 28 | nodeId = 12 29 | # Channel to publish data to should leave as ToEmonCMS 30 | pubchannels = ToEmonCMS, 31 | # time in seconds between checks, This is in addition to emonhub_interfacer.run() sleep time of .01 32 | # use this value to set the frequency of data retrieval from modbus client 33 | interval = 10 34 | ``` 35 | 36 | The default unit or slave number is 1. To query registers from different slaves, add a `nUnit` key, and specify as many numbers as there are registers to query. 37 | 38 | ``` 39 | [[[runtimesettings]]] 40 | register = 305, 306, 314, 321, 0xA001 41 | # unit number for each Register 42 | nUnit = 2, 2, 2, 2, 3 43 | ``` 44 | 45 | ### Sample Node declaration in emonhub.conf 46 | Node ID must match node ID set in interfacer definition above 47 | 48 | ``` 49 | [[12]] 50 | nodename = fronius 51 | [[[rx]]] 52 | # list of names of items being retrieved 53 | # This example retrieves the Inverter status, AC power in watts being produced, AC Lifetime KWh produced, 54 | # KWh produced for current day,.... 55 | names = Inverter_status,AC_power_watts,AC_LifetimekWh,DayWh,mppt1,mppt2,Vmppt1,Vmppt2,PhVphA,PhVphB,PhVphC 56 | datacodes = H,f,f,Q,H,H,H,H,f,f,f 57 | scales = 1,0.1,0.1,1,1,1,1,1,0.1,0.1,0.1 58 | units = V,W,kWh,Wh,W,W,V,V,V,V,V 59 | ``` 60 | -------------------------------------------------------------------------------- /conf/interfacer_examples/MQTT/readme.md: -------------------------------------------------------------------------------- 1 | ### [[MQTT]] 2 | 3 | Emonhub supports publishing to MQTT topics through the EmonHubMqttInterfacer, defined in the interfacers section of emonhub.conf. 4 | 5 | There are two formats that can be used for publishing node data to MQTT: 6 | 7 | #### **1. Node only format** 8 | 9 | (default base topic is `emonhub`) 10 | 11 | ```text 12 | topic: basetopic/rx/10/values 13 | payload: 100,200,300 14 | ``` 15 | 16 | The 'node only format' is used with the emoncms Nodes Module (now deprecated on Emoncms V9+) and the emonPiLCD python service. 17 | 18 | #### **2. Node variable format** 19 | 20 | (default base topic is `emon`) 21 | 22 | ```text 23 | topic: basetopic/emontx/power1 24 | payload: 100 25 | ``` 26 | 27 | The 'Node variable format' is the current default format from Emoncms V9. It's a more generic MQTT publishing format that can more easily be used by applications such as NodeRED and OpenHab. This format can also be used with the emoncms `phpmqtt_input.php` script in conjunction with the emoncms inputs module. See [User Guide > Technical MQTT](https://guide.openenergymonitor.org/technical/mqtt/). 28 | 29 | #### **3. JSON format** 30 | 31 | ##### Defaults 32 | 33 | ```python 34 | 'node_format_enable': 1, 35 | 'node_format_basetopic': 'emonhub/', 36 | 'nodevar_format_enable': 0, 37 | 'nodevar_format_basetopic': "nodes/", 38 | 'node_JSON_enable': 0, 39 | 'node_JSON_basetopic': "emon/" 40 | ``` 41 | 42 | Emoncms default base topic that it listens for is `emon/`. 43 | 44 | ```text 45 | topic: basetopic/ 46 | payload: {"key1":value1, "key2":value2, .... "time":, "rssi":} 47 | ``` 48 | 49 | This forat exports the data as a single JSOn string with key:value pairs. The timestamp is automatically added and used for the input time to emoncms. The RSSI is added if available (RF in use). 50 | 51 | ### Default `[MQTT]` config 52 | 53 | Note - the trailing `/` is required on the topic definition. 54 | 55 | ```text 56 | [[MQTT]] 57 | 58 | Type = EmonHubMqttInterfacer 59 | [[[init_settings]]] 60 | mqtt_host = 127.0.0.1 61 | mqtt_port = 1883 62 | mqtt_user = emonpi 63 | mqtt_passwd = emonpimqtt2016 64 | 65 | [[[runtimesettings]]] 66 | # pubchannels = ToRFM12, 67 | subchannels = ToEmonCMS, 68 | 69 | # emonhub/rx/10/values format 70 | # Use with emoncms Nodes module 71 | node_format_enable = 0 72 | node_format_basetopic = emonhub/ 73 | 74 | # emon/emontx/power1 format - use with Emoncms MQTT input 75 | # http://github.com/emoncms/emoncms/blob/master/docs/RaspberryPi/MQTT.md 76 | nodevar_format_enable = 1 77 | nodevar_format_basetopic = emon/ 78 | 79 | # Single JSON payload published - use with Emoncms MQTT 80 | node_JSON_enable = 0 81 | node_JSON_basetopic = emon/ 82 | ``` 83 | 84 | To enable one of the formats set the `enable` flag to `1`. More than one format can be used simultaneously. 85 | -------------------------------------------------------------------------------- /src/interfacers/EmonHubSerialInterfacer.py: -------------------------------------------------------------------------------- 1 | import serial 2 | from emonhub_interfacer import EmonHubInterfacer 3 | 4 | import Cargo 5 | 6 | """class EmonhubSerialInterfacer 7 | 8 | Monitors the serial port for data 9 | 10 | """ 11 | 12 | class EmonHubSerialInterfacer(EmonHubInterfacer): 13 | 14 | def __init__(self, name, com_port='', com_baud=9600): 15 | """Initialize interfacer 16 | 17 | com_port (string): path to COM port 18 | 19 | """ 20 | 21 | # Initialization 22 | super().__init__(name) 23 | 24 | self._connect_failure_count = 0 25 | 26 | # Open serial port 27 | self._ser = self._open_serial_port(com_port, com_baud) 28 | 29 | # Initialize RX buffer 30 | self._rx_buf = '' 31 | 32 | def close(self): 33 | """Close serial port""" 34 | 35 | # Close serial port 36 | if self._ser is not None: 37 | self._log.debug("Closing serial port") 38 | self._ser.close() 39 | 40 | def _open_serial_port(self, com_port, com_baud): 41 | """Open serial port 42 | 43 | com_port (string): path to COM port 44 | 45 | """ 46 | 47 | #if not int(com_baud) in [75, 110, 300, 1200, 2400, 4800, 9600, 19200, 38400, 57600, 115200]: 48 | # self._log.debug("Invalid 'com_baud': %d | Default of 9600 used", com_baud) 49 | # com_baud = 9600 50 | try: 51 | s = serial.Serial(com_port, com_baud, timeout=0) 52 | self._log.debug("Opening serial port: %s @ %s bits/s", com_port, com_baud) 53 | self._connect_failure_count = 0 54 | except serial.SerialException as e: 55 | self._connect_failure_count += 1 56 | if self._connect_failure_count==1: 57 | self._log.error("Could not open serial port: %s @ %s bits/s (retry every 10s)", com_port, com_baud) 58 | 59 | s = False 60 | # raise EmonHubInterfacerInitError('Could not open COM port %s' % com_port) 61 | return s 62 | 63 | def read(self): 64 | """Read data from serial port and process if complete line received. 65 | 66 | Return data as a list: [NodeID, val1, val2] 67 | 68 | """ 69 | 70 | if not self._ser: 71 | return False 72 | 73 | # Read serial RX 74 | self._rx_buf = self._rx_buf + self._ser.readline().decode() 75 | 76 | # If line incomplete, exit 77 | if '\r\n' not in self._rx_buf: 78 | return 79 | 80 | # Remove CR,LF 81 | f = self._rx_buf[:-2] 82 | 83 | # Reset buffer 84 | self._rx_buf = '' 85 | 86 | # Create a Payload object 87 | c = Cargo.new_cargo(rawdata=f) 88 | 89 | f = f.split() 90 | 91 | if int(self._settings['nodeoffset']): 92 | c.nodeid = int(self._settings['nodeoffset']) 93 | c.realdata = f 94 | else: 95 | c.nodeid = int(f[0]) 96 | c.realdata = f[1:] 97 | 98 | return c 99 | -------------------------------------------------------------------------------- /src/emonhub_auto_conf.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | This code is released under the GNU Affero General Public License. 4 | 5 | OpenEnergyMonitor project: 6 | http://openenergymonitor.org 7 | 8 | """ 9 | 10 | import time 11 | import logging 12 | from configobj import ConfigObj 13 | import emonhub_coder as ehc 14 | """class EmonHubAutoConf 15 | 16 | """ 17 | 18 | auto_conf_enabled = False 19 | 20 | available = {} 21 | 22 | def match_from_available(nodeid,realdata): 23 | if not str(nodeid).isnumeric(): 24 | return False 25 | 26 | match = False 27 | # Find templates that match data length 28 | datalength_match = [] 29 | for nodekey in available: 30 | if 'datalength' in available[nodekey]: 31 | if len(realdata)==available[nodekey]['datalength']: 32 | datalength_match.append(nodekey) 33 | # If we have a datalength match, attempt to match nodeid 34 | if len(datalength_match): 35 | for nodekey in datalength_match: 36 | if nodeid in available[nodekey]['nodeids']: 37 | match = nodekey 38 | break 39 | # if no nodeid match assume first datalength match 40 | if not match: 41 | match = datalength_match[0] 42 | 43 | return match 44 | 45 | 46 | class EmonHubAutoConf: 47 | 48 | def __init__(self,settings): 49 | filename = "/opt/openenergymonitor/emonhub/conf/available.conf" 50 | 51 | # Initialize logger 52 | self._log = logging.getLogger("EmonHub") 53 | 54 | self.enabled = False 55 | 56 | if 'autoconf' in settings['hub']: 57 | if int(settings['hub']['autoconf'])==1: 58 | self.enabled = True 59 | else: 60 | self.enabled = False 61 | 62 | if self.enabled: 63 | self._log.debug("Automatic configuration of nodes enabled") 64 | else: 65 | self._log.debug("Automatic configuration of nodes disabled") 66 | 67 | # Initialize attribute settings as a ConfigObj instance 68 | try: 69 | result = ConfigObj(filename, file_error=True) 70 | self.available = self.prepare_available(result['available']) 71 | except Exception as e: 72 | raise EmonHubAutoConfError(e) 73 | 74 | def prepare_available(self,nodes): 75 | for n in nodes: 76 | if 'nodeids' in nodes[n]: 77 | nodes[n]['nodeids'] = list(map(int,nodes[n]['nodeids'])) 78 | if 'datacodes' in nodes[n]['rx']: 79 | datasizes = [] 80 | for code in nodes[n]['rx']['datacodes']: 81 | datasizes.append(ehc.check_datacode(str(code))) 82 | nodes[n]['datalength'] = sum(datasizes) 83 | if 'scales' in nodes[n]['rx']: 84 | for i in range(0,len(nodes[n]['rx']['scales'])): 85 | nodes[n]['rx']['scales'][i] = float(nodes[n]['rx']['scales'][i]) 86 | if 'whitening' in nodes[n]['rx']: 87 | nodes[n]['rx']['whitening'] = int(nodes[n]['rx']['whitening']) 88 | return nodes 89 | 90 | """class EmonHubSetupInitError 91 | 92 | Raise this when init fails. 93 | 94 | """ 95 | class EmonHubAutoConfError(Exception): 96 | pass 97 | -------------------------------------------------------------------------------- /src/emonhub_buffer.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | This code is released under the GNU Affero General Public License. 4 | 5 | OpenEnergyMonitor project: 6 | http://openenergymonitor.org 7 | 8 | """ 9 | 10 | import logging 11 | 12 | """class AbstractBuffer 13 | 14 | Represents the actual buffer being used. 15 | """ 16 | 17 | 18 | class AbstractBuffer: 19 | 20 | def storeItem(self, data): 21 | raise NotImplementedError 22 | 23 | def retrieveItems(self, number): 24 | raise NotImplementedError 25 | 26 | def retrieveItem(self): 27 | raise NotImplementedError 28 | 29 | def discardLastRetrievedItem(self): 30 | raise NotImplementedError 31 | 32 | def discardLastRetrievedItems(self, number): 33 | raise NotImplementedError 34 | 35 | def hasItems(self): 36 | raise NotImplementedError 37 | 38 | """ 39 | This implementation of the AbstractBuffer just uses an in-memory data structure. 40 | It's basically identical to the previous (inline) buffer. 41 | """ 42 | 43 | 44 | class InMemoryBuffer(AbstractBuffer): 45 | 46 | def __init__(self, bufferName, buffer_size): 47 | self._bufferName = str(bufferName) 48 | self._buffer_type = "memory" 49 | self._maximumEntriesInBuffer = int(buffer_size) 50 | self._data_buffer = [] 51 | self._log = logging.getLogger("EmonHub") 52 | 53 | def hasItems(self): 54 | return self.size() > 0 55 | 56 | def isFull(self): 57 | return self.size() >= self._maximumEntriesInBuffer 58 | 59 | def getMaxEntrySliceIndex(self): 60 | return max(0, 61 | self.size() - self._maximumEntriesInBuffer - 1) 62 | 63 | def discardOldestItems(self): 64 | self._data_buffer = self._data_buffer[self.getMaxEntrySliceIndex():] 65 | 66 | def discardOldestItemsIfFull(self): 67 | if self.isFull(): 68 | self._log.warning( 69 | "In-memory buffer (%s) reached limit of %d items, deleting oldest", 70 | self._bufferName, self._maximumEntriesInBuffer) 71 | self.discardOldestItems() 72 | 73 | def storeItem(self, data): 74 | self.discardOldestItemsIfFull() 75 | self._data_buffer.append(data) 76 | 77 | def retrieveItem(self): 78 | return self._data_buffer[0] 79 | 80 | def retrieveItems(self, number): 81 | blen = len(self._data_buffer) 82 | if number > blen: 83 | number = blen 84 | return self._data_buffer[:number] 85 | 86 | def discardLastRetrievedItem(self): 87 | del self._data_buffer[0] 88 | 89 | def discardLastRetrievedItems(self, number): 90 | blen = len(self._data_buffer) 91 | if number > blen: 92 | number = blen 93 | self._data_buffer = self._data_buffer[number:] 94 | 95 | def size(self): 96 | return len(self._data_buffer) 97 | 98 | 99 | """ 100 | The getBuffer function returns the buffer class corresponding to a 101 | buffering method passed as argument. 102 | """ 103 | bufferMethodMap = { 104 | 'memory': InMemoryBuffer 105 | } 106 | 107 | 108 | def getBuffer(method): 109 | """Returns the buffer class corresponding to the method 110 | 111 | method (string): buffering method 112 | 113 | """ 114 | return bufferMethodMap[method] 115 | -------------------------------------------------------------------------------- /conf/interfacer_examples/modbus/modbusTCP.emonhub.conf: -------------------------------------------------------------------------------- 1 | ####################################################################### 2 | ####################### emonhub.conf ######################### 3 | ####################################################################### 4 | ### emonHub configuration file, for info see documentation: 5 | ### https://docs.openenergymonitor.org/emonhub/configuration.html 6 | ####################################################################### 7 | ####################### emonHub settings ####################### 8 | ####################################################################### 9 | 10 | [hub] 11 | ### loglevel must be one of DEBUG, INFO, WARNING, ERROR, and CRITICAL 12 | loglevel = DEBUG 13 | 14 | 15 | ### Uncomment this to also send to syslog 16 | # use_syslog = yes 17 | ####################################################################### 18 | ####################### Interfacers ####################### 19 | ####################################################################### 20 | 21 | [interfacers] 22 | 23 | ### This interfacer manages connections to modbustcp clients 24 | 25 | [[ModbusTCP]] 26 | # this interfacer retrieves register information from modbusTCP clients 27 | # retrieve register information from modbus TCP documentation for your inverter. 28 | # Information here is designed for Fronius Symo 3 phase inverter. 29 | Type = EmonModbusTcpInterfacer 30 | [[[init_settings]]] 31 | modbus_IP = 192.168.1.10 # ip address of client to retrieve data from 32 | modbus_port = 502 # Portclient listens on 33 | [[[runtimesettings]]] 34 | # List of starting registers for items listed above 35 | register = 40118,40092,40102,502,40285,40305,40284,40304,40086,40088,40090 36 | # nodeid used to match with node definition in nodes section below. Can be set to any integer value not previously used. 37 | nodeId = 12 38 | # Channel to publish data to should leave as ToEmonCMS 39 | pubchannels = ToEmonCMS, 40 | # time in seconds between checks, This is in addition to emonhub_interfacer.run() sleep time of .01 41 | # use this value to set the frequency of data retrieval from modbus client 42 | interval = 10 43 | 44 | [[MQTT]] 45 | 46 | Type = EmonHubMqttInterfacer 47 | [[[init_settings]]] 48 | mqtt_host = 127.0.0.1 49 | mqtt_port = 1883 50 | mqtt_user = emonpi 51 | mqtt_passwd = emonpimqtt2016 52 | 53 | [[[runtimesettings]]] 54 | subchannels = ToEmonCMS, 55 | 56 | # emonhub/rx/10/values format 57 | # Use with emoncms Nodes module 58 | node_format_enable = 0 59 | node_format_basetopic = emonhub/ 60 | 61 | # emon/emontx/power1 format - use with Emoncms MQTT input 62 | # https://docs.openenergymonitor.org/emonhub/configuration.html 63 | nodevar_format_enable = 1 64 | nodevar_format_basetopic = emon/ 65 | 66 | 67 | ####################################################################### 68 | ####################### Nodes ####################### 69 | ####################################################################### 70 | 71 | [nodes] 72 | 73 | ## See config user guide: http://github.com/openenergymonitor/emonhub/blob/master/configuration.md 74 | 75 | [[12]] 76 | nodename = fronius 77 | [[[rx]]] 78 | names = Inverter_status,AC_power_watts,AC_LifetimekWh,DayWh,mppt1,mppt2,Vmppt1,Vmppt2,PhVphA,PhVphB,PhVphC 79 | datacodes = H,f,f,Q,H,H,H,H,f,f,f 80 | scales = 1,0.1,0.1,1,1,1,1,1,0.1,0.1,0.1 81 | units = V,W,kWh,Wh,W,W,V,V,V,V,V 82 | -------------------------------------------------------------------------------- /src/interfacers/EmonHubTx3eInterfacer.py: -------------------------------------------------------------------------------- 1 | import re 2 | import Cargo 3 | from . import EmonHubSerialInterfacer as ehi 4 | 5 | """class EmonHubTx3eInterfacer 6 | 7 | EmonHub Serial Interfacer key:value pair format 8 | e.g: ct1:0,ct2:0,ct3:0,ct4:0,vrms:524,pulse:0 9 | for csv format use the EmonHubSerialInterfacer 10 | 11 | """ 12 | 13 | 14 | class EmonHubTx3eInterfacer(ehi.EmonHubSerialInterfacer): 15 | 16 | def __init__(self, name, com_port='', com_baud=9600): 17 | """Initialize interfacer 18 | 19 | com_port (string): path to COM port e.g /dev/ttyUSB0 20 | com_baud (numeric): typically 115200 now for emontx etc 21 | 22 | """ 23 | 24 | # Initialization 25 | super().__init__(name, com_port, com_baud) 26 | 27 | self._settings.update({ 28 | 'nodename': "" 29 | }) 30 | 31 | # Initialize RX buffer 32 | self._rx_buf = '' 33 | 34 | def read(self): 35 | """Read data from serial port and process if complete line received. 36 | 37 | Read data format is key:value pairs e.g: 38 | ct1:0,ct2:0,ct3:0,ct4:0,vrms:524,pulse:0 39 | 40 | """ 41 | 42 | if not self._ser: 43 | return False 44 | 45 | # Read serial RX 46 | self._rx_buf = self._rx_buf + self._ser.readline().decode() 47 | 48 | # If line incomplete, exit 49 | if '\r\n' not in self._rx_buf: 50 | # If string longer than 3 print message 51 | if len(self._rx_buf) > 3: 52 | self._log.info("START MESSAGE: %s", self._rx_buf.rstrip()) 53 | 54 | self._rx_buf = '' 55 | return False 56 | 57 | #Check for MSG data string. If not found... 58 | if self._rx_buf.find("MSG:",0,4) == -1: 59 | self._log.info("START MESSAGE: %s", self._rx_buf.rstrip()) 60 | self._rx_buf = '' 61 | return False 62 | 63 | # Remove CR,LF 64 | f = self._rx_buf[:-2].strip() 65 | 66 | # Create a Payload object 67 | c = Cargo.new_cargo(rawdata=f) 68 | 69 | # Reset buffer 70 | self._rx_buf = '' 71 | 72 | # Parse the ESP format string 73 | values = [] 74 | names = [] 75 | 76 | for item in f.split(','): 77 | parts = item.split(':') 78 | if len(parts) == 2: 79 | # check for alphanumeric input name 80 | if re.match(r'^[\w-]+$', parts[0]): 81 | # check for numeric value 82 | value = 0 83 | try: 84 | value = float(parts[1]) 85 | except Exception: 86 | self._log.debug("input value is not numeric: %s", parts[1]) 87 | 88 | names.append(parts[0]) 89 | values.append(value) 90 | else: 91 | self._log.debug("invalid input name: %s", parts[0]) 92 | 93 | if self._settings["nodename"] != "": 94 | c.nodename = self._settings["nodename"] 95 | c.nodeid = self._settings["nodename"] 96 | else: 97 | c.nodeid = int(self._settings['nodeoffset']) 98 | 99 | c.realdata = values 100 | c.names = names 101 | 102 | if len(values) == 0: 103 | return False 104 | 105 | return c 106 | 107 | def set(self, **kwargs): 108 | for key, setting in self._settings.items(): 109 | if key in kwargs: 110 | # replace default 111 | # self._log.debug(kwargs[key]) 112 | self._settings[key] = kwargs[key] 113 | -------------------------------------------------------------------------------- /src/interfacers/EmonHubTeslaPowerWallInterfacer.py: -------------------------------------------------------------------------------- 1 | import time, json, Cargo, requests 2 | from emonhub_interfacer import EmonHubInterfacer 3 | 4 | """class EmonHubTeslaPowerWallInterfacer 5 | 6 | Fetch Tesla Power Wall state of charge 7 | 8 | """ 9 | 10 | class EmonHubTeslaPowerWallInterfacer(EmonHubInterfacer): 11 | 12 | def __init__(self, name): 13 | super().__init__(name) 14 | 15 | self._settings.update(self._defaults) 16 | 17 | # Interfacer specific settings 18 | self._template_settings = {'name': 'powerwall', 19 | 'url': False, 20 | 'readinterval': 10.0} 21 | 22 | # FIXME is there a good reason to reduce this from the default of 1000? If so, document it here. 23 | # set an absolute upper limit for number of items to process per post 24 | self._item_limit = 250 25 | 26 | # Fetch first reading at one interval lengths time 27 | self._last_time = 0 28 | 29 | def read(self): 30 | # Request Power Wall data at user specified interval 31 | if time.time() - self._last_time >= self._settings['readinterval']: 32 | self._last_time = time.time() 33 | 34 | # If URL is set, fetch the SOC 35 | if self._settings['url']: 36 | # HTTP Request 37 | try: 38 | reply = requests.get(self._settings['url'], timeout=int(self._settings['readinterval']), verify=False) 39 | reply.raise_for_status() # Raise an exception if status code isn't 200 40 | except requests.exceptions.RequestException as ex: 41 | self._log.warning("%s couldn't send to server: %s", self.name, ex) 42 | 43 | jsonstr = reply.text.rstrip() 44 | self._log.debug("%s Request response: %s", self.name, jsonstr) 45 | 46 | # Decode JSON 47 | try: 48 | data = json.loads(jsonstr) 49 | except Exception: # FIXME Too general exception 50 | self._log.warning("%s Invalid JSON", self.name) 51 | return 52 | 53 | # Check if battery percentage key is in data object 54 | if not 'percentage' in data: 55 | self._log.warning("%s Percentage key not found", self.name) 56 | return 57 | 58 | # Create cargo object 59 | c = Cargo.new_cargo() 60 | c.nodeid = self._settings['name'] 61 | c.names = ["soc"] 62 | c.realdata = [data['percentage']] 63 | return c 64 | 65 | # return empty if not time 66 | return 67 | 68 | def set(self, **kwargs): 69 | for key, setting in self._template_settings.items(): 70 | # Decide which setting value to use 71 | if key in kwargs.keys(): 72 | setting = kwargs[key] 73 | else: 74 | setting = self._template_settings[key] 75 | if key in self._settings and self._settings[key] == setting: 76 | continue 77 | elif key == 'readinterval': 78 | self._log.info("Setting %s %s: %s", self.name, key, setting) 79 | self._settings[key] = float(setting) 80 | continue 81 | elif key == 'name': 82 | self._log.info("Setting %s %s: %s", self.name, key, setting) 83 | self._settings[key] = setting 84 | continue 85 | elif key == 'url': 86 | self._log.info("Setting %s %s: %s", self.name, key, setting) 87 | self._settings[key] = setting 88 | continue 89 | else: 90 | self._log.warning("'%s' is not valid for %s: %s", setting, self.name, key) 91 | 92 | # include kwargs from parent 93 | super().set(**kwargs) 94 | -------------------------------------------------------------------------------- /conf/default.emonhub.conf: -------------------------------------------------------------------------------- 1 | ####################################################################### 2 | ####################### emonhub.conf ######################### 3 | ####################################################################### 4 | ### emonHub configuration file, for info see documentation: 5 | ### https://docs.openenergymonitor.org/emonhub/configuration.html 6 | ####################################################################### 7 | ####################### emonHub settings ####################### 8 | ####################################################################### 9 | 10 | [hub] 11 | ### loglevel must be one of DEBUG, INFO, WARNING, ERROR, and CRITICAL 12 | loglevel = DEBUG 13 | autoconf = 1 14 | ### Uncomment this to also send to syslog 15 | # use_syslog = yes 16 | ####################################################################### 17 | ####################### Interfacers ####################### 18 | ####################################################################### 19 | 20 | [interfacers] 21 | ### This interfacer manages the RFM12Pi/RFM69Pi/emonPi module 22 | [[EmonPi2]] 23 | Type = EmonHubOEMInterfacer 24 | [[[init_settings]]] 25 | com_port = /dev/ttyAMA0 26 | com_baud = 38400 27 | [[[runtimesettings]]] 28 | pubchannels = ToEmonCMS, 29 | subchannels = ToRFM12, 30 | 31 | [[USB0]] 32 | Type = EmonHubOEMInterfacer 33 | [[[init_settings]]] 34 | com_port = /dev/ttyUSB0 35 | com_baud = 115200 36 | [[[runtimesettings]]] 37 | pubchannels = ToEmonCMS, 38 | subchannels = ToRFM12, 39 | nodename = emonTx4 40 | 41 | [[SPI]] 42 | Type = EmonHubRFM69LPLInterfacer 43 | [[[init_settings]]] 44 | nodeid = 5 45 | networkID = 210 46 | [[[runtimesettings]]] 47 | pubchannels = ToEmonCMS, 48 | 49 | 50 | [[MQTT]] 51 | Type = EmonHubMqttInterfacer 52 | [[[init_settings]]] 53 | mqtt_host = 127.0.0.1 54 | mqtt_port = 1883 55 | mqtt_user = emonpi 56 | mqtt_passwd = emonpimqtt2016 57 | 58 | [[[runtimesettings]]] 59 | pubchannels = ToRFM12, 60 | subchannels = ToEmonCMS, 61 | 62 | # emonhub/rx/10/values format 63 | # Use with emoncms Nodes module 64 | node_format_enable = 0 65 | node_format_basetopic = emonhub/ 66 | 67 | # emon/emontx/power1 format - use with Emoncms MQTT input 68 | # http://github.com/emoncms/emoncms/blob/master/docs/RaspberryPi/MQTT.md 69 | nodevar_format_enable = 1 70 | nodevar_format_basetopic = emon/ 71 | 72 | # Single JSON payload published - use with Emoncms MQTT 73 | node_JSON_enable = 0 74 | node_JSON_basetopic = emon/ 75 | 76 | [[emoncmsorg]] 77 | Type = EmonHubEmoncmsHTTPInterfacer 78 | [[[init_settings]]] 79 | [[[runtimesettings]]] 80 | pubchannels = ToRFM12, 81 | subchannels = ToEmonCMS, 82 | url = https://emoncms.org 83 | apikey = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 84 | senddata = 1 # Enable sending data to Emoncms.org 85 | sendnames = 1 # Send full input names (compression will be automatically enabled) 86 | interval = 30 # Bulk send interval to Emoncms.org in seconds 87 | 88 | ####################################################################### 89 | ####################### Nodes ####################### 90 | ####################################################################### 91 | 92 | ## See config user guide: https://github.com/openenergymonitor/emonhub 93 | ## If autoconf is enabled above, node configuration will automatically 94 | ## populate based on templates listed in available.conf 95 | 96 | [nodes] 97 | -------------------------------------------------------------------------------- /src/interfacers/EmonHubRedisInterfacer.py: -------------------------------------------------------------------------------- 1 | import time 2 | import json 3 | import Cargo 4 | from emonhub_interfacer import EmonHubInterfacer 5 | 6 | """ 7 | [[Redis]] 8 | Type = EmonHubRedisInterfacer 9 | [[[init_settings]]] 10 | redis_host = localhost 11 | redis_port = 6379 12 | redis_db = 0 13 | [[[runtimesettings]]] 14 | subchannels = ToEmonCMS, 15 | prefix = "emonhub:" 16 | """ 17 | 18 | """class EmonHubRedisInterfacer 19 | 20 | Redis interfacer for use in development 21 | 22 | """ 23 | 24 | class EmonHubRedisInterfacer(EmonHubInterfacer): 25 | 26 | def __init__(self, name, redis_host='localhost', redis_port=6379, redis_db=0): 27 | """Initialize Interfacer 28 | 29 | """ 30 | # Initialization 31 | super(EmonHubRedisInterfacer, self).__init__(name) 32 | self._settings.update(self._defaults) 33 | 34 | # Interfacer specific settings 35 | self._redis_settings = {'prefix': ''} 36 | 37 | # Only load module if it is installed 38 | try: 39 | import redis 40 | self.r = redis.Redis(redis_host, redis_port, redis_db) 41 | except ModuleNotFoundError as err: 42 | self._log.error(err) 43 | self.r = False 44 | 45 | 46 | def read(self): 47 | if self.r: 48 | result = self.r.lpop("emonhub:sub") 49 | if result: 50 | try: 51 | read_data = json.loads(result) 52 | except Exception as e: 53 | logging.error(e) 54 | 55 | c = Cargo.new_cargo() 56 | c.names = [] 57 | c.realdata = [] 58 | c.units = [] 59 | 60 | c.nodeid = "redis" 61 | if 'node' in read_data: 62 | c.nodeid = read_data['node'] 63 | del read_data['node'] 64 | 65 | if 'time' in read_data: 66 | del read_data['time'] 67 | 68 | for key in read_data: 69 | c.names.append(key) 70 | c.realdata.append(read_data[key]) 71 | 72 | return c 73 | return False 74 | 75 | def add(self, cargo): 76 | """set data in redis 77 | 78 | """ 79 | if not self.r: 80 | return False 81 | 82 | nodeid = cargo.nodeid 83 | 84 | if len(cargo.names) <= len(cargo.realdata): 85 | for i in range(0, len(cargo.names)): 86 | name = cargo.names[i] 87 | value = cargo.realdata[i] 88 | 89 | name_parts = [] 90 | if self._settings['prefix'] != '': 91 | name_parts.append(self._settings['prefix']) 92 | name_parts.append(str(nodeid)) 93 | name_parts.append(str(name)) 94 | 95 | name = ":".join(name_parts) 96 | 97 | self._log.info("redis set " + name + " " + str(value)) 98 | try: 99 | self.r.set(name, value) 100 | except Exception as err: 101 | self._log.error(err) 102 | return False 103 | 104 | def set(self, **kwargs): 105 | for key, setting in self._redis_settings.items(): 106 | # Decide which setting value to use 107 | if key in kwargs: 108 | setting = kwargs[key] 109 | else: 110 | setting = self._redis_settings[key] 111 | if key in self._settings and self._settings[key] == setting: 112 | continue 113 | elif key == 'prefix': 114 | self._log.info("Setting %s prefix: %s", self.name, setting) 115 | self._settings[key] = setting 116 | continue 117 | else: 118 | self._log.warning("'%s' is not valid for %s: %s", setting, self.name, key) 119 | 120 | # include kwargs from parent 121 | super().set(**kwargs) 122 | -------------------------------------------------------------------------------- /src/interfacers/EmonHubPulseCounterInterfacer.py: -------------------------------------------------------------------------------- 1 | from emonhub_interfacer import EmonHubInterfacer 2 | import time 3 | import atexit 4 | 5 | import Cargo 6 | 7 | try: 8 | import RPi.GPIO as GPIO 9 | RPi_found = True 10 | except: 11 | RPi_found = False 12 | 13 | """class EmonhubPulseCounterInterfacer 14 | 15 | Authors @borpin & @bwduncan 16 | Version: 1 17 | Date: 11 June 2020 18 | 19 | Monitors GPIO pins for pulses 20 | 21 | Example emonhub configuration 22 | [[pulse2]] 23 | Type = EmonHubPulseCounterInterfacer 24 | [[[init_settings]]] 25 | # pin number must be specified. Create a second 26 | # interfacer for more than one pulse sensor 27 | pulse_pin = 15 28 | # bouncetime default to 1. 29 | # bouncetime = 2 30 | # Rate_limit is the rate at which the interfacer will pass data 31 | # to emonhub for sending on. Too short and pulses will be missed. 32 | # rate_limit is minimum number of seconds between data output. 33 | # pulses are accumulated in this period. 34 | # rate_limit default to 2. 35 | # rate_limit = 2 36 | [[[runtimesettings]]] 37 | pubchannels = ToEmonCMS, 38 | 39 | # Default NodeID is 0. Use nodeoffset to set NodeID 40 | # No decoder required as key:value pair returned 41 | nodeoffset = 3 42 | """ 43 | 44 | class EmonHubPulseCounterInterfacer(EmonHubInterfacer): 45 | 46 | def __init__(self, name, pulse_pin=None, bouncetime=1, rate_limit=2): 47 | """Initialize interfacer 48 | 49 | """ 50 | 51 | # Initialization 52 | super().__init__(name) 53 | 54 | self._settings.update({ 55 | 'pulse_pin' : int(pulse_pin), 56 | 'bouncetime' : int(bouncetime), 57 | 'rate_limit' : int(rate_limit) 58 | }) 59 | 60 | self._pulse_settings = {} 61 | self.pulse_count = 0 62 | self.last_pulse = 0 63 | self.last_time = (time.time()//10)*10 64 | 65 | if RPi_found: 66 | self.init_gpio() 67 | else: 68 | self._log.error("Pulse counter not initialised. Please install the RPi GPIO Python3 module") 69 | 70 | def init_gpio(self): 71 | """Register GPIO callbacks 72 | 73 | """ 74 | 75 | atexit.register(GPIO.cleanup) 76 | GPIO.setmode(GPIO.BOARD) 77 | self._log.info('%s : Pulse pin set to: %d', self.name, self._settings['pulse_pin']) 78 | GPIO.setup(self._settings['pulse_pin'], GPIO.IN, pull_up_down=GPIO.PUD_DOWN) 79 | GPIO.add_event_detect(self._settings['pulse_pin'], GPIO.FALLING, callback=self.process_pulse, bouncetime=self._settings['bouncetime']) 80 | 81 | def process_pulse(self, channel): 82 | self.pulse_count += 1 83 | self._log.debug('%s : pulse received - count: %d', self.name, self.pulse_count) 84 | 85 | def read(self): 86 | time_now = time.time() 87 | 88 | if self.last_pulse == self.pulse_count: 89 | return False 90 | elif self.last_time + self._settings['rate_limit'] > time_now: 91 | return False 92 | 93 | self._log.debug('Data to Post: last_time: %d time_now: %d', self.last_time, time_now) 94 | self.last_pulse = self.pulse_count 95 | self.last_time = int(time_now) 96 | 97 | c = Cargo.new_cargo(nodename=self.name, timestamp=time_now) 98 | c.names = ["Pulse"] 99 | c.realdata = [self.last_pulse] 100 | 101 | if int(self._settings['nodeoffset']): 102 | c.nodeid = int(self._settings['nodeoffset']) 103 | else: 104 | c.nodeid = 0 105 | return c 106 | 107 | 108 | def set(self, **kwargs): 109 | super().set(**kwargs) 110 | 111 | for key, setting in self._pulse_settings.items(): 112 | 113 | if key not in kwargs: 114 | setting = self._pulse_settings[key] 115 | else: 116 | setting = kwargs[key] 117 | 118 | if key in self._settings and self._settings[key] == setting: 119 | continue 120 | else: 121 | self._log.warning("'%s' is not valid for %s: %s", setting, self.name, key) 122 | -------------------------------------------------------------------------------- /docs/overview.md: -------------------------------------------------------------------------------- 1 | --- 2 | github_url: "https://github.com/openenergymonitor/emonhub/blob/master/docs/overview.md" 3 | --- 4 | # emonHub Overview 5 | 6 | ## Introduction 7 | 8 | EmonHub is a piece of software running on the emonPi and emonBase that can read/subscribe or send/publish data to and from a multitude of services. It is primarily used as the bridge between the OpenEnergyMonitor monitoring hardware and the Emoncms software but it can also be used to read in data from a number of other sources, providing an easy way to interface with a wider range of sensors. 9 | 10 | ```{admonition} Troubleshooting? 11 | See common issues and troubleshooting tips here: [Troubleshooting](troubleshooting) 12 | ``` 13 | 14 | --- 15 | 16 | ## Features 17 | 18 | The OpenEnergyMonitor variant of emonhub is based on [@pb66 Paul Burnell's](https://github.com/pb66) original adding: 19 | 20 | - Internal pub/sub message bus based on pydispatcher 21 | - Publish to MQTT 22 | - Https Emoncms interface 23 | - A multi-file implementation of interfacers. 24 | - Rx and tx modes for node decoding/encoding provides improved control support. 25 | - json based config file option so that emonhub.conf can be loaded by emoncms 26 | - Ongoing development on other interfacers such as the MBUS and Modbus interfacers. 27 | 28 | --- 29 | 30 | ## Basic Concept 31 | 32 | A number of individual **Interfacers** can be configured within emonHub to collect data from multiple sources and distribute that information to multiple targets, using different protocols. 33 | 34 | In its simplest form, emonHub takes data from a Serial Interface and transforms it to a format suitable for emoncms to take as an Input, then sends it to emoncms via HTTP or MQTT. 35 | 36 | Each Interfacer communicates by creating *channels*, much like an MQTT Broker, that allows the Interfacer to *Publish* data to a channel and *Subscribe* (get) data from a channel. Each interfacer can communicate over multiple channels. 37 | 38 | Each interfacer can listen on a `subchannel` or publish on a `pubchannel`. Some interfacers can do both. An Interfacer needs at least one channel defined of either type. 39 | 40 | **For Example:** 41 | 42 | The Serial Interfacer listens on a serial port then publishes that data for onward transmission - it has a `pubchannel` defined. 43 | 44 | The MQTT interfacer listens for data which it then sends out via MQTT, it therefore defines a `subchannel` that it will listen on for data to send via MQTT. 45 | 46 | For data to be passed, the name of the 2 channels must match. 47 | 48 | Each Interfacer can have multiple channels defined and multiple interfacers can listen to the same channel. e.g. data published by the Serial Interfacer can be listened (subscribed) for by the MQTT and the HTTP interfacer. 49 | 50 | **Note** The channel definition is a list so **must** end with a comma e.g. `pubchannels = ToEmonCMS,` or `pubchannels = ToEmonCMS,ToXYZ,` 51 | 52 | --- 53 | 54 | ## Installing Emonhub 55 | 56 | ### emonScripts 57 | 58 | emonHub is installed as standard on the emonSD image built using the EmonScripts install scripts. 59 | 60 | ### Manual Install (standalone) 61 | 62 | Preparation: 63 | On a vanilla system, you will need to install git. 64 | ```bash 65 | sudo apt install git 66 | ``` 67 | 68 | To fit in with the usual filesystem conventions, create an `openenergymonitor` folder. It is assumed the standard user `pi` is being used (adjust as necessary). 69 | 70 | ```bash 71 | cd /opt 72 | sudo mkdir openenergymonitor 73 | sudo chown pi openenergymonitor/ 74 | cd openenergymonitor/ 75 | ``` 76 | 77 | Install emonHub: 78 | 79 | ```bash 80 | git clone https://github.com/openenergymonitor/emonhub.git 81 | cd emonhub 82 | git checkout stable 83 | sudo ./install.sh 84 | ``` 85 | 86 | To view the emonhub log via terminal on the emonpi or emonbase: 87 | 88 | ```bash 89 | journalctl -f -u emonhub 90 | ``` 91 | 92 | If the MQTT Interfacer is to be used, either Mosquitto needs to be installed locally or the configuration file needs to be edited to point to the MQTT Broker to be used. 93 | 94 | To install mosquitto locally; 95 | 96 | ```bash 97 | sudo apt-get update 98 | sudo apt-get install -y mosquitto 99 | ``` 100 | 101 | It is recommended to turn off mosquitto persistence 102 | 103 | ```bash 104 | sudo nano /etc/mosquitto/mosquitto.conf 105 | ``` 106 | 107 | Set 108 | 109 | ```text 110 | persistence false 111 | ``` 112 | 113 | -------------------------------------------------------------------------------- /src/interfacers/EmonHubDigitalInputInterfacer.py: -------------------------------------------------------------------------------- 1 | from emonhub_interfacer import EmonHubInterfacer 2 | import time 3 | import atexit 4 | 5 | import Cargo 6 | 7 | try: 8 | import RPi.GPIO as GPIO 9 | RPi_found = True 10 | except: 11 | RPi_found = False 12 | 13 | """class EmonHubDigitalInputInterfacer 14 | 15 | Authors @trystanlea based on pulse counter interfacer by @borpin & @bwduncan 16 | Version: 1 17 | Date: 02 September 2024 18 | 19 | Monitors GPIO pins for digital state 20 | 21 | Example emonhub configuration 22 | [[digital]] 23 | Type = EmonHubDigitalInputInterfacer 24 | [[[init_settings]]] 25 | # Include comma even if only one pin 26 | pins = 13, 15 27 | invert = 1 28 | [[[runtimesettings]]] 29 | pubchannels = ToEmonCMS, 30 | nodename = gpio 31 | read_interval = 10 32 | """ 33 | 34 | class EmonHubDigitalInputInterfacer(EmonHubInterfacer): 35 | 36 | def __init__(self, name, pins, invert=0): 37 | """Initialize interfacer 38 | 39 | """ 40 | 41 | # Initialization 42 | super().__init__(name) 43 | 44 | # Pins is already a list e.g ['13','15'] 45 | # convert to list of integers 46 | pins = [int(p) for p in pins] 47 | 48 | self._settings.update({ 49 | 'pins' : pins, 50 | 'invert' : int(invert), 51 | 'read_interval' : 10, 52 | 'nodename' : 'gpio' 53 | }) 54 | 55 | self._digital_settings = {} 56 | 57 | if RPi_found: 58 | self.init_gpio() 59 | else: 60 | self._log.error("Pulse counter not initialised. Please install the RPi GPIO Python3 module") 61 | 62 | def init_gpio(self): 63 | """Register GPIO callbacks 64 | 65 | """ 66 | atexit.register(GPIO.cleanup) 67 | GPIO.setmode(GPIO.BOARD) 68 | 69 | for pin in self._settings['pins']: 70 | self._log.info('%s : Setting up digital pin: %d', self.name, pin) 71 | GPIO.setup(int(pin), GPIO.IN) 72 | 73 | def read(self): 74 | 75 | if int(time.time()) % self._settings['read_interval'] == 0: 76 | 77 | # Read the digital pin state 78 | # state = GPIO.input(self._settings['digital_pin']) 79 | # self._log.debug('%s : state: %d', self.name, state) 80 | 81 | # Add to cargo 82 | c = Cargo.new_cargo() 83 | c.names = [] 84 | c.realdata = [] 85 | c.units = [] 86 | c.nodeid = self._settings['nodename'] 87 | c.timestamp = int(time.time()) 88 | 89 | for pin in self._settings['pins']: 90 | state = GPIO.input(int(pin)) 91 | self._log.debug('%s : state: %d', self.name, state) 92 | 93 | # Invert the state if invert is set 94 | if self._settings['invert']: 95 | state = not state 96 | self._log.debug('%s : inverted state: %d', self.name, state) 97 | 98 | c.realdata.append(state) 99 | c.names.append("pin" + str(pin)) 100 | 101 | c.nodeid = self._settings['nodename'] 102 | 103 | # Minimum sleep time is 1 second 104 | time.sleep(1) 105 | 106 | return c 107 | 108 | 109 | def set(self, **kwargs): 110 | super().set(**kwargs) 111 | 112 | for key, setting in self._digital_settings.items(): 113 | 114 | if key not in kwargs: 115 | setting = self._digital_settings[key] 116 | else: 117 | setting = kwargs[key] 118 | 119 | if key in self._settings and self._settings[key] == setting: 120 | continue 121 | 122 | # How fast to read the digital pins 123 | elif key == 'read_interval': 124 | self._log.info("Setting %s read_interval: %s", self.name, setting) 125 | self._settings[key] = float(setting) 126 | # Minimum read interval is 1 second 127 | if self._settings[key] < 1: 128 | self._settings[key] = 1 129 | continue 130 | 131 | # Option to set nodename 132 | elif key == 'nodename': 133 | self._log.info("Setting %s nodename: %s", self.name, setting) 134 | self._settings[key] = str(setting) 135 | continue 136 | 137 | else: 138 | self._log.warning("'%s' is not valid for %s: %s", setting, self.name, key) 139 | -------------------------------------------------------------------------------- /src/interfacers/tmp/EmonHubSmilicsInterfacer.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import datetime 3 | import time 4 | 5 | from BaseHTTPServer import BaseHTTPRequestHandler 6 | from Queue import Queue 7 | from SocketServer import TCPServer, ThreadingMixIn 8 | from urlparse import parse_qs 9 | 10 | import Cargo 11 | import emonhub_coder as ehc 12 | from emonhub_interfacer import EmonHubInterfacer 13 | 14 | class ThreadedTCPServer(ThreadingMixIn, TCPServer): 15 | def serve_forever(self, queue): 16 | self.RequestHandlerClass.queue = queue 17 | TCPServer.serve_forever(self) 18 | 19 | class ServerHandler(BaseHTTPRequestHandler): 20 | def do_GET(self): 21 | data = parse_qs(self.path[18:]) 22 | self.queue.put(data) 23 | 24 | 25 | class EmonHubSmilicsInterfacer(EmonHubInterfacer): 26 | """ Interface for the Smilics Wibee 27 | 28 | Listen for get request on the specified port 29 | """ 30 | 31 | def __init__(self, name, port): 32 | """ 33 | Args: 34 | name (str): Configuration name. 35 | port (int): The port the webserver should listen on. 36 | """ 37 | super().__init__(name) 38 | 39 | self._settings = { 40 | 'subchannels': ['ch1'], 41 | 'pubchannels': ['ch2'], 42 | } 43 | self._queue = Queue() 44 | self._server = ThreadedTCPServer(("0.0.0.0", int(port)), ServerHandler) 45 | 46 | def close(self): 47 | """Cleanup when the interface closes""" 48 | if self._server is not None: 49 | self._log.debug('Closing server') 50 | self._server.shutdown() 51 | self._server.server_close() 52 | 53 | def run(self): 54 | """Starts the server on a new thread and processes the queue""" 55 | server_thread = threading.Thread(target=self._server.serve_forever, args=(self._queue,)) 56 | server_thread.daemon = True 57 | server_thread.start() 58 | 59 | while not self.stop: 60 | while not self._queue.empty(): 61 | rxc = self._process_rx(self._queue.get(False)) 62 | self._queue.task_done() 63 | 64 | if rxc: 65 | rxc = self._process_rx(rxc) 66 | if rxc: 67 | for channel in self._settings["pubchannels"]: 68 | self._log.debug("%d Sent to channel(start)' : %s", rxc.uri, channel) 69 | 70 | # Add cargo item to channel 71 | self._pub_channels.setdefault(channel, []).append(rxc) 72 | 73 | self._log.debug("%d Sent to channel(end)' : %s", rxc.uri, channel) 74 | 75 | # Don't loop too fast 76 | time.sleep(0.1) 77 | 78 | self.close() 79 | 80 | def _process_rx(self, smilics_dict): 81 | """ Converts the data received on the webserver to an instance of 82 | the Cargo class 83 | 84 | Args: 85 | smilics_dict: Dict with smilics data. 86 | 87 | Returns: 88 | Cargo if successful, None otherwise. 89 | """ 90 | try: 91 | c = Cargo.new_cargo() 92 | if 'mac' not in smilics_dict.keys(): 93 | return None 94 | 95 | c.nodeid = smilics_dict['mac'][0] 96 | if c.nodeid not in ehc.nodelist.keys(): 97 | self._log.debug("%d Not in config", c.nodeid) 98 | return None 99 | 100 | node_config = ehc.nodelist[str(c.nodeid)] 101 | 102 | c.names = node_config['rx']['names'] 103 | c.nodename = node_config['nodename'] 104 | 105 | c.realdata = [ 106 | smilics_dict['a1'][0], 107 | smilics_dict['a2'][0], 108 | smilics_dict['a3'][0], 109 | smilics_dict['at'][0], 110 | smilics_dict['e1'][0], 111 | smilics_dict['e2'][0], 112 | smilics_dict['e3'][0], 113 | smilics_dict['et'][0], 114 | ] 115 | 116 | c.timestamp = time.mktime(datetime.datetime.now().timetuple()) 117 | 118 | return c 119 | except Exception: 120 | return None 121 | 122 | def set(self, **kwargs): 123 | """ Override default settings with settings entered in the config file 124 | """ 125 | for key, setting in self._settings.items(): 126 | if key in kwargs.keys(): 127 | # replace default 128 | self._settings[key] = kwargs[key] 129 | -------------------------------------------------------------------------------- /conf/emonpi2.default.emonhub.conf: -------------------------------------------------------------------------------- 1 | ####################################################################### 2 | ####################### emonhub.conf ######################### 3 | ####################################################################### 4 | ### emonHub configuration file, for info see documentation: 5 | ### https://docs.openenergymonitor.org/emonhub/configuration.html 6 | ####################################################################### 7 | ####################### emonHub settings ####################### 8 | ####################################################################### 9 | 10 | [hub] 11 | ### loglevel must be one of DEBUG, INFO, WARNING, ERROR, and CRITICAL 12 | loglevel = DEBUG 13 | autoconf = 1 14 | ### Uncomment this to also send to syslog 15 | # use_syslog = yes 16 | ####################################################################### 17 | ####################### Interfacers ####################### 18 | ####################################################################### 19 | 20 | [interfacers] 21 | ### This interfacer manages the RFM12Pi/RFM69Pi/emonPi module 22 | [[EmonPi2]] 23 | Type = EmonHubOEMInterfacer 24 | [[[init_settings]]] 25 | com_port = /dev/ttyAMA0 26 | com_baud = 115200 27 | [[[runtimesettings]]] 28 | pubchannels = ToEmonCMS, 29 | subchannels = ToRFM12, 30 | 31 | [[USB0]] 32 | Type = EmonHubOEMInterfacer 33 | [[[init_settings]]] 34 | com_port = /dev/ttyUSB0 35 | com_baud = 115200 36 | [[[runtimesettings]]] 37 | pubchannels = ToEmonCMS, 38 | subchannels = ToRFM12, 39 | nodename = emonTx4 40 | 41 | [[SPI]] 42 | Type = EmonHubRFM69LPLInterfacer 43 | [[[init_settings]]] 44 | nodeid = 5 45 | networkID = 210 46 | resetPin = 24 # remove line if hardware is emonBase RFM69 SPI 47 | selPin = 16 # remove line or change to selPin = 26 if hardware is emonBase RFM69 SPI 48 | [[[runtimesettings]]] 49 | pubchannels = ToEmonCMS, 50 | 51 | 52 | [[MQTT]] 53 | Type = EmonHubMqttInterfacer 54 | [[[init_settings]]] 55 | mqtt_host = 127.0.0.1 56 | mqtt_port = 1883 57 | mqtt_user = emonpi 58 | mqtt_passwd = emonpimqtt2016 59 | 60 | [[[runtimesettings]]] 61 | pubchannels = ToRFM12, 62 | subchannels = ToEmonCMS, 63 | 64 | # emonhub/rx/10/values format 65 | # Use with emoncms Nodes module 66 | node_format_enable = 0 67 | node_format_basetopic = emonhub/ 68 | 69 | # emon/emontx/power1 format - use with Emoncms MQTT input 70 | # http://github.com/emoncms/emoncms/blob/master/docs/RaspberryPi/MQTT.md 71 | nodevar_format_enable = 1 72 | nodevar_format_basetopic = emon/ 73 | 74 | # Single JSON payload published - use with Emoncms MQTT 75 | node_JSON_enable = 0 76 | node_JSON_basetopic = emon/ 77 | 78 | [[emoncmsorg]] 79 | Type = EmonHubEmoncmsHTTPInterfacer 80 | [[[init_settings]]] 81 | [[[runtimesettings]]] 82 | pubchannels = ToRFM12, 83 | subchannels = ToEmonCMS, 84 | url = https://emoncms.org 85 | apikey = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 86 | senddata = 1 # Enable sending data to Emoncms.org 87 | sendnames = 1 # Send full input names (compression will be automatically enabled) 88 | interval = 30 # Bulk send interval to Emoncms.org in seconds 89 | 90 | [[DS18B20]] 91 | Type = EmonHubDS18B20Interfacer 92 | [[[init_settings]]] 93 | [[[runtimesettings]]] 94 | pubchannels = ToEmonCMS, 95 | read_interval = 10 96 | nodename = sensors 97 | # In the two lines following, un-comment the lines and replace the IDs 98 | # and names with those applicable to your system. 99 | 100 | # ids = 28-000008e2db06, 28-000009770529, 28-0000096a49b4 101 | # names = ambient, cyl_bot, cyl_top 102 | 103 | ####################################################################### 104 | ####################### Nodes ####################### 105 | ####################################################################### 106 | 107 | ## See config user guide: https://github.com/openenergymonitor/emonhub 108 | ## If autoconf is enabled above, node configuration will automatically 109 | ## populate based on templates listed in available.conf 110 | 111 | [nodes] 112 | -------------------------------------------------------------------------------- /src/interfacers/EmonHubRFM69LPLInterfacer.py: -------------------------------------------------------------------------------- 1 | 2 | from emonhub_interfacer import EmonHubInterfacer 3 | import Cargo 4 | import time 5 | 6 | """class EmonHubRFM69LPLInterfacer 7 | 8 | Read RFM69 radio data (LowPowerLabs format) 9 | 10 | """ 11 | class EmonHubRFM69LPLInterfacer(EmonHubInterfacer): 12 | 13 | def __init__(self, name, nodeid = 5, networkID = 210, interruptPin = 22, resetPin = None, selPin = 26): 14 | """Initialize Interfacer 15 | 16 | nodeid (integer): radio nodeid 1-1023 17 | networkID (integer): radio networkID 0-255 18 | 19 | """ 20 | try: 21 | import spidev 22 | except ModuleNotFoundError as err: 23 | self._log.error(err) 24 | 25 | try: 26 | import RPi.GPIO as GPIO 27 | self.GPIO = GPIO 28 | GPIO.setwarnings(False) 29 | except ModuleNotFoundError as err: 30 | self._log.error(err) 31 | 32 | self.Radio = False 33 | try: 34 | from RFM69 import Radio 35 | self.Radio = Radio 36 | except ModuleNotFoundError as err: 37 | self._log.error(err) 38 | 39 | # sudo adduser emonhub spi 40 | 41 | # Initialization 42 | super().__init__(name) 43 | 44 | # Watchdog variables 45 | self.last_received = False 46 | self.watchdog_period = 300 47 | 48 | self.node_id = int(nodeid) 49 | self.network_id = int(networkID) 50 | self.interruptPin = int(interruptPin) 51 | self.selPin = int(selPin) 52 | 53 | if resetPin != None and resetPin != 'None': 54 | resetPin = int(resetPin) 55 | else: 56 | resetPin = None 57 | 58 | self.resetPin = resetPin 59 | 60 | self._log.info("Creating RFM69 LowPowerLabs interfacer") 61 | self._log.info("RFM69 node_id = "+str(self.node_id)) 62 | self._log.info("RFM69 network_id = "+str(self.network_id)) 63 | self._log.info("RFM69 interruptPin = "+str(self.interruptPin)) 64 | self._log.info("RFM69 resetPin = "+str(self.resetPin)) 65 | self._log.info("RFM69 selPin = "+str(self.selPin)) 66 | 67 | self._log.info("Starting radio setup") 68 | self.connect() 69 | 70 | def connect(self): 71 | """Connect to RFM69 72 | 73 | """ 74 | self._log.info("Connecting to RFM69") 75 | self.last_received = False 76 | 77 | board = {'isHighPower': False, 'interruptPin': self.interruptPin, 'resetPin': self.resetPin, 'selPin':self.selPin, 'spiDevice': 0, 'encryptionKey':"89txbe4p8aik5kt3"} 78 | self.radio = self.Radio(43, self.node_id, self.network_id, verbose=False, **board) 79 | 80 | if not self.radio.init_success: 81 | self._log.error("Could not connect to RFM69 module") 82 | else: 83 | self._log.info("Radio setup complete") 84 | self.last_packet_nodeid = 0 85 | self.last_packet_data = [] 86 | self.last_packet_time = 0 87 | self.radio.__enter__() 88 | 89 | 90 | def shutdown(self): 91 | self.radio.__exit__() 92 | pass 93 | 94 | def read(self): 95 | """Read data from RFM69 96 | 97 | """ 98 | if not self.radio.init_success: 99 | return False 100 | 101 | packet = self.radio.get_packet() 102 | if packet: 103 | self._log.info("Packet received "+str(len(packet.data))+" bytes") 104 | # Make sure packet is a unique new packet rather than a 2nd or 3rd retry attempt 105 | if packet.sender==self.last_packet_nodeid and packet.data==self.last_packet_data and (time.time()-self.last_packet_time)<0.5: 106 | self._log.info("Discarding duplicate packet") 107 | return False 108 | 109 | self.last_packet_nodeid = packet.sender 110 | self.last_packet_data = packet.data 111 | self.last_packet_time = time.time() 112 | # Process packet 113 | c = Cargo.new_cargo(rawdata='') 114 | c.nodeid = packet.sender 115 | c.realdata = packet.data 116 | c.rssi = packet.RSSI 117 | 118 | # Set watchdog timer 119 | self.last_received = time.time() 120 | return c 121 | 122 | if self.last_received and (time.time()-self.last_received) > self.watchdog_period: 123 | self._log.warning("No radio packets received in last "+str(self.watchdog_period)+" seconds, restarting radio") 124 | self.connect() 125 | 126 | return False 127 | 128 | def set(self, **kwargs): 129 | """ 130 | 131 | """ 132 | # include kwargs from parent 133 | super().set(**kwargs) 134 | -------------------------------------------------------------------------------- /src/interfacers/EmonHubGraphiteInterfacer.py: -------------------------------------------------------------------------------- 1 | """class EmonHubGraphiteInterfacer 2 | """ 3 | import time 4 | import socket 5 | from emonhub_interfacer import EmonHubInterfacer 6 | 7 | class EmonHubGraphiteInterfacer(EmonHubInterfacer): 8 | 9 | def __init__(self, name): 10 | # Initialization 11 | super().__init__(name) 12 | 13 | self._defaults.update({'batchsize': 100, 'interval': 30}) 14 | self._settings.update(self._defaults) 15 | 16 | # interfacer specific settings 17 | self._graphite_settings = { 18 | 'graphite_host': 'localhost', 19 | 'graphite_port': '2003', 20 | 'prefix': 'emonpi' 21 | } 22 | 23 | self.lastsent = time.time() 24 | self.lastsentstatus = time.time() 25 | 26 | # set an absolute upper limit for number of items to process per post 27 | self._item_limit = 250 28 | 29 | def add(self, cargo): 30 | """Append data to buffer. 31 | 32 | format: {"emontx":{"power1":100,"power2":200,"power3":300}} 33 | 34 | """ 35 | 36 | nodename = str(cargo.nodeid) 37 | if cargo.nodename: 38 | nodename = cargo.nodename 39 | 40 | f = {} 41 | f['node'] = nodename 42 | f['data'] = {} 43 | 44 | # FIXME replace with zip 45 | for i in range(len(cargo.realdata)): 46 | name = str(i + 1) 47 | if i < len(cargo.names): 48 | name = cargo.names[i] 49 | value = cargo.realdata[i] 50 | f['data'][name] = value 51 | 52 | if cargo.rssi: 53 | f['data']['rssi'] = cargo.rssi 54 | 55 | self.buffer.storeItem(f) 56 | 57 | 58 | def _process_post(self, databuffer): 59 | timestamp = int(time.time()) 60 | 61 | metrics = [] 62 | for frame in databuffer: 63 | nodename = frame['node'] 64 | 65 | for inputname, value in frame['data'].items(): 66 | # path 67 | path = self._settings['prefix'] + '.' + nodename + "." + inputname 68 | # payload 69 | payload = str(value) 70 | # timestamp 71 | #timestamp = frame['timestamp'] 72 | 73 | metrics.append(path + " " + payload + " " + str(timestamp)) 74 | 75 | return self._send_metrics(metrics) 76 | 77 | def _send_metrics(self, metrics=[]): 78 | """ 79 | 80 | :param post_url: 81 | :param post_body: 82 | :return: the received reply if request is successful 83 | """ 84 | """Send data to server. 85 | 86 | metrics (list): metric path and values (eg: '["path.node1 val1 time","path.node2 val2 time",...]') 87 | 88 | return True if data sent correctly 89 | 90 | """ 91 | 92 | host = self._settings['graphite_host'].strip("[']") 93 | port = int(self._settings['graphite_port'].strip("[']")) 94 | self._log.debug("Graphite target: %s:%s", host, port) 95 | message = '\n'.join(metrics) + '\n' 96 | self._log.debug("Sending metrics: %s", message) 97 | 98 | try: 99 | sock = socket.socket() 100 | sock.connect((host, port)) 101 | sock.sendall(message.encode()) 102 | sock.close() 103 | except socket.error as e: 104 | self._log.error(e) 105 | return False 106 | 107 | return True 108 | 109 | def set(self, **kwargs): 110 | super().set(**kwargs) 111 | for key, setting in self._graphite_settings.items(): 112 | if key in kwargs: 113 | # replace default 114 | self._settings[key] = kwargs[key] 115 | 116 | """ 117 | def set(self, **kwargs): 118 | super ().set(**kwargs) 119 | for key, setting in self._graphite_settings.items(): 120 | #valid = False 121 | if key not in kwargs: 122 | setting = self._graphite_settings[key] 123 | else: 124 | setting = kwargs[key] 125 | if key in self._settings and self._settings[key] == setting: 126 | continue 127 | elif key == 'graphite_host': 128 | self._log.info("Setting %s graphite_host: %s", self.name, setting) 129 | self._settings[key] = setting 130 | continue 131 | elif key == 'graphite_port': 132 | self._log.info("Setting %s graphite_port: %s", self.name, setting) 133 | self._settings[key] = setting 134 | continue 135 | elif key == 'prefix': 136 | self._log.info("Setting %s prefix: %s", self.name, setting) 137 | self._settings[key] = setting 138 | continue 139 | else: 140 | self._log.warning("'%s' is not valid for %s: %s", setting, self.name, key) 141 | """ 142 | -------------------------------------------------------------------------------- /src/interfacers/EmonHubGoodWeInterfacer.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | # EmonHubSMASolarInterfacer released for use by OpenEnergyMonitor project 3 | # GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 4 | # See LICENCE and README file for details 5 | 6 | __author__ = 'Jo Vanvoorden' 7 | 8 | import asyncio 9 | import Cargo 10 | import time 11 | 12 | from emonhub_interfacer import EmonHubInterfacer 13 | from goodwe import Goodwe_inverter 14 | 15 | 16 | """class EmonHubGoodWeInterfacer 17 | 18 | Fetch GoodWe state of charge and other variables 19 | 20 | """ 21 | 22 | class EmonHubGoodWeInterfacer(EmonHubInterfacer): 23 | 24 | def __init__(self, name): 25 | super().__init__(name) 26 | 27 | # Interfacer specific settings 28 | self._template_settings = {'name': 'goodwe', 29 | 'ip': None, 30 | 'port': 8899, 31 | 'timeout': 2, 32 | 'retries': 3, 33 | 'readinterval': 10.0} 34 | 35 | # FIXME is there a good reason to reduce this from the default of 1000? If so, document it here. 36 | # set an absolute upper limit for number of items to process per post 37 | self._item_limit = 250 38 | 39 | 40 | # Fetch first reading at one interval lengths time 41 | self._last_time = 0 42 | 43 | def read(self): 44 | # Request GoodWe data at user specified interval 45 | if time.time() - self._last_time >= self._settings['readinterval']: 46 | self._last_time = time.time() 47 | 48 | # If URL is set, fetch the SOC 49 | if self._settings['ip'] != None: 50 | try: 51 | self._inverter = asyncio.run(Goodwe_inverter.discover(self._settings['ip'], self._settings['port'], self._settings['timeout'], self._settings['retries'])) 52 | data = asyncio.run(self._inverter.read_runtime_data()) 53 | except asyncio.CancelledError: 54 | self._log.warning("The task %s is cancelled", self.name) 55 | 56 | self._log.debug("%s Request response: %s", self.name, data) 57 | 58 | names = [] 59 | values = [] 60 | 61 | for key in self._inverter.sensors(): 62 | self._log.debug("Key %s found", key.id) 63 | # Check if key.id is in data object readout 64 | if not key.id in data: 65 | self._log.warning("Key %s not found", key.id) 66 | return 67 | # Check if we have a numerical response and return it 68 | if isinstance(data[key.id], (int, float)): 69 | names.append(key.id) 70 | values.append(data[key.id]) 71 | 72 | # Create cargo object 73 | c = Cargo.new_cargo() 74 | c.nodeid = self._settings['name'] 75 | 76 | c.names = names 77 | c.realdata = values 78 | return c 79 | 80 | # return empty if not time 81 | return 82 | 83 | def set(self, **kwargs): 84 | for key, setting in self._template_settings.items(): 85 | # Decide which setting value to use 86 | if key in kwargs.keys(): 87 | setting = kwargs[key] 88 | else: 89 | setting = self._template_settings[key] 90 | if key in self._settings and self._settings[key] == setting: 91 | continue 92 | elif key == 'readinterval': 93 | self._log.info("Setting %s %s: %s", self.name, key, setting) 94 | self._settings[key] = float(setting) 95 | continue 96 | elif key == 'name': 97 | self._log.info("Setting %s %s: %s", self.name, key, setting) 98 | self._settings[key] = setting 99 | continue 100 | elif key == 'ip': 101 | self._log.info("Setting %s %s: %s", self.name, key, setting) 102 | self._settings[key] = setting 103 | continue 104 | elif key == 'port': 105 | self._log.info("Setting %s %s: %s", self.name, key, setting) 106 | self._settings[key] = setting 107 | continue 108 | elif key == 'timeout': 109 | self._log.info("Setting %s %s: %s", self.name, key, setting) 110 | self._settings[key] = setting 111 | continue 112 | elif key == 'retries': 113 | self._log.info("Setting %s %s: %s", self.name, key, setting) 114 | self._settings[key] = setting 115 | continue 116 | else: 117 | self._log.warning("'%s' is not valid for %s: %s", setting, self.name, key) 118 | 119 | # include kwargs from parent 120 | super().set(**kwargs) 121 | -------------------------------------------------------------------------------- /src/emonhub_setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | This code is released under the GNU Affero General Public License. 4 | 5 | OpenEnergyMonitor project: 6 | http://openenergymonitor.org 7 | 8 | """ 9 | 10 | import time 11 | import logging 12 | from configobj import ConfigObj 13 | 14 | """class EmonHubSetup 15 | 16 | User interface to setup the hub. 17 | 18 | The settings attribute stores the settings of the hub. It is a 19 | dictionary with the following keys: 20 | 21 | 'hub': a dictionary containing the hub settings 22 | 'interfacers': a dictionary containing the interfacers 23 | 24 | The hub settings are: 25 | 'loglevel': the logging level 26 | 27 | interfacers are dictionaries with the following keys: 28 | 'Type': class name 29 | 'init_settings': dictionary with initialization settings 30 | 'runtimesettings': dictionary with runtime settings 31 | Initialization and runtime settings depend on the interfacer type. 32 | 33 | The run() method is supposed to be run regularly by the instantiater, to 34 | perform regular communication tasks. 35 | 36 | The check_settings() method is run regularly as well. It checks the settings 37 | and returns True is settings were changed. 38 | 39 | This almost empty class is meant to be inherited by subclasses specific to 40 | each setup. 41 | 42 | """ 43 | 44 | class EmonHubSetup: 45 | def __init__(self): 46 | # Initialize logger 47 | self._log = logging.getLogger("EmonHub") 48 | 49 | # Initialize settings 50 | self.settings = None 51 | 52 | def run(self): 53 | """Run in background. 54 | 55 | To be implemented in child class. 56 | 57 | """ 58 | pass 59 | 60 | def check_settings(self): 61 | """Check settings 62 | 63 | Update attribute settings and return True if modified. 64 | 65 | To be implemented in child class. 66 | 67 | """ 68 | 69 | 70 | class EmonHubFileSetup(EmonHubSetup): 71 | def __init__(self, filename): 72 | # Initialization 73 | super().__init__() 74 | 75 | self._filename = filename 76 | 77 | # Initialize update timestamp 78 | self._settings_update_timestamp = 0 79 | self._retry_time_interval = 5 80 | 81 | self.retry_msg = " Retry in " + str(self._retry_time_interval) + " seconds" 82 | 83 | # Initialize attribute settings as a ConfigObj instance 84 | try: 85 | self.settings = ConfigObj(filename, file_error=True) 86 | 87 | # Check the settings file sections 88 | self.settings['hub'] 89 | self.settings['interfacers'] 90 | except IOError as e: 91 | raise EmonHubSetupInitError(e) 92 | except SyntaxError as e: 93 | raise EmonHubSetupInitError( 94 | 'Error parsing config file "%s": ' % filename + str(e)) 95 | except KeyError as e: 96 | raise EmonHubSetupInitError( 97 | 'Configuration file error - section: ' + str(e)) 98 | 99 | def check_settings(self): 100 | """Check settings 101 | 102 | Update attribute settings and return True if modified. 103 | 104 | """ 105 | 106 | # Check settings only once per second (could be extended if processing power is scarce) 107 | now = time.time() 108 | if now - self._settings_update_timestamp < 60: 109 | return 110 | # Update timestamp 111 | self._settings_update_timestamp = now 112 | 113 | # Backup settings 114 | settings = dict(self.settings) 115 | 116 | # Get settings from file 117 | try: 118 | self.settings.reload() 119 | except IOError as e: 120 | self._log.warning('Could not get settings: %s %s', e, self.retry_msg) 121 | self._settings_update_timestamp = now + self._retry_time_interval 122 | return 123 | except SyntaxError as e: 124 | self._log.warning('Could not get settings: ' + 125 | 'Error parsing config file: %s %s', e, self.retry_msg) 126 | self._settings_update_timestamp = now + self._retry_time_interval 127 | return 128 | except Exception: 129 | import traceback 130 | self._log.warning("Couldn't get settings, Exception: %s %s", 131 | traceback.format_exc(), self.retry_msg) 132 | self._settings_update_timestamp = now + self._retry_time_interval 133 | return 134 | 135 | if self.settings != settings: 136 | # Check the settings file sections 137 | try: 138 | self.settings['hub'] 139 | self.settings['interfacers'] 140 | except KeyError as e: 141 | self._log.warning("Configuration file missing section: %s", e) 142 | else: 143 | return True 144 | 145 | """class EmonHubSetupInitError 146 | 147 | Raise this when init fails. 148 | 149 | """ 150 | class EmonHubSetupInitError(Exception): 151 | pass 152 | -------------------------------------------------------------------------------- /src/interfacers/EmonHubTemplateInterfacer.py: -------------------------------------------------------------------------------- 1 | import time 2 | import json 3 | from itertools import zip_longest 4 | import Cargo 5 | from emonhub_interfacer import EmonHubInterfacer 6 | 7 | """class EmonHubTemplateInterfacer 8 | 9 | Template interfacer for use in development 10 | 11 | """ 12 | 13 | class EmonHubTemplateInterfacer(EmonHubInterfacer): 14 | 15 | def __init__(self, name, port_nb=50011): 16 | """Initialize Interfacer 17 | 18 | """ 19 | 20 | # Initialization 21 | super().__init__(name) 22 | 23 | # add or alter any default settings for this interfacer 24 | # defaults previously defined in inherited emonhub_interfacer 25 | # here we are just changing the batchsize from 1 to 100 26 | # and the interval from 0 to 30 27 | # self._defaults.update({'batchsize': 100,'interval': 30}) 28 | 29 | # This line will stop the default values printing to logfile at start-up 30 | self._settings.update(self._defaults) 31 | 32 | # Interfacer specific settings 33 | # (settings not included in the inherited EmonHubInterfacer) 34 | # The set method below is called from emonhub.py on 35 | # initialisation and settings change and copies the 36 | # interfacer specific settings over to _settings 37 | 38 | # read_interval is just an example setting here 39 | # and can be removed and replaced with applicable settings 40 | self._template_settings = {'read_interval': 10.0} 41 | 42 | # set an absolute upper limit for number of items to process per post 43 | self._item_limit = 250 44 | 45 | def read(self): 46 | """Read data and process 47 | 48 | Return data as a list: [NodeID, val1, val2] 49 | 50 | """ 51 | 52 | # create a new cargo object, set data values 53 | c = Cargo.new_cargo() 54 | 55 | # Example cargo data 56 | # An interfacer would typically at this point 57 | # read from a socket or serial port and decode 58 | # the read data before setting the cargo object 59 | # variables 60 | c.nodeid = "test" 61 | c.names = ["power1", "power2", "power3"] 62 | c.realdata = [100, 200, 300] 63 | 64 | # usually the serial port or socket will provide 65 | # a delay as the interfacer waits at this point 66 | # to read a line of data but for testing here 67 | # we slow it down. 68 | 69 | time.sleep(self._settings['read_interval']) 70 | 71 | return c 72 | 73 | def add(self, cargo): 74 | """Append data to buffer. 75 | 76 | format: {"emontx":{"power1":100,"power2":200,"power3":300}} 77 | 78 | """ 79 | 80 | nodename = str(cargo.nodeid) 81 | if cargo.nodename: 82 | nodename = cargo.nodename 83 | 84 | f = {'node': nodename, 85 | 'data': {} 86 | } 87 | 88 | # FIXME zip_longest mimics the previous behaviour of this code which 89 | # filled the gaps with a numeric string. However it's surely an error 90 | # to provide more data than the schema expects, so it should either 91 | # be an explicit error or silently dropped. 92 | # If that's the case all this code can be simplified to: 93 | # f['data'] = {name: value for name, value in zip(cargo.names, cargo.realdata)} 94 | for i, (name, value) in enumerate(zip_longest(cargo.names, cargo.realdata, fill_value=None), start=1): 95 | f['data'][name or str(i)] = value 96 | 97 | if cargo.rssi: 98 | f['data']['rssi'] = cargo.rssi 99 | 100 | self.buffer.storeItem(f) 101 | 102 | def _process_post(self, databuffer): 103 | """Send data to server/broker or other output 104 | 105 | """ 106 | 107 | for frame in databuffer: 108 | # Here we might typically publish or post the data 109 | # via MQTT, HTTP a socket or other output 110 | self._log.debug("node = %s node_data = %s", frame['node'], json.dumps(frame['data'])) 111 | 112 | # We could check for successful data receipt here 113 | # and return false to retry next time 114 | # if not success: return False 115 | 116 | return True 117 | 118 | def set(self, **kwargs): 119 | for key, setting in self._template_settings.items(): 120 | # Decide which setting value to use 121 | if key in kwargs: 122 | setting = kwargs[key] 123 | else: 124 | setting = self._template_settings[key] 125 | if key in self._settings and self._settings[key] == setting: 126 | continue 127 | elif key == 'read_interval': 128 | self._log.info("Setting %s read_interval: %s", self.name, setting) 129 | self._settings[key] = float(setting) 130 | continue 131 | else: 132 | self._log.warning("'%s' is not valid for %s: %s", setting, self.name, key) 133 | 134 | # include kwargs from parent 135 | super().set(**kwargs) 136 | -------------------------------------------------------------------------------- /conf/available.conf: -------------------------------------------------------------------------------- 1 | [available] 2 | 3 | [[emonpi]] 4 | nodename = emonpi 5 | nodeids = 5, 6 | [[[rx]]] 7 | names = power1,power2,power1pluspower2,vrms,t1,t2,t3,t4,t5,t6,pulsecount 8 | datacodes = h, h, h, h, h, h, h, h, h, h, L 9 | scales = 1,1,1,0.01,0.1,0.1,0.1,0.1,0.1,0.1,1 10 | units = W,W,W,V,C,C,C,C,C,C,p 11 | 12 | [[emonpiCM]] 13 | nodename = emonpiCM 14 | nodeids = 5, 15 | [[[rx]]] 16 | names = Msg,power1,power2,power1pluspower2,vrms,t1,t2,t3,t4,t5,t6,pulse1count,pulse2count,E1,E2 17 | datacodes = L, h, h, h, h, h, h, h, h, h, h, L, L, l, l 18 | scales = 1,1,1,1, 0.01, 0.01,0.01,0.01,0.01,0.01,0.01, 1, 1, 1,1 19 | units = n,W,W,W, V, C,C,C,C,C,C, p, p, Wh,Wh 20 | 21 | [[emontxshield]] 22 | nodename = emontxshield 23 | nodeids = 6, 24 | [[[rx]]] 25 | names = power1, power2, power3, power4, vrms 26 | datacodes = h,h,h,h,h 27 | scales = 1,1,1,1,0.01 28 | units = W,W,W,W,V 29 | 30 | [[emontx3_discreet]] 31 | nodename = emontx3 32 | nodeids = 7,8,9,10 33 | [[[rx]]] 34 | names = power1, power2, power3, power4, vrms, temp1, temp2, temp3, temp4, temp5, temp6, pulse 35 | datacodes = h,h,h,h,h,h,h,h,h,h,h,L 36 | scales = 1,1,1,1,0.01,0.1,0.1, 0.1,0.1,0.1,0.1,1 37 | units = W,W,W,W,V,C,C,C,C,C,C,p 38 | 39 | [[emontx3_3phase]] 40 | nodename = 3phase 41 | nodeids = 11,12,13,14 42 | [[[rx]]] 43 | names = powerL1, powerL2, powerL3, power4, Vrms, temp1, temp2, temp3, temp4, temp5, temp6, pulse 44 | datacodes = h,h,h,h,h,h,h,h,h,h,h,L 45 | scales = 1,1,1,1,0.01,0.01,0.01,0.01,0.01,0.01,0.01,1 46 | units = W,W,W,W,V,C,C,C,C,C,C,p 47 | 48 | [[emontx3_cm]] 49 | nodename = emontx3cm 50 | nodeids = 15,16 51 | [[[rx]]] 52 | names = MSG, Vrms, P1, P2, P3, P4, E1, E2, E3, E4, T1, T2, T3, pulse 53 | datacodes = L,h,h,h,h,h,l,l,l,l,h,h,h,L 54 | scales = 1,0.01,1,1,1,1,1,1,1,1,0.01,0.01,0.01,1 55 | units = n,V,W,W,W,W,Wh,Wh,Wh,Wh,C,C,C,p 56 | whitening = 1 57 | 58 | [[emontx3_cm_rf69n]] 59 | nodename = emontx3cm 60 | nodeids = 15,16 61 | [[[rx]]] 62 | names = MSG2, Vrms, P1, P2, P3, P4, E1, E2, E3, E4, T1, T2, T3, pulse 63 | datacodes = L,h,h,h,h,h,l,l,l,l,h,h,h,L 64 | scales = 1,0.01,1,1,1,1,1,1,1,1,0.01,0.01,0.01,1 65 | units = n,V,W,W,W,W,Wh,Wh,Wh,Wh,C,C,C,p 66 | whitening = 0 67 | 68 | [[emonth1]] 69 | nodename = emonth 70 | nodeids = 19,20,21,22 71 | [[[rx]]] 72 | names = temperature, external temperature, humidity, battery 73 | datacodes = h,h,h,h 74 | scales = 0.1,0.1,0.1,0.1 75 | units = C,C,%,V 76 | 77 | [[emonth2]] 78 | nodename = emonth 79 | nodeids = 23,24,25,26 80 | [[[rx]]] 81 | names = temperature, external temperature, humidity, battery, pulsecount 82 | datacodes = h,h,h,h,L 83 | scales = 0.1,0.1,0.1,0.1,1 84 | units = C,C,%,V,p 85 | 86 | [[emonTx4]] 87 | nodename = emonTx4 88 | nodeids = 17,18 89 | [[[rx]]] 90 | names = MSG, Vrms, P1, P2, P3, P4, P5, P6, E1, E2, E3, E4, E5, E6, T1, T2, T3, pulse 91 | datacodes = L,h,h,h,h,h,h,h,l,l,l,l,l,l,h,h,h,L 92 | scales = 1,0.01,1,1,1,1,1,1,1,1,1,1,1,1,0.01,0.01,0.01,1 93 | units = n,V,W,W,W,W,W,W,Wh,Wh,Wh,Wh,Wh,Wh,C,C,C,p 94 | 95 | [[emonTx4_3phase]] 96 | nodename = EmonTx4_DB_3phase 97 | nodeids = 27, 98 | [[[rx]]] 99 | names = MSG, V1, V2, V3, P1, P2, P3, P4, P5, P6, E1, E2, E3, E4, E5, E6, pulse 100 | datacodes = L, h,h,h, h,h,h,h,h,h, l,l,l,l,l,l ,L 101 | scales = 1,0.01,0.01,0.01,1,1,1,1,1,1,1,1,1,1,1 102 | units = n,V,V,V,W,W,W,W,W,W,Wh,Wh,Wh,Wh,Wh,Wh,p 103 | 104 | 105 | [[emon_DB_6CT_1phase]] 106 | nodename = emon_DB_6CT_1phase 107 | nodeids = 27 108 | [[[rx]]] 109 | names = MSG, V1, P1, P2, P3, P4, P5, P6, E1, E2, E3, E4, E5, E6, pulse 110 | datacodes = L, h, h,h,h,h,h,h, l,l,l,l,l,l ,L 111 | scales = 1,0.01,1,1,1,1,1,1,1,1,1,1,1 112 | units = n,V,W,W,W,W,W,W,Wh,Wh,Wh,Wh,Wh,Wh,p 113 | 114 | [[EmonTx4DB_rf1]] 115 | nodename = EmonTx4DB_rf1 116 | nodeids = 28, 117 | [[[rx]]] 118 | names = MSG, Vrms1, Vrms2, Vrms3, P1, P2, P3, P4, P5, P6, E1, E2, E3, E4, E5, E6, pulse, Analog 119 | datacodes = L, h, h, h, h, h, h, h, h, h, l, l, l, l, l, l, L, H 120 | scales = 1.0, 0.01, 0.01, 0.01, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 121 | units = n, V, V, V, W, W, W, W, W, W, Wh, Wh, Wh, Wh, Wh, Wh, p, n 122 | 123 | [[EmonTx4DB_rf2]] 124 | nodename = EmonTx4DB_rf2 125 | nodeids = 29, 126 | [[[rx]]] 127 | names = MSG, Vrms2, Vrms3, P7, P8, P9, P10, P11, P12, E7, E8, E9, E10, E11, E12, digPulse, anaPulse 128 | datacodes = L, h, h, h, h, h, h, h, h, l, l, l, l, l, l, L, L 129 | scales = 1.0, 0.01, 0.01, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 130 | units = n, V, V, W, W, W, W, W, W, Wh, Wh, Wh, Wh, Wh, Wh, p, p 131 | 132 | -------------------------------------------------------------------------------- /src/interfacers/EmonHubSunampInterfacer.py: -------------------------------------------------------------------------------- 1 | import time 2 | import json 3 | import datetime 4 | import Cargo 5 | import re 6 | 7 | from . import EmonHubSerialInterfacer as ehi 8 | 9 | """class EmonHubSunampInterfacer 10 | 11 | Monitors the serial port for data from 'Serial Format 2' type devices 12 | 13 | """ 14 | 15 | class EmonHubSunampInterfacer(ehi.EmonHubSerialInterfacer): 16 | 17 | def __init__(self, name, com_port='/dev/ttyUSB0', com_baud=115200): 18 | """Initialize Interfacer 19 | 20 | com_port (string): path to COM port 21 | 22 | """ 23 | 24 | # Initialization 25 | super().__init__(name, com_port, com_baud) 26 | 27 | # Display device firmware version and current settings 28 | self.info = ["", ""] 29 | 30 | self._rx_buf = "" 31 | # self._ser.flushInput() 32 | 33 | # Initialize settings 34 | self._defaults.update({'pause': 'off', 'interval': 0, 'nodename': name}) 35 | 36 | # This line will stop the default values printing to logfile at start-up 37 | # unless they have been overwritten by emonhub.conf entries 38 | # comment out if diagnosing a startup value issue 39 | self._settings.update(self._defaults) 40 | 41 | self._com_port = com_port 42 | self._com_baud = com_baud 43 | self._last_connection_attempt = time.time() 44 | 45 | 46 | def read(self): 47 | 48 | if not self._ser: 49 | if (time.time()-self._last_connection_attempt)>=10: 50 | self._last_connection_attempt = time.time() 51 | self._ser = self._open_serial_port(self._com_port, self._com_baud) 52 | 53 | if not self._ser: 54 | return 55 | 56 | # Read serial RX 57 | try: 58 | ser_data = self._ser.readline() 59 | self._rx_buf = self._rx_buf + ser_data.decode() 60 | except UnicodeDecodeError: 61 | return 62 | except Exception as e: 63 | self._log.error(e) 64 | self._ser = False 65 | 66 | # If line incomplete, exit 67 | if '\n' not in self._rx_buf: 68 | return 69 | 70 | # Remove CR,LF. 71 | f = self._rx_buf[:-1].strip() 72 | 73 | # Reset buffer 74 | self._rx_buf = '' 75 | 76 | if not f: 77 | return 78 | 79 | if f[0] == '\x01': 80 | return 81 | 82 | c = Cargo.new_cargo(rawdata=f) 83 | c.names = [] 84 | c.realdata = [] 85 | 86 | # print() 87 | 88 | # Fetch default nodename from settings 89 | if self._settings["nodename"] != "": 90 | c.nodename = self._settings["nodename"] 91 | c.nodeid = self._settings["nodename"] 92 | 93 | 94 | # Example sunamp data: 95 | # e V12.2.0 4/1/0 F:0, TS: 81.78, 27.08, 23.65, err: 0, SOHT: 0, ELCD: 1, extD: 0, SOC: 0, CHG: 1, DC_R1: 1, DC_R2: 0, RLY: 1, RLY1: 0, CL: 1, L3: 0, DSR: 0 96 | 97 | # check that first character is 'e' 98 | if f[0] != 'e': 99 | return False 100 | 101 | # split the string into parts 102 | csv_parts = f.split(',') 103 | 104 | # first part beyond e contains, version number, something else and then F:0 105 | first = csv_parts[0].split(' ') 106 | 107 | # get version 108 | version = first[1] 109 | 110 | # add version to the list of names 111 | c.names.append('version') 112 | # only allow 0-9 characters in version 113 | c.realdata.append(int(re.sub(r'[^\d]', '', version))) 114 | 115 | unknown = first[2] 116 | c.names.append('unknown') 117 | c.realdata.append(int(re.sub(r'[^\d]', '', unknown))) 118 | 119 | # check if F: 120 | if 'F:' in first[3]: 121 | # add this to the list of names 122 | c.names.append('F') 123 | # add the value to the list of realdata 124 | c.realdata.append(float(first[3].split(':')[1])) 125 | 126 | # The first 127 | 128 | key = "" 129 | key_index = 1 130 | 131 | for kv_str in csv_parts[1:]: 132 | if ":" in kv_str: 133 | kv = kv_str.split(':') 134 | key = kv[0] 135 | value = kv[1] 136 | key_index = 1 137 | else: 138 | value = kv_str 139 | key_index += 1 140 | 141 | try: 142 | # add the key to the list of names 143 | if key_index == 1: 144 | c.names.append(key) 145 | else: 146 | c.names.append(key+str(key_index)) 147 | # add the value to the list of realdata 148 | c.realdata.append(float(value)) 149 | except Exception as e: 150 | return False 151 | 152 | self._log.debug("names: %s", c.names) 153 | 154 | if len(c.realdata) == 0: 155 | return False 156 | else: 157 | return c 158 | 159 | 160 | def set(self, **kwargs): 161 | for key, setting in self._settings.items(): 162 | if key in kwargs: 163 | self._settings[key] = kwargs[key] -------------------------------------------------------------------------------- /src/interfacers/EmonHubBleInterfacer.py: -------------------------------------------------------------------------------- 1 | import time 2 | import struct 3 | 4 | try: 5 | from bluepy import btle 6 | btle_found = True 7 | except ImportError: 8 | btle_found = False 9 | 10 | import Cargo 11 | from emonhub_interfacer import EmonHubInterfacer 12 | 13 | """class EmonhubBleInterfacer 14 | 15 | Polls a Bluetooth LE sensor for temperature, huimidity and battery level 16 | 17 | Currently only tested with a Silicon Labs Thunderboard Sense 2 18 | 19 | 20 | Example config snippets: 21 | 22 | [interfacers] 23 | 24 | [[blesensor]] 25 | Type = EmonHubBleInterfacer 26 | [[[init_settings]]] 27 | device_addr = '00:0b:57:64:8c:a2' 28 | [[[runtimesettings]]] 29 | pubchannels = ToEmonCMS, 30 | read_interval = 20 31 | 32 | [nodes] 33 | 34 | [[1]] 35 | nodename = Sensornode 36 | 37 | [[[rx]]] 38 | names = temp,humidity,battery 39 | scales = 0.01,0.01,1 40 | units = C,%,% 41 | 42 | """ 43 | 44 | class EmonHubBleInterfacer(EmonHubInterfacer): 45 | 46 | def __init__(self, name, device_addr): 47 | """Initialize interfacer 48 | 49 | device_addr (string): BLE MAC address to connect to 50 | 51 | """ 52 | 53 | # Initialization 54 | super(EmonHubBleInterfacer, self).__init__(name) 55 | 56 | self._private_settings = { 57 | 'read_interval': 60 58 | } 59 | 60 | self._addr = device_addr 61 | self._last_read_time = 0 62 | self._bat_readings = [] 63 | if btle_found: 64 | self._connect() 65 | else: 66 | self._log.error("EmonHubBleInterfacer bluepy module btle not found") 67 | 68 | def close(self): 69 | """Close serial port""" 70 | 71 | # Close serial port 72 | if self._ble: 73 | self._log.debug("Closing Bluetooth connection") 74 | self._ble.disconnect() 75 | 76 | return 77 | 78 | def read(self): 79 | """Read data from bluetooth sensor 80 | 81 | """ 82 | if not btle_found: 83 | return False 84 | 85 | # Don't read before the configured interval 86 | interval = int(self._private_settings['read_interval']) 87 | if time.time() - self._last_read_time < interval: 88 | return 89 | 90 | self._last_read_time = time.time() 91 | 92 | # Check connection, connect if we didn't connect during init 93 | if not self._ble: 94 | self._connect() 95 | 96 | if not self._ble: 97 | return False 98 | 99 | temp = self._get_temperature() 100 | rh = self._get_humidity() 101 | bat = self._get_bat_level() 102 | 103 | # Create a Payload object 104 | c = Cargo.new_cargo() 105 | c.realdata = (temp, rh, bat) 106 | 107 | if int(self._settings['nodeoffset']): 108 | c.nodeid = int(self._settings['nodeoffset']) 109 | else: 110 | c.nodeid = 1 111 | 112 | return c 113 | 114 | def set(self, **kwargs): 115 | 116 | for key, setting in self._private_settings.items(): 117 | # Decide which setting value to use 118 | if key in kwargs: 119 | setting = kwargs[key] 120 | else: 121 | setting = self._private_settings[key] 122 | 123 | # Ignore unchanged 124 | if key in self._settings and self._settings[key] == setting: 125 | continue 126 | elif key == 'read_interval': 127 | setting = float(setting) 128 | else: 129 | self._log.warning("'%s' is not valid for %s: %s" % (str(setting), self.name, key)) 130 | continue 131 | 132 | self._log.debug('Setting {}: {}'.format(key, setting)) 133 | self._private_settings[key] = setting 134 | 135 | # include kwargs from parent 136 | super(EmonHubBleInterfacer, self).set(**kwargs) 137 | 138 | def _get_temperature(self): 139 | val = self._temperature.read() 140 | (val,) = struct.unpack('h', val) 141 | return val 142 | 143 | def _get_humidity(self): 144 | val = self._humidity.read() 145 | (val,) = struct.unpack('h', val) 146 | return val 147 | 148 | def _get_bat_level(self): 149 | val = self._bat_level.read() 150 | (val,) = struct.unpack('B', val) 151 | 152 | # The battery reading is very noisy - do a simple average 153 | self._bat_readings.insert(0, val) 154 | self._bat_readings = self._bat_readings[0:20] 155 | 156 | val = sum(self._bat_readings)//len(self._bat_readings) 157 | 158 | return round(val) 159 | 160 | def _connect(self): 161 | self._log.debug("Connecting to BLE address {}...".format(self._addr)) 162 | self._ble = None 163 | 164 | try: 165 | self._ble = btle.Peripheral(self._addr) 166 | except btle.BTLEException as e: 167 | self._log.exception("Failed to read from BTLE device") 168 | return False 169 | 170 | self._temperature = self._ble.getCharacteristics(uuid=btle.AssignedNumbers.temperature)[0] 171 | self._humidity = self._ble.getCharacteristics(uuid=btle.AssignedNumbers.humidity)[0] 172 | self._bat_level = self._ble.getCharacteristics(uuid=btle.AssignedNumbers.battery_level)[0] 173 | 174 | return True 175 | 176 | -------------------------------------------------------------------------------- /src/interfacers/EmonHubPacketGenInterfacer.py: -------------------------------------------------------------------------------- 1 | import time 2 | import requests 3 | from Cargo import new_cargo 4 | from emonhub_interfacer import EmonHubInterfacer 5 | 6 | """class EmonHubPacketGenInterfacer 7 | 8 | Monitors a socket for data, typically from ethernet link 9 | 10 | """ 11 | 12 | class EmonHubPacketGenInterfacer(EmonHubInterfacer): 13 | 14 | def __init__(self, name): 15 | """Initialize interfacer 16 | 17 | """ 18 | 19 | # Initialization 20 | super().__init__(name) 21 | 22 | self._control_timestamp = 0 23 | self._control_interval = 5 24 | self._defaults.update({'interval': 5, 'datacode': 'b'}) 25 | self._pg_settings = {'apikey': "", 'url': 'http://localhost/emoncms'} 26 | self._settings.update(self._pg_settings) 27 | 28 | def read(self): 29 | """Read data from the PacketGen emonCMS module. 30 | 31 | """ 32 | t = time.time() 33 | 34 | if t - self._control_timestamp <= self._control_interval: 35 | return 36 | 37 | req = self._settings['url'] + \ 38 | "/emoncms/packetgen/getpacket.json?apikey=" 39 | 40 | # logged without apikey added for security 41 | self._log.info("requesting packet: %sE-M-O-N-C-M-S-A-P-I-K-E-Y", req) 42 | 43 | try: 44 | packet = requests.get(req + self._settings['apikey'], timeout=60).json() 45 | except (ValueError, requests.exceptions.RequestException) as ex: 46 | self._log.warning("no packet returned: %s", ex) 47 | return 48 | 49 | raw = "" 50 | values = [] 51 | datacodes = [] 52 | 53 | for v in packet: 54 | raw += str(v['value']) + " " 55 | values.append(int(v['value'])) 56 | # PacketGen datatypes are 0, 1 or 2 for bytes, ints & bools 57 | # bools are currently read as bytes 0 & 1 58 | datacodes.append(['B', 'h', 'B'][v['type']]) 59 | 60 | c = new_cargo(rawdata=raw) 61 | 62 | # Extract the Target id if one is expected 63 | if self._settings['targeted']: 64 | #setting = str(setting).capitalize() 65 | c.target = int(values[0]) 66 | values = values[1:] 67 | datacodes = datacodes[1:] 68 | 69 | c.realdata = values 70 | c.realdatacodes = datacodes 71 | 72 | self._control_timestamp = t 73 | c.timestamp = t 74 | 75 | # Return a Payload object 76 | #x = new_cargo(realdata=data) 77 | #x.realdatacodes = datacodes 78 | return c 79 | 80 | 81 | def action(self): 82 | """Actions that need to be done on a regular basis. 83 | 84 | This should be called in main loop by instantiater. 85 | 86 | """ 87 | 88 | t = time.time() 89 | 90 | # Keep in touch with PacketGen and update refresh time 91 | interval = int(self._settings['interval']) 92 | if interval: # A value of 0 means don't do anything 93 | if t - self._interval_timestamp < interval: 94 | return 95 | 96 | try: 97 | z = requests.get(self._settings['url'] + 98 | "/emoncms/packetgen/getinterval.json?apikey=" 99 | + self._settings['apikey'], timeout=60).text 100 | i = int(z[1:-1]) 101 | except: 102 | self._log.info("request interval not returned") 103 | return 104 | 105 | if self._control_interval != i: 106 | self._control_interval = i 107 | self._log.info("request interval set to: %d seconds", i) 108 | 109 | self._interval_timestamp = t 110 | 111 | def set(self, **kwargs): 112 | """ 113 | 114 | """ 115 | 116 | for key, setting in self._pg_settings.items(): 117 | # Decide which setting value to use 118 | if key in kwargs: 119 | setting = kwargs[key] 120 | else: 121 | setting = self._pg_settings[key] 122 | if key in self._settings and self._settings[key] == setting: 123 | continue 124 | elif key == 'apikey': 125 | if setting.lower().startswith('xxxx'): # FIXME compare whole string to 'x'*32? 126 | self._log.warning("Setting %s apikey: obscured", self.name) 127 | elif len(setting) == 32: 128 | self._log.info("Setting %s apikey: set", self.name) 129 | elif setting == "": 130 | self._log.info("Setting %s apikey: null", self.name) 131 | else: 132 | self._log.warning("Setting %s apikey: invalid format", self.name) 133 | continue 134 | self._settings[key] = setting 135 | # Next line will log apikey if uncommented (privacy ?) 136 | #self._log.debug("%s apikey: %s", self.name, setting) 137 | continue 138 | elif key == 'url' and setting.startswith("http"): 139 | self._log.info("Setting %s url: %s", self.name, setting) 140 | self._settings[key] = setting 141 | continue 142 | else: 143 | self._log.warning("'%s' is not valid for %s: %s", setting, self.name, key) 144 | 145 | # include kwargs from parent 146 | super().set(**kwargs) 147 | -------------------------------------------------------------------------------- /src/smalibrary/SMABluetoothPacket.py: -------------------------------------------------------------------------------- 1 | # GNU GENERAL PUBLIC LICENSE - Version 2, June 1991 2 | # See LICENCE and README file for details 3 | 4 | __author__ = 'Stuart Pittaway' 5 | 6 | class SMABluetoothPacket: 7 | def __str__(self): 8 | return "I am an instance of SMABluetoothPacket" 9 | 10 | def getLevel2Checksum(self): 11 | return (self.UnescapedArray[-2] << 8) + self.UnescapedArray[-3] 12 | 13 | def lastByte(self): 14 | return self.UnescapedArray[-1] 15 | 16 | def getLevel2Payload(self): 17 | skipendbytes = None 18 | startbyte = 0 19 | 20 | if self.UnescapedArray[0] == 0x7e: 21 | startbyte = 1 22 | 23 | if self.lastByte() == 0x7e: 24 | skipendbytes = -3 25 | 26 | # FIXME This comment says to skip the first 3 bytes, but the code skips the *last* 3 bytes 27 | # Skip the first 3 bytes, they are the command code 0x0001 and 0x7E start byte 28 | return self.UnescapedArray[startbyte:skipendbytes] 29 | 30 | def pushRawByteArray(self, barray): 31 | # Raw byte array 32 | for bte in barray: 33 | self.pushRawByte(bte) 34 | 35 | def pushRawByte(self, value): 36 | # Accept a byte of ESCAPED data (ie. raw byte from Bluetooth) 37 | self.UnescapedArray.append(value) 38 | self.RawByteArray.append(value) 39 | 40 | def pushUnescapedByteArray(self, barray): 41 | for bte in barray: 42 | self.pushUnescapedByte(bte) 43 | 44 | def pushUnescapedByte(self, value): 45 | # Store the raw byte 46 | self.UnescapedArray.append(value) 47 | 48 | if value == 0x7d or value == 0x7e or value == 0x11 or value == 0x12 or value == 0x13: 49 | self.RawByteArray.append(0x7d) # byte to indicate escape character 50 | self.RawByteArray.append(value ^ 0x20) 51 | else: 52 | self.RawByteArray.append(value) 53 | 54 | def setChecksum(self): 55 | self.header[3] = self.header[0] ^ self.header[1] ^ self.header[2] 56 | 57 | def finish(self): 58 | # Not seen any packets over 256 bytes, so zero second byte (needs to be fixed LOL!) 59 | self.header[1] = len(self.RawByteArray) + self.headerlength 60 | self.header[2] = 0 61 | self.setChecksum() 62 | 63 | # Just in case! 64 | if not self.ValidateHeaderChecksum(): 65 | raise Exception("Invalid header checksum when finishing!") 66 | 67 | def pushEscapedByte(self, value): 68 | previousUnescapedByte = 0 69 | 70 | if len(self.RawByteArray) > 0: 71 | previousUnescapedByte = self.RawByteArray[-1] 72 | 73 | # Store the raw byte as it was received into RawByteArray 74 | self.RawByteArray.append(value) 75 | 76 | # did we receive the escape char in previous byte? 77 | if len(self.RawByteArray) > 0 and previousUnescapedByte == 0x7d: 78 | self.UnescapedArray[-1] = value ^ 0x20 79 | else: 80 | # Unescaped array is same as raw array 81 | self.UnescapedArray.append(value) 82 | 83 | def sendPacket(self, btSocket): 84 | return btSocket.send(bytes(self.header + self.SourceAddress + self.DestinationAddress + self.cmdcode + self.RawByteArray)) 85 | 86 | def containsLevel2Packet(self): 87 | return (len(self.UnescapedArray) >= 5 and 88 | self.UnescapedArray[0] == 0x7e and 89 | self.UnescapedArray[1] == 0xff and 90 | self.UnescapedArray[2] == 0x03 and 91 | self.UnescapedArray[3] == 0x60 and 92 | self.UnescapedArray[4] == 0x65) 93 | 94 | def CommandCode(self): 95 | return self.cmdcode[0] + (self.cmdcode[1] << 8) 96 | 97 | def setCommandCode(self, byteone, bytetwo): 98 | self.cmdcode = bytearray() 99 | self.cmdcode.append(byteone) 100 | self.cmdcode.append(bytetwo) 101 | 102 | def getByte(self, indexfromstartofdatapayload): 103 | return self.UnescapedArray[indexfromstartofdatapayload] 104 | 105 | def pushEscapedByteArray(self, barray): 106 | for bte in barray: 107 | self.pushEscapedByte(bte) 108 | 109 | def TotalUnescapedPacketLength(self): 110 | return len(self.UnescapedArray) + self.headerlength 111 | 112 | def TotalRawPacketLength(self): 113 | return self.header[1] + (self.header[2] << 8) 114 | 115 | def TotalPayloadLength(self): 116 | return self.TotalRawPacketLength() - self.headerlength 117 | 118 | def ValidateHeaderChecksum(self): 119 | # Thanks to 120 | # http://groups.google.com/group/sma-bluetooth/browse_thread/thread/50fe13a7c39bdce0/2caea56cdfb3a68a#2caea56cdfb3a68a 121 | # for this checksum information !! 122 | return (self.header[0] ^ self.header[1] ^ self.header[2] ^ self.header[3]) == 0 123 | 124 | def __init__(self, length1, length2, checksum=0, cmd1=0, cmd2=0, SourceAddress=bytearray(), DestinationAddress=bytearray([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])): 125 | self.headerlength = 18 126 | self.SourceAddress = SourceAddress 127 | self.DestinationAddress = DestinationAddress 128 | self.header = bytearray([0x7e, length1, length2, checksum]) 129 | 130 | # Create our array to hold the payload bytes 131 | self.RawByteArray = bytearray() 132 | self.UnescapedArray = bytearray() 133 | self.setCommandCode(cmd1, cmd2) 134 | 135 | if checksum > 0 and not self.ValidateHeaderChecksum(): 136 | raise Exception("Invalid header checksum!") 137 | -------------------------------------------------------------------------------- /src/interfacers/EmonHubDS18B20Interfacer.py: -------------------------------------------------------------------------------- 1 | import time 2 | import json 3 | import Cargo 4 | import os 5 | import glob 6 | from emonhub_interfacer import EmonHubInterfacer 7 | 8 | """ 9 | [[DS18B20]] 10 | Type = EmonHubDS18B20Interfacer 11 | [[[init_settings]]] 12 | [[[runtimesettings]]] 13 | pubchannels = ToEmonCMS, 14 | read_interval = 10 15 | ids = 28-000008e2db06, 28-000009770529, 28-0000096a49b4 16 | names = ambient, cylb, cylt 17 | """ 18 | 19 | class DS18B20: 20 | def __init__(self): 21 | os.system('modprobe w1-gpio') 22 | os.system('modprobe w1-therm') 23 | self._base_dir = '/sys/bus/w1/devices/' 24 | 25 | def scan(self): 26 | devices = glob.glob(self._base_dir + '28*') 27 | sensors = [] 28 | for device in devices: 29 | sensor = device.replace(self._base_dir, "") 30 | sensors.append(sensor) 31 | return sensors 32 | 33 | def _read_raw(self, sensor): 34 | f = open(self._base_dir + sensor + '/w1_slave', 'r') 35 | lines = f.readlines() 36 | f.close() 37 | return lines 38 | 39 | def tempC(self, sensor): 40 | lines = self._read_raw(sensor) 41 | # retry = 0 42 | if len(lines[0]): 43 | while lines[0].strip()[-3:] != 'YES': 44 | # time.sleep(0.2) 45 | # lines = self._read_raw(sensor) 46 | # retry += 1 47 | # if retry==3: return False 48 | return False 49 | 50 | equals_pos = lines[1].find('t=') 51 | if equals_pos != -1: 52 | temp_string = lines[1][equals_pos+2:] 53 | temp_c = float(temp_string) / 1000.0 54 | return temp_c 55 | 56 | """class EmonHubDS18B20Interfacer 57 | 58 | DS18B20 interfacer for use in development 59 | 60 | """ 61 | 62 | class EmonHubDS18B20Interfacer(EmonHubInterfacer): 63 | 64 | def __init__(self, name): 65 | """Initialize Interfacer 66 | 67 | """ 68 | # Initialization 69 | super(EmonHubDS18B20Interfacer, self).__init__(name) 70 | 71 | # This line will stop the default values printing to logfile at start-up 72 | # self._settings.update(self._defaults) 73 | 74 | # Interfacer specific settings 75 | self._DS18B20_settings = {'read_interval': 10.0, 'nodename':'sensors', 'ids':[], 'names':[]} 76 | 77 | self.ds = DS18B20() 78 | 79 | self.next_interval = True 80 | 81 | 82 | def read(self): 83 | """Read data and process 84 | 85 | Return data as a list: [NodeID, val1, val2] 86 | 87 | """ 88 | 89 | if int(time.time()) % self._settings['read_interval'] == 0: 90 | if self.next_interval: 91 | self.next_interval = False 92 | 93 | c = Cargo.new_cargo() 94 | c.names = [] 95 | c.realdata = [] 96 | c.nodeid = self._settings['nodename'] 97 | 98 | if self.ds: 99 | for sensor in self.ds.scan(): 100 | # Check if user has set a name for given sensor id 101 | name = sensor 102 | try: 103 | index = self._settings['ids'].index(sensor) 104 | if index < len(self._settings['names']): 105 | name = self._settings['names'][index] 106 | except ValueError: 107 | pass 108 | 109 | # Read sensor value 110 | value = self.ds.tempC(sensor) 111 | 112 | # Add sensor to arrays 113 | c.names.append(name) 114 | c.realdata.append(value) 115 | 116 | # Log output 117 | self._log.debug(sensor + ": " + name + " " + str(value)) 118 | 119 | if len(c.realdata) > 0: 120 | return c 121 | 122 | else: 123 | self.next_interval = True 124 | 125 | return False 126 | 127 | 128 | def set(self, **kwargs): 129 | for key, setting in self._DS18B20_settings.items(): 130 | # Decide which setting value to use 131 | if key in kwargs: 132 | setting = kwargs[key] 133 | else: 134 | setting = self._DS18B20_settings[key] 135 | 136 | if key in self._settings and self._settings[key] == setting: 137 | continue 138 | elif key == 'read_interval': 139 | self._log.info("Setting %s read_interval: %s", self.name, setting) 140 | self._settings[key] = float(setting) 141 | continue 142 | elif key == 'nodename': 143 | self._log.info("Setting %s nodename: %s", self.name, setting) 144 | self._settings[key] = str(setting) 145 | continue 146 | elif key == 'ids': 147 | self._log.info("Setting %s ids: %s", self.name, ", ".join(setting)) 148 | self._settings[key] = setting 149 | continue 150 | elif key == 'names': 151 | self._log.info("Setting %s names: %s", self.name, ", ".join(setting)) 152 | self._settings[key] = setting 153 | continue 154 | else: 155 | self._log.warning("'%s' is not valid for %s: %s", setting, self.name, key) 156 | 157 | # include kwargs from parent 158 | super().set(**kwargs) 159 | -------------------------------------------------------------------------------- /src/interfacers/EmonHubInfluxInterfacer.py: -------------------------------------------------------------------------------- 1 | """class EmonHubInfluxInterfacer 2 | """ 3 | import time 4 | import requests 5 | from emonhub_interfacer import EmonHubInterfacer 6 | 7 | class EmonHubInfluxInterfacer(EmonHubInterfacer): 8 | 9 | def __init__(self, name, influx_host='localhost', influx_interval=30, influx_port=8086, influx_user='emoncms', influx_passwd='emoncmspw', influx_db='emoncms'): 10 | # Initialization 11 | super().__init__(name) 12 | 13 | self._defaults.update({'batchsize': 100, 'interval': influx_interval }) 14 | self._settings.update(self._defaults) 15 | 16 | # interfacer specific settings 17 | self._influx_settings = { 18 | 'prefix': 'prefix' 19 | } 20 | self._settings.update(self._influx_settings) 21 | 22 | self.init_settings.update({ 23 | 'influx_host': influx_host, 24 | 'influx_port': influx_port, 25 | 'influx_user': influx_user, 26 | 'influx_passwd': influx_passwd, 27 | 'influx_db': influx_db 28 | }) 29 | 30 | self.lastsent = time.time() 31 | self.lastsentstatus = time.time() 32 | 33 | # set an absolute upper limit for number of items to process per post 34 | self._item_limit = 250 35 | 36 | def add(self, cargo): 37 | """Append data to buffer. 38 | 39 | format: {"emontx":{"power1":100,"power2":200,"power3":300}} 40 | 41 | """ 42 | 43 | nodename = str(cargo.nodeid) 44 | if cargo.nodename: 45 | nodename = cargo.nodename 46 | 47 | f = {} 48 | f['node'] = nodename 49 | f['data'] = {} 50 | 51 | # FIXME replace with zip 52 | for i in range(len(cargo.realdata)): 53 | name = str(i + 1) 54 | if i < len(cargo.names): 55 | name = cargo.names[i] 56 | value = cargo.realdata[i] 57 | f['data'][name] = value 58 | 59 | if cargo.rssi: 60 | f['data']['rssi'] = cargo.rssi 61 | 62 | self.buffer.storeItem(f) 63 | 64 | 65 | def _process_post(self, databuffer): 66 | timestamp = int(time.time_ns()) 67 | 68 | metrics = [] 69 | for frame in databuffer: 70 | nodename = frame['node'] 71 | 72 | for inputname, value in frame['data'].items(): 73 | # path 74 | path = inputname + ',prefix=' + self._settings['prefix'] + ',node=' + nodename 75 | # payload 76 | payload = str(value) 77 | # timestamp 78 | #timestamp = frame['timestamp'] 79 | 80 | metrics.append(path + " value=" + payload + " " + str(timestamp)) 81 | 82 | return self._send_metrics(metrics) 83 | 84 | def _send_metrics(self, metrics=[]): 85 | """ 86 | 87 | :param post_url: 88 | :param post_body: 89 | :return: the received reply if request is successful 90 | """ 91 | """Send data to server. 92 | 93 | metrics (list): metric path and values (eg: '["path.node1 val1 time","path.node2 val2 time",...]') 94 | 95 | return True if data sent correctly 96 | 97 | """ 98 | 99 | host = self.init_settings['influx_host'].strip("[']") 100 | port = int(self.init_settings['influx_port'].strip("[']")) 101 | url = "http://" + host + ":" + str(port) + "/write" 102 | self._log.debug("Influx target: %s", url) 103 | message = '\n'.join(metrics) + '\n' 104 | self._log.debug("Influx data: %s", message ) 105 | params = {'db': self.init_settings['influx_db'], 'u': self.init_settings['influx_user'], 'p': self.init_settings['influx_passwd']} 106 | 107 | try: 108 | requests.post(url, params=params, data=message) 109 | except requests.exceptions.RequestException as e: 110 | self._log.error(e) 111 | return False 112 | 113 | return True 114 | 115 | def set(self, **kwargs): 116 | super().set(**kwargs) 117 | for key, setting in self._influx_settings.items(): 118 | # Decide which setting value to use 119 | if key in kwargs.keys(): 120 | setting = kwargs[key] 121 | else: 122 | setting = self._influx_settings[key] 123 | if key in self._settings and self._settings[key] == setting: 124 | continue 125 | else: 126 | self._log.warning("'%s' is not valid for %s: %s", setting, self.name, key) 127 | 128 | """ 129 | def set(self, **kwargs): 130 | super ().set(**kwargs) 131 | for key, setting in self._influx_settings.items(): 132 | #valid = False 133 | if key not in kwargs: 134 | setting = self._influx_settings[key] 135 | else: 136 | setting = kwargs[key] 137 | if key in self._settings and self._settings[key] == setting: 138 | continue 139 | elif key == 'influx_host': 140 | self._log.info("Setting %s influx_host: %s", self.name, setting) 141 | self._settings[key] = setting 142 | continue 143 | elif key == 'influx_port': 144 | self._log.info("Setting %s influx_port: %s", self.name, setting) 145 | self._settings[key] = setting 146 | continue 147 | elif key == 'prefix': 148 | self._log.info("Setting %s prefix: %s", self.name, setting) 149 | self._settings[key] = setting 150 | continue 151 | else: 152 | self._log.warning("'%s' is not valid for %s: %s", setting, self.name, key) 153 | """ 154 | -------------------------------------------------------------------------------- /src/interfacers/EmonHubSocketInterfacer.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import select 3 | from emonhub_interfacer import EmonHubInterfacer 4 | import Cargo 5 | 6 | """class EmonHubSocketInterfacer 7 | 8 | Monitors a socket for data, typically from ethernet link 9 | 10 | """ 11 | 12 | class EmonHubSocketInterfacer(EmonHubInterfacer): 13 | 14 | def __init__(self, name, port_nb=50011): 15 | """Initialize Interfacer 16 | 17 | port_nb (string): port number on which to open the socket 18 | 19 | """ 20 | 21 | # Initialization 22 | super().__init__(name) 23 | 24 | # add an apikey setting 25 | self._skt_settings = {'apikey': ""} 26 | self._settings.update(self._skt_settings) 27 | 28 | # Open socket 29 | self._socket = self._open_socket(int(port_nb)) 30 | 31 | # Initialize RX buffer for socket 32 | self._sock_rx_buf = '' 33 | 34 | def _open_socket(self, port_nb): 35 | """Open a socket 36 | 37 | port_nb (string): port number on which to open the socket 38 | 39 | """ 40 | 41 | self._log.debug('Opening socket on port %d', port_nb) 42 | 43 | try: 44 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 45 | s.bind(('', int(port_nb))) 46 | s.listen(1) 47 | except socket.error as e: 48 | self._log.error(e) 49 | # raise EmonHubInterfacerInitError('Could not open port %s' % port_nb) 50 | else: 51 | return s 52 | 53 | def close(self): 54 | """Close socket.""" 55 | # Close socket 56 | if self._socket is not None: 57 | self._log.debug('Closing socket') 58 | self._socket.close() 59 | 60 | def read(self): 61 | """Read data from socket and process if complete line received. 62 | 63 | Return data as a list: [NodeID, val1, val2] 64 | 65 | """ 66 | 67 | # Check if data received 68 | ready_to_read, ready_to_write, in_error = \ 69 | select.select([self._socket], [], [], 0) 70 | 71 | # If data received, add it to socket RX buffer 72 | if self._socket in ready_to_read: 73 | 74 | # Accept connection 75 | conn, addr = self._socket.accept() 76 | 77 | # Read data 78 | self._sock_rx_buf = self._sock_rx_buf + conn.recv(1024).decode("utf-8") 79 | 80 | # Close connection 81 | conn.close() 82 | 83 | # If there is at least one complete frame in the buffer 84 | if '\r\n' not in self._sock_rx_buf: 85 | return 86 | 87 | # Process and return first frame in buffer: 88 | f, self._sock_rx_buf = self._sock_rx_buf.split('\r\n', 1) 89 | 90 | # create a new cargo 91 | c = Cargo.new_cargo(rawdata=f) 92 | 93 | # Split string into values 94 | f = f.split(' ') 95 | 96 | # If apikey is specified, 32chars and not all x's 97 | if 'apikey' in self._settings: 98 | if len(self._settings['apikey']) == 32 and self._settings['apikey'].lower() != "x" * 32: 99 | # Discard if apikey is not in received frame 100 | if self._settings['apikey'] not in f: 101 | self._log.warning("%d discarded frame: apikey not matched", c.uri) 102 | return 103 | # Otherwise remove apikey from frame 104 | f = [v for v in f if self._settings['apikey'] not in v] 105 | c.rawdata = ' '.join(f) 106 | 107 | # Extract timestamp value if one is expected or use 0 108 | if self._settings['timestamped']: 109 | c.timestamp = f[0] 110 | f = f[1:] 111 | # Extract source's node id 112 | c.nodeid = int(f[0]) + int(self._settings['nodeoffset']) 113 | f = f[1:] 114 | # Extract the Target id if one is expected 115 | if self._settings['targeted']: 116 | c.target = int(f[0]) 117 | f = f[1:] 118 | # Extract list of data values 119 | c.realdata = f 120 | 121 | return c 122 | 123 | def set(self, **kwargs): 124 | """ 125 | 126 | """ 127 | 128 | for key, setting in self._skt_settings.items(): 129 | # Decide which setting value to use 130 | if key in kwargs: 131 | setting = kwargs[key] 132 | else: 133 | setting = self._skt_settings[key] 134 | if key in self._settings and self._settings[key] == setting: 135 | continue 136 | elif key == 'apikey': 137 | if setting.lower().startswith('xxxx'): # FIXME compare whole string to 'x'*32? 138 | self._log.warning("Setting %s apikey: obscured", self.name) 139 | elif len(setting) == 32: 140 | self._log.info("Setting %s apikey: set", self.name) 141 | elif setting == "": 142 | self._log.info("Setting %s apikey: null", self.name) 143 | else: 144 | self._log.warning("Setting %s apikey: invalid format", self.name) 145 | continue 146 | self._settings[key] = setting 147 | # Next line will log apikey if uncommented (privacy ?) 148 | #self._log.debug("%s apikey: %s", self.name, setting) 149 | continue 150 | elif key == 'url' and setting.startswith("http"): 151 | self._log.info("Setting %s url: %s", self.name, setting) 152 | self._settings[key] = setting 153 | continue 154 | else: 155 | self._log.warning("'%s' is not valid for %s: %s", setting, self.name, key) 156 | 157 | # include kwargs from parent 158 | super().set(**kwargs) 159 | -------------------------------------------------------------------------------- /src/interfacers/EmonHubModbusRenogyInterfacer.py: -------------------------------------------------------------------------------- 1 | import time 2 | import datetime 3 | import Cargo 4 | import sys 5 | 6 | try: 7 | from pymodbus.client.sync import ModbusSerialClient as ModbusClient 8 | pymodbus_found = True 9 | except ImportError: 10 | pymodbus_found = False 11 | 12 | from emonhub_interfacer import EmonHubInterfacer 13 | 14 | """class EmonModbusTcpInterfacer 15 | Monitors Renogy Rover via USB RS232 Cable over modbus 16 | """ 17 | 18 | class EmonHubModbusRenogyInterfacer(EmonHubInterfacer): 19 | 20 | def __init__(self, name, com_port='/dev/ttyUSB0', com_baud=9600, toextract='' , poll_interval=30): 21 | """Initialize Interfacer 22 | com_port (string): path to COM port 23 | """ 24 | 25 | # Initialization 26 | super(EmonHubModbusRenogyInterfacer, self).__init__(name) 27 | 28 | self._modcon = False 29 | if not pymodbus_found: 30 | self._log.error("PYMODBUS NOT PRESENT BUT NEEDED !!") 31 | # open connection 32 | if pymodbus_found: 33 | self._log.info("pymodbus installed") 34 | self._log.debug("EmonHubModbusRenogyInterfacer args: " + str(com_port) + " - " + str(com_baud) ) 35 | 36 | self._con = self._open_modbus(com_port,com_baud) 37 | if self._modcon : 38 | self._log.info("Modbus client Connected!") 39 | else: 40 | self._log.info("Connection to Modbus client failed. Will try again later") 41 | 42 | def close(self): 43 | 44 | # Close TCP connection 45 | if self._con is not None: 46 | self._log.debug("Closing USB/Serial port") 47 | self._con.close() 48 | 49 | def _open_modbus(self,com_port,com_baud): 50 | """ Open connection to modbus device """ 51 | BATTERY_TYPE = { 52 | 1: 'open', 53 | 2: 'sealed', 54 | 3: 'gel', 55 | 4: 'lithium', 56 | 5: 'self-customized' 57 | } 58 | 59 | try: 60 | self._modcon = False 61 | self._log.info("Starting Modbus client on " + com_port + " at " + com_baud) 62 | # Connect to the controller and get values 63 | client = ModbusClient(method = 'rtu', port = com_port, baudrate = int(com_baud), stopbits = 1, bytesize = 8, parity = 'N') 64 | client.connect() 65 | 66 | Model = client.read_holding_registers(12, 8, slave=1) 67 | self._log.info("Connected to Renogy Model: " + str(Model.registers[0])) 68 | 69 | BatteryType = client.read_holding_registers(57348, 1, slave=1).registers[0] 70 | BatteryCapacity = client.read_holding_registers(57346, 1, slave=1).registers[0] 71 | self._log.info("Battery Type: " + BATTERY_TYPE[BatteryType] + " " + str(BatteryCapacity) + "ah") 72 | 73 | self._modcon = True 74 | 75 | except Exception as e: 76 | self._log.error("modbus connection failed " + str(e)) 77 | self._log.error("Error on line {}".format(sys.exc_info()[-1].tb_lineno)) 78 | pass 79 | else: 80 | return client 81 | 82 | def read(self): 83 | 84 | CHARGING_STATE = { 85 | 0: 'deactivated', 86 | 1: 'activated', 87 | 2: 'mppt', 88 | 3: 'equalizing', 89 | 4: 'boost', 90 | 5: 'floating', 91 | 6: 'current limiting' 92 | } 93 | 94 | """ Read registers from client""" 95 | if pymodbus_found: 96 | time.sleep(float(self._settings["interval"])) 97 | 98 | if not self._modcon : 99 | self.close() 100 | self._log.info("Not connected, retrying connect" + str(self.init_settings)) 101 | self._con = self._open_modbus(self.init_settings["com_port"], self.init_settings["com_baud"]) 102 | 103 | if self._modcon : 104 | 105 | # read battery registers 106 | BatteryPercent = self._con.read_holding_registers(256, 1, slave=1).registers[0] 107 | Charging_Stage = self._con.read_holding_registers(288, 1, slave=1).registers[0] 108 | self._log.debug("Battery Percent " + str(BatteryPercent) + "%") 109 | self._log.debug("Charging Stage " + str(CHARGING_STATE[Charging_Stage])) 110 | 111 | Temp_raw = self._con.read_holding_registers(259, 2, slave=1) 112 | temp_value = Temp_raw.registers[0] & 0x0ff 113 | sign = Temp_raw.registers[0] >> 7 114 | BatteryTemp_C = -(temp_value - 128) if sign == 1 else temp_value 115 | BatteryTemp_F = (BatteryTemp_C * 9/5) + 32 116 | self._log.debug("BatteryTemp_C " + str(BatteryTemp_C)) 117 | self._log.debug("BatteryTemp_F " +str(BatteryTemp_F)) 118 | 119 | # read Solar registers 120 | SolarVoltage = self._con.read_holding_registers(263, 1, slave=1).registers[0] 121 | SolarCurrent = self._con.read_holding_registers(264, 1, slave=1).registers[0] 122 | SolarPower = self._con.read_holding_registers(265, 1, slave=1).registers[0] 123 | self._log.debug("SolarVoltage " + str(SolarVoltage) + "v") 124 | self._log.debug("SolarCurrent " + str(SolarCurrent) + "a") 125 | self._log.debug("SolarPower " + str(SolarPower) + "w") 126 | 127 | PowerGenToday = self._con.read_holding_registers(275, 1, slave=1).registers[0] 128 | self._log.debug("PowerGenToday " + str(PowerGenToday) + "kWh") 129 | 130 | # Create a Payload object 131 | c = Cargo.new_cargo() 132 | 133 | if int(self._settings['nodeoffset']): 134 | c.nodeid = int(self._settings['nodeoffset']) 135 | c.realdata = [BatteryPercent, Charging_Stage, BatteryTemp_F, SolarVoltage, SolarCurrent, SolarPower, PowerGenToday] 136 | else: 137 | self._log.error("nodeoffset needed in emonhub configuration, make sure it exits and is integer ") 138 | pass 139 | 140 | self._log.debug("Return from read data: " + str(c.realdata)) 141 | return c 142 | 143 | 144 | -------------------------------------------------------------------------------- /src/interfacers/EmonHubRF69Interfacer.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import select 3 | from emonhub_interfacer import EmonHubInterfacer 4 | import Cargo 5 | 6 | """class EmonHubRF69Interfacer 7 | 8 | Monitors a socket for data, typically from ethernet link 9 | 10 | """ 11 | 12 | REG_FIFO = 0x00 13 | REG_OPMODE = 0x01 14 | REG_RSSIVALUE = 0x24 15 | REG_IRQFLAGS1 = 0x27 16 | REG_IRQFLAGS2 = 0x28 17 | REG_SYNCVALUE1 = 0x2F 18 | REG_SYNCVALUE2 = 0x30 19 | MODE_RECEIVE = 4<<2 20 | IRQ1_MODEREADY = 1<<7 21 | IRQ2_PAYLOADREADY = 1<<2 22 | 23 | class EmonHubRF69Interfacer(EmonHubInterfacer): 24 | 25 | def __init__(self, name, nodeid, group): 26 | """Initialize Interfacer 27 | 28 | port_nb (string): port number on which to open the socket 29 | 30 | """ 31 | try: 32 | import spidev 33 | except ModuleNotFoundError as err: 34 | self._log.error(err) 35 | 36 | try: 37 | import RPi.GPIO as GPIO 38 | self.GPIO = GPIO 39 | except ModuleNotFoundError as err: 40 | self._log.error(err) 41 | 42 | # sudo adduser emonhub spi 43 | 44 | # Initialization 45 | super().__init__(name) 46 | 47 | self.myId = int(nodeid) 48 | group = int(group) 49 | 50 | self.parity = group ^ (group << 4) 51 | self.parity = (self.parity ^ (self.parity << 2)) & 0xC0 52 | 53 | self.spi = spidev.SpiDev() 54 | self.spi.open(0,1) 55 | self.spi.max_speed_hz = 4000000 56 | self.spi.no_cs = True 57 | self.sel_pin = 23 58 | 59 | GPIO.setwarnings(False) 60 | GPIO.setmode(GPIO.BCM) 61 | GPIO.setup(self.sel_pin, GPIO.OUT) 62 | 63 | while self.readReg(REG_SYNCVALUE1) != 0xAA: 64 | self.writeReg(REG_SYNCVALUE1, 0xAA) 65 | 66 | while self.readReg(REG_SYNCVALUE1) != 0x55: 67 | self.writeReg(REG_SYNCVALUE1, 0x55) 68 | 69 | CONFIG = { 70 | # POR value is better for first rf_sleep 0x01, 0x00, # OpMode = sleep 71 | 0x01: 0x04, # OpMode = standby 72 | 0x02: 0x00, # DataModul = packet mode, fsk 73 | 0x03: 0x02, # BitRateMsb, data rate = 49,261 bits/s 74 | 0x04: 0x8A, # BitRateLsb, divider = 32 MHz / 650 75 | 0x05: 0x05, # FdevMsb 90 kHz 76 | 0x06: 0xC3, # FdevLsb 90 kHz 77 | 78 | 0x07: 0x6C, # 433 Mhz 79 | 0x08: 0x80, # RegFrfMid 80 | 0x09: 0x00, # RegFrfLsb 81 | 82 | 0x0B: 0x20, # Low M 83 | 0x11: 0x99, # OutputPower = +7 dBm - was default = max = +13 dBm 84 | 0x19: 0x42, # RxBw 125 kHz 85 | #0x1A: 0x42, # AfcBw 125 kHz 86 | 0x1E: 0x2C, # AfcAutoclearOn, AfcAutoOn 87 | #0x25: 0x40, #0x80, # DioMapping1 = SyncAddress (Rx) 88 | 0x26: 0x07, # disable clkout 89 | 0x29: 0xA0, # RssiThresh -80 dB 90 | 0x2D: 0x05, # PreambleSize = 5 91 | 0x2E: 0x88, # SyncConfig = sync on, sync size = 2 92 | 0x2F: 0x2D, # SyncValue1 = 0x2D 93 | 0x37: 0xD0, # PacketConfig1 = variable, white, no filtering 94 | 0x38: 0x42, # PayloadLength = 0, unlimited 95 | 0x3C: 0x8F, # FifoThresh, not empty, level 15 96 | 0x3D: 0x12, # 0x10, # PacketConfig2, interpkt = 1, autorxrestart off 97 | 0x6F: 0x20, # TestDagc ... 98 | 0x71: 0x02 # RegTestAfc 99 | } 100 | 101 | for key, value in CONFIG.items(): 102 | self.writeReg(key, value) 103 | 104 | self.writeReg(REG_SYNCVALUE2, 210); 105 | 106 | self.rxMsg = [] 107 | self.mode = False 108 | 109 | def select(self): 110 | self.GPIO.output(self.sel_pin, self.GPIO.LOW) 111 | 112 | def unselect(self): 113 | self.GPIO.output(self.sel_pin, self.GPIO.HIGH) 114 | 115 | def readReg(self,addr): 116 | self.select() 117 | regval = self.spi.xfer([addr & 0x7F, 0])[1] 118 | self.unselect() 119 | return regval 120 | 121 | def writeReg(self, addr, value): 122 | self.select() 123 | self.spi.xfer([addr | 0x80, value]) 124 | self.unselect() 125 | 126 | def rfm69_setMode (self,newMode): 127 | self.mode = newMode 128 | self.writeReg(REG_OPMODE, (self.readReg(REG_OPMODE) & 0xE3) | newMode) 129 | while (self.readReg(REG_IRQFLAGS1) & IRQ1_MODEREADY) == 0x00: 130 | pass 131 | 132 | def rfm69_receive (self): 133 | if self.mode != MODE_RECEIVE: 134 | self.rfm69_setMode(MODE_RECEIVE) 135 | else: 136 | if self.readReg(REG_IRQFLAGS2) & IRQ2_PAYLOADREADY: 137 | # FIFO access 138 | self.select() 139 | count = self.spi.xfer([REG_FIFO & 0x7F,0])[1] 140 | if count: 141 | self.rxMsg = self.spi.xfer2([0 for i in range(0, count)]) 142 | self.unselect() 143 | # only accept packets intended for us, or broadcasts 144 | # ... or any packet if we're the special catch-all node 145 | self.rssi = self.readReg(REG_RSSIVALUE) 146 | dest = self.rxMsg[0] 147 | if (dest & 0xC0) == self.parity: 148 | destId = dest & 0x3F; 149 | if destId == self.myId or destId == 0 or self.myId == 63: 150 | return count; 151 | 152 | return -1 153 | 154 | 155 | def close(self): 156 | """Close socket.""" 157 | pass 158 | 159 | def read(self): 160 | """Read data from RFM69 and process if complete line received. 161 | 162 | Return data as a list: [NodeID, val1, val2] 163 | 164 | """ 165 | msg_len = self.rfm69_receive() 166 | if msg_len > 1: 167 | # print (msg_len) 168 | # print (self.rxMsg[1:]) 169 | # print (self.rssi) 170 | 171 | c = Cargo.new_cargo(rawdata='') 172 | c.nodeid = self.rxMsg[1] 173 | c.realdata = self.rxMsg[2:] 174 | c.rssi = -0.5*self.rssi 175 | return c 176 | 177 | def set(self, **kwargs): 178 | """ 179 | 180 | """ 181 | # include kwargs from parent 182 | super().set(**kwargs) 183 | --------------------------------------------------------------------------------