diff --git a/dbus-modbus-client.py b/dbus-modbus-client.py index a472811..0834956 100755 --- a/dbus-modbus-client.py +++ b/dbus-modbus-client.py @@ -29,6 +29,7 @@ import abb import comap import victron_em +import shelly import logging log = logging.getLogger() diff --git a/device.py b/device.py index 7fb63f6..8ce5261 100644 --- a/device.py +++ b/device.py @@ -1,6 +1,7 @@ from copy import copy import dbus from functools import partial +from enum import Enum import logging import os import time @@ -44,6 +45,12 @@ def pack_regs(method, regs): return regs + +class RegisterType(Enum): + INPUT = 1 + HOLDING = 2 + + class BaseDevice: min_timeout = 0.1 refresh_time = None @@ -61,6 +68,8 @@ def __init__(self): self.dbus_settings = {} self.info_regs = [] self.data_regs = [] + # Whether to read from holding registers or input registers + self.register_type = RegisterType.HOLDING def destroy(self): if self.dbus: @@ -72,8 +81,12 @@ def destroy(self): self.settings = None def read_register(self, reg): - rr = self.modbus.read_holding_registers(reg.base, reg.count, - unit=self.unit) + if self.register_type == RegisterType.HOLDING: + rr = self.modbus.read_holding_registers(reg.base, reg.count, + unit=self.unit) + else: + rr = self.modbus.read_input_registers(reg.base, reg.count, + unit=self.unit) if rr.isError(): self.log.error('Error reading register %#04x: %s', reg.base, rr) @@ -106,7 +119,10 @@ def read_data_regs(self, regs, d): start = regs[0].base count = regs[-1].base + regs[-1].count - start - rr = self.modbus.read_holding_registers(start, count, unit=self.unit) + if self.register_type == RegisterType.HOLDING: + rr = self.modbus.read_holding_registers(start, count, unit=self.unit) + else: + rr = self.modbus.read_input_registers(start, count, unit=self.unit) latency = time.time() - now diff --git a/shelly.py b/shelly.py new file mode 100644 index 0000000..1c1e72c --- /dev/null +++ b/shelly.py @@ -0,0 +1,64 @@ +import logging + +import device +import probe +from register import * + +log = logging.getLogger(__name__) + + +class ShellyEnergyMeter(device.CustomName, device.EnergyMeter): + # Define mandatory properties so the device gets saved + productid = 0xFFFF + productname = 'Shelly Energy Meter' + + def __init__(self, spec, modbus, model): + super().__init__(spec, modbus, model) + # Increase default timeout + self.min_timeout = 0.5 + # Shelly Modbus devices oddly use input registers for everything + self.register_type = device.RegisterType.INPUT + self.nr_phases = 3 + + def device_init(self): + log.info('Initializing Shelly energy meter using connection "%s"', self.connection()) + + self.info_regs = [ + Reg_text(0, 6, '/Serial', little=True), + Reg_text(6, 10, '/ProductName', little=True), + ] + + self.data_regs = [ + Reg_f32l(1162, '/Ac/Energy/Forward', 1000, '%.1f kWh'), + Reg_f32l(1164, '/Ac/Energy/Reverse', 1000, '%.1f kWh'), + Reg_f32l(1013, '/Ac/Power', 1, '%.1f W'), + Reg_f32l(1182, '/Ac/L1/Energy/Forward', 1000, '%.1f kWh'), + Reg_f32l(1184, '/Ac/L1/Energy/Reverse', 1000, '%.1f kWh'), + Reg_f32l(1020, '/Ac/L1/Voltage', 1, '%.1f V'), + Reg_f32l(1022, '/Ac/L1/Current', 1, '%.1f A'), + Reg_f32l(1024, '/Ac/L1/Power', 1, '%.1f W'), + Reg_f32l(1202, '/Ac/L2/Energy/Forward', 1000, '%.1f kWh'), + Reg_f32l(1204, '/Ac/L2/Energy/Reverse', 1000, '%.1f kWh'), + Reg_f32l(1040, '/Ac/L2/Voltage', 1, '%.1f V'), + Reg_f32l(1042, '/Ac/L2/Current', 1, '%.1f A'), + Reg_f32l(1044, '/Ac/L2/Power', 1, '%.1f W'), + Reg_f32l(1222, '/Ac/L3/Energy/Forward', 1000, '%.1f kWh'), + Reg_f32l(1224, '/Ac/L3/Energy/Reverse', 1000, '%.1f kWh'), + Reg_f32l(1060, '/Ac/L3/Voltage', 1, '%.1f V'), + Reg_f32l(1062, '/Ac/L3/Current', 1, '%.1f A'), + Reg_f32l(1064, '/Ac/L3/Power', 1, '%.1f W'), + ] + + def get_ident(self): + return 'shelly_{}'.format(self.info['/Serial']) + + +class ShellyModelRegister(probe.ModelRegister): + def __init__(self, **args): + super().__init__(None, [], **args) + + def probe(self, spec, modbus, timeout=None): + return ShellyEnergyMeter(spec, modbus, 'SPEM-003CE') + + +probe.add_handler(ShellyModelRegister(methods=['tcp'], units=[1]))