|
| 1 | +import device |
| 2 | +import probe |
| 3 | +from register import * |
| 4 | + |
| 5 | +# This Python script adds support for Modbus enabled Shelly devices in dbus-modbus-client version 1.58 and later. |
| 6 | +# |
| 7 | + |
| 8 | +# This is just a helper class to make life a bit easier with the Modbus registers. |
| 9 | +# |
| 10 | +class Reg_shelly(Reg_f32l): |
| 11 | + def __init__(self, base, name=None, scale=1, text=None, write=False, invalid=[], **kwargs): |
| 12 | + super().__init__(base - 30000, name, scale, text, write, invalid, **kwargs) |
| 13 | + |
| 14 | +# Shelly Pro 3EM |
| 15 | +# https://shelly-api-docs.shelly.cloud/gen2/Devices/Gen2/ShellyPro3EM |
| 16 | +# |
| 17 | +# Triphase (defualt) |
| 18 | +# https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/EM/ |
| 19 | +# https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/EMData/ |
| 20 | +# |
| 21 | +# Monophase |
| 22 | +# https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/EM1/ |
| 23 | +# https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/EM1Data/ |
| 24 | +# |
| 25 | +# Modbus |
| 26 | +# https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Modbus |
| 27 | +# |
| 28 | +class Shelly_Pro_Meter(device.CustomName, device.EnergyMeter): |
| 29 | + vendor_id = 'shelly' |
| 30 | + vendor_name = 'Shelly' |
| 31 | + min_timeout = 0.5 |
| 32 | + |
| 33 | + # Shelly uses input registers! |
| 34 | + default_access = 'input' |
| 35 | + |
| 36 | + # Subclasses must set the following three properties! |
| 37 | + # productname |
| 38 | + # productmodel |
| 39 | + # productid |
| 40 | + |
| 41 | + def device_init(self): |
| 42 | + self.info_regs = [ |
| 43 | + Reg_text(0, 6, '/Serial', little=True), |
| 44 | + ] |
| 45 | + |
| 46 | + em_components = self.em_components() |
| 47 | + |
| 48 | + # Monophase |
| 49 | + if em_components < 0: |
| 50 | + self.init_monophase(em_components) |
| 51 | + |
| 52 | + # Triphase (default) |
| 53 | + else: |
| 54 | + self.init_triphase(em_components) |
| 55 | + |
| 56 | + # |
| 57 | + # Initializer for the Shelly Monophase profile. |
| 58 | + # |
| 59 | + def init_monophase(self, em_components): |
| 60 | + em_components = abs(em_components) |
| 61 | + |
| 62 | + if em_components > 3: |
| 63 | + raise Exception('Too many EM components: %d' % em_components) |
| 64 | + |
| 65 | + self.nr_phases = em_components |
| 66 | + |
| 67 | + self.data_regs = [ |
| 68 | + # NOTE: We leave the 'AC Totals' blank as the monophase profile does not |
| 69 | + # provide us the totals. |
| 70 | + ] |
| 71 | + |
| 72 | + for n in range(em_components): |
| 73 | + phase = n + 1 |
| 74 | + |
| 75 | + em_offset = n * 20 |
| 76 | + data_offset = n * 20 |
| 77 | + |
| 78 | + self.data_regs += [ |
| 79 | + Reg_shelly(32003 + em_offset, '/Ac/L%d/Voltage' % phase, 1, '%.1f V'), |
| 80 | + Reg_shelly(32005 + em_offset, '/Ac/L%d/Current' % phase, 1, '%.1f A'), |
| 81 | + Reg_shelly(32310 + data_offset, '/Ac/L%d/Energy/Forward' % phase, 1000, '%.1f kWh'), |
| 82 | + Reg_shelly(32312 + data_offset, '/Ac/L%d/Energy/Reverse' % phase, 1000, '%.1f kWh'), |
| 83 | + Reg_shelly(32007 + em_offset, '/Ac/L%d/Power' % phase, 1, '%.1f W'), |
| 84 | + Reg_shelly(32011 + em_offset, '/Ac/L%d/PowerFactor' % phase, 1, '%.3f'), |
| 85 | + ] |
| 86 | + |
| 87 | + # |
| 88 | + # Initializer for the Shelly Triphase profile (default). |
| 89 | + # |
| 90 | + def init_triphase(self, em_components): |
| 91 | + |
| 92 | + self.nr_phases = 3 |
| 93 | + |
| 94 | + # |
| 95 | + # This is hypothetical but the Shelly documention does mention the possibility of |
| 96 | + # more than one EM component in the Triphase profile. There is noting useful we |
| 97 | + # can do with these components other than letting the user pick the one they want |
| 98 | + # to show/use in the GX land. |
| 99 | + # |
| 100 | + component = 0 |
| 101 | + if component >= em_components: |
| 102 | + raise Exception('EM component out of range: %d >= %d' % (component, em_components)) |
| 103 | + |
| 104 | + em_offset = component * 80 |
| 105 | + data_offset = component * 70 |
| 106 | + |
| 107 | + self.data_regs = [ |
| 108 | + Reg_shelly(31162 + data_offset, '/Ac/Energy/Forward', 1000, '%.1f kWh'), |
| 109 | + Reg_shelly(31164 + data_offset, '/Ac/Energy/Reverse', 1000, '%.1f kWh'), |
| 110 | + Reg_shelly(31013 + em_offset, '/Ac/Power', 1, '%.1f W'), |
| 111 | + ] |
| 112 | + |
| 113 | + for n in range(self.nr_phases): |
| 114 | + phase = n + 1 |
| 115 | + |
| 116 | + phase_em_offset = em_offset + (n * 20) |
| 117 | + phase_data_offset = data_offset + (n * 20) |
| 118 | + |
| 119 | + self.data_regs += [ |
| 120 | + Reg_shelly(31020 + phase_em_offset, '/Ac/L%d/Voltage' % phase, 1, '%.1f V'), |
| 121 | + Reg_shelly(31022 + phase_em_offset, '/Ac/L%d/Current' % phase, 1, '%.1f A'), |
| 122 | + Reg_shelly(31182 + phase_data_offset, '/Ac/L%d/Energy/Forward' % phase, 1000, '%.1f kWh'), |
| 123 | + Reg_shelly(31184 + phase_data_offset, '/Ac/L%d/Energy/Reverse' % phase, 1000, '%.1f kWh'), |
| 124 | + Reg_shelly(31024 + phase_em_offset, '/Ac/L%d/Power' % phase, 1, '%.1f W'), |
| 125 | + Reg_shelly(31028 + phase_em_offset, '/Ac/L%d/PowerFactor' % phase, 1, '%.3f'), |
| 126 | + ] |
| 127 | + |
| 128 | + # |
| 129 | + # Attepts to read the given register and returns True upon success. |
| 130 | + # |
| 131 | + def check_register(self, reg): |
| 132 | + try: |
| 133 | + self.read_register(Reg_shelly(reg)) |
| 134 | + return True |
| 135 | + except Exception as err: |
| 136 | + self.log.info("No such Modbus register: %s", reg) |
| 137 | + return False |
| 138 | + |
| 139 | + # |
| 140 | + # Returns the number of EM components. Negative numbers imply the Shelly device |
| 141 | + # is operating in the Monophase profile. Postive numbers imply the device is |
| 142 | + # operating in the Triphase profile. |
| 143 | + # |
| 144 | + def em_components(self): |
| 145 | + # Monophase EM1:2 |
| 146 | + if self.check_register(32040): |
| 147 | + return -3 |
| 148 | + |
| 149 | + # Monophase EM1:1 |
| 150 | + elif self.check_register(32020): |
| 151 | + return -2 |
| 152 | + |
| 153 | + # Monophase EM1:0 |
| 154 | + elif self.check_register(32000): |
| 155 | + return -1 |
| 156 | + |
| 157 | + # Triphase EM1 |
| 158 | + elif self.check_register(31080): |
| 159 | + return 2 |
| 160 | + |
| 161 | + # Triphase EM0 |
| 162 | + elif self.check_register(31000): |
| 163 | + return 1 |
| 164 | + |
| 165 | + else: |
| 166 | + raise Exception("Unable to determine the number of EM components.") |
| 167 | + |
| 168 | + |
| 169 | + |
| 170 | +# Shelly Pro 3EM |
| 171 | +# https://www.shelly.com/products/shelly-pro-3em-x1 |
| 172 | +# |
| 173 | +class Shelly_Pro_3EM(Shelly_Pro_Meter): |
| 174 | + productname = 'Shelly Pro 3EM' |
| 175 | + productmodel = 'SPEM-003CEBEU' |
| 176 | + # P120 |
| 177 | + productid = 0x50313230 |
| 178 | + |
| 179 | +# Shelly Pro EM-50 |
| 180 | +# https://www.shelly.com/products/shelly-pro-em-50 |
| 181 | +# |
| 182 | +class Shelly_Pro_EM50(Shelly_Pro_Meter): |
| 183 | + productname = 'Shelly Pro EM-50' |
| 184 | + productmodel = 'SPEM-002CEBEU50' |
| 185 | + # P050 |
| 186 | + productid = 0x50303530 |
| 187 | + |
| 188 | +# Shelly Pro 3EM 3CT63 |
| 189 | +# https://www.shelly.com/products/shelly-pro-3em-3ct63 |
| 190 | +# |
| 191 | +class Shelly_Pro_3EM_3CT63(Shelly_Pro_Meter): |
| 192 | + productname = 'Shelly Pro 3EM 3CT63' |
| 193 | + productmodel = 'SPEM-003CEBEU63' |
| 194 | + # P063 |
| 195 | + productid = 0x50303633 |
| 196 | + |
| 197 | +# Shelly 3EM-63T or EM-63W Gen3 |
| 198 | +# https://www.shelly.com/products/shelly-3em-63t-gen3 |
| 199 | +# https://www.shelly.com/products/shelly-3em-63w-gen3 |
| 200 | +# |
| 201 | +class Shelly_Pro_3EM_63_Gen3(Shelly_Pro_Meter): |
| 202 | + productname = 'Shelly 3EM-63 Gen3' |
| 203 | + productmodel = 'S3EM-003CXCEU63' |
| 204 | + # P063 |
| 205 | + productid = 0x50303633 |
| 206 | + |
| 207 | +models = { |
| 208 | + Shelly_Pro_3EM.productmodel: { |
| 209 | + 'model': Shelly_Pro_3EM.productmodel, |
| 210 | + 'handler': Shelly_Pro_3EM, |
| 211 | + }, |
| 212 | + Shelly_Pro_EM50.productmodel: { |
| 213 | + 'model': Shelly_Pro_EM50.productmodel, |
| 214 | + 'handler': Shelly_Pro_EM50, |
| 215 | + }, |
| 216 | + Shelly_Pro_3EM_3CT63.productmodel: { |
| 217 | + 'model': Shelly_Pro_3EM_3CT63.productmodel, |
| 218 | + 'handler': Shelly_Pro_3EM_3CT63, |
| 219 | + }, |
| 220 | + Shelly_Pro_3EM_63_Gen3.productmodel: { |
| 221 | + 'model': Shelly_Pro_3EM_63_Gen3.productmodel, |
| 222 | + 'handler': Shelly_Pro_3EM_63_Gen3, |
| 223 | + }, |
| 224 | +} |
| 225 | + |
| 226 | +probe.add_handler(probe.ModelRegister(Reg_text(6, 10, 'model', little=True), models, |
| 227 | + methods=['tcp'], |
| 228 | + units=[1])) |
0 commit comments