Skip to content

Commit 5ed8df4

Browse files
committed
feat: add register specifications for new functions
Signed-off-by: Milad Makdesi <milad@id8-engineering.io>
1 parent 0af06c0 commit 5ed8df4

4 files changed

Lines changed: 1083 additions & 12 deletions

File tree

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ repos:
2727
hooks:
2828
- id: pytest-check
2929
name: pytest-check
30-
entry: pytest
30+
entry: uv run pytest
3131
language: system
3232
pass_filenames: false
3333
always_run: true

src/em511/em511.py

Lines changed: 243 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,91 @@ class Em511:
125125
INPUT_MAX_VALUE_32 = 0x7FFFFFFF
126126
INPUT_MAX_VALUE_16 = 0x7FFF
127127

128+
EM511_REGISTER_RESET_TOT_ENERGY_AND_RUN_HOUR_COUNTER = 0x4003
129+
EM511_REGISTER_RESET_PARTIAL_ENERGY_AND_HOUR_COUNTER = 0x4004
130+
EM511_REGISTER_RESET_DMD_AND_DMD_MAX = 0x4005
131+
EM511_REGISTER_RESET_TO_FACTORY_SETTINGS = 0x4020
132+
EM511_REGISTER_FIRMWARE_AND_REVISION = 0x0302
133+
128134
_register_specs: Final[dict[str, RegisterSpec]] = {
129-
"V": RegisterSpec(address=0x0000, count=2, decimals=1, scale=10),
130-
"A": RegisterSpec(address=0x0002, count=2, decimals=3, scale=1000),
131-
"W": RegisterSpec(address=0x0004, count=2, decimals=1, scale=10),
135+
"V": RegisterSpec(
136+
address=0x0000,
137+
count=2,
138+
decimals=1,
139+
scale=10,
140+
),
141+
"A": RegisterSpec(
142+
address=0x0002,
143+
count=2,
144+
decimals=3,
145+
scale=1000,
146+
),
147+
"A_dmd": RegisterSpec(
148+
address=0x003A,
149+
count=2,
150+
decimals=3,
151+
scale=1000,
152+
),
153+
"A_dmd_peak": RegisterSpec(
154+
address=0x003C,
155+
count=2,
156+
decimals=3,
157+
scale=1000,
158+
),
159+
"W": RegisterSpec(
160+
address=0x0004,
161+
count=2,
162+
decimals=1,
163+
scale=10,
164+
),
165+
"W_dmd": RegisterSpec(
166+
address=0x000A,
167+
count=2,
168+
decimals=1,
169+
scale=10,
170+
),
171+
"W_dmd_peak": RegisterSpec(
172+
address=0x000C,
173+
count=2,
174+
decimals=1,
175+
scale=10,
176+
),
177+
"Hz": RegisterSpec(
178+
address=0x000F,
179+
count=1,
180+
decimals=1,
181+
scale=10,
182+
),
183+
"kwh_tot": RegisterSpec(
184+
address=0x0010,
185+
count=2,
186+
decimals=1,
187+
scale=10,
188+
),
189+
"kwh_partial": RegisterSpec(
190+
address=0x0014,
191+
count=2,
192+
decimals=1,
193+
scale=10,
194+
),
195+
"hour_counter": RegisterSpec(
196+
address=0x002C,
197+
count=2,
198+
decimals=2,
199+
scale=100,
200+
),
201+
"lifetime_counter": RegisterSpec(
202+
address=0x0030,
203+
count=2,
204+
decimals=2,
205+
scale=100,
206+
),
207+
"hour_counter_part": RegisterSpec(
208+
address=0x0036,
209+
count=2,
210+
decimals=2,
211+
scale=100,
212+
),
132213
"password": RegisterSpec(
133214
address=0x1000,
134215
count=1,
@@ -138,7 +219,102 @@ class Em511:
138219
return_type=int,
139220
writable=True,
140221
),
141-
"alarm_status": RegisterSpec(address=0x0307, count=1, return_type=int),
222+
"alarm_status": RegisterSpec(
223+
address=0x0306,
224+
count=1,
225+
range=True,
226+
min=0,
227+
max=1,
228+
return_type=int,
229+
),
230+
"alarm_mode": RegisterSpec(
231+
address=0x1015,
232+
count=1,
233+
range=True,
234+
min=1,
235+
max=6,
236+
return_type=int,
237+
writable=True,
238+
),
239+
"alarm_delay": RegisterSpec(
240+
address=0x101A,
241+
count=1,
242+
range=True,
243+
min=0,
244+
max=3600,
245+
return_type=int,
246+
writable=True,
247+
),
248+
"dmd_integration_time": RegisterSpec(
249+
address=0x1010,
250+
count=2,
251+
range=True,
252+
min=0,
253+
max=6,
254+
return_type=int,
255+
writable=True,
256+
),
257+
"device_id": RegisterSpec(
258+
address=0x2000,
259+
count=1,
260+
range=True,
261+
min=1,
262+
max=247,
263+
return_type=int,
264+
writable=True,
265+
),
266+
"baud_rate": RegisterSpec(
267+
address=0x2001,
268+
count=1,
269+
range=True,
270+
min=1,
271+
max=5,
272+
return_type=int,
273+
writable=True,
274+
),
275+
"parity": RegisterSpec(
276+
address=0x2002,
277+
count=1,
278+
range=True,
279+
min=1,
280+
max=2,
281+
return_type=int,
282+
writable=True,
283+
),
284+
"stop_bit": RegisterSpec(
285+
address=0x2003,
286+
count=1,
287+
range=True,
288+
min=0,
289+
max=1,
290+
return_type=int,
291+
writable=True,
292+
),
293+
"reply_delay": RegisterSpec(
294+
address=0x2004,
295+
count=1,
296+
range=True,
297+
min=0,
298+
max=1000,
299+
return_type=int,
300+
writable=True,
301+
),
302+
"identification_code": RegisterSpec(
303+
address=0x000B,
304+
count=1,
305+
range=True,
306+
min=1792,
307+
max=1795,
308+
return_type=int,
309+
),
310+
"measure_mode": RegisterSpec(
311+
address=0x1103,
312+
count=1,
313+
range=True,
314+
min=0,
315+
max=1,
316+
return_type=int,
317+
),
142318
}
143319

144320
def __init__(self, device_address: int, client: ModbusSerialClient) -> None:
@@ -211,6 +387,10 @@ def _write_register(self, address: int, value: int) -> None:
211387
)
212388
raise ModbusException(msg)
213389

390+
def write_register(self, address: int, value: int) -> None:
391+
"""Public wrapper for `_write_register`."""
392+
self._write_register(address, value)
393+
214394
def _unpack(self, regs: list[int], address: int) -> int:
215395
"""Unpack raw Modbus register data into an integer value.
216396
@@ -244,3 +424,62 @@ def _unpack(self, regs: list[int], address: int) -> int:
244424

245425
msg = f"Unexpected register count: {len(regs)} for address={address}"
246426
raise ValueError(msg)
427+
428+
@property
429+
def firmware_and_revision_code(self) -> str:
430+
"""Read firmware version and revision code.
431+
432+
This property reads a 16-bit Modbus register that holds firmware version
433+
and revision information.
434+
435+
Returns:
436+
str: A string formatted as "<major>.<minor>,<revision>", for example "4.3,67".
437+
438+
Raises:
439+
ValueError: If the raw register value is outside the valid 16-bit range
440+
or cannot be decoded properly.
441+
"""
442+
regs = self._read_input_registers(self.EM511_REGISTER_FIRMWARE_AND_REVISION, self.INT16_REG_COUNT)
443+
value = regs[0]
444+
445+
msb = (value >> 8) & 0xFF
446+
revision = value & 0xFF
447+
minor = msb & 0x0F
448+
major = (msb >> 4) & 0x0F
449+
450+
return f"{major}.{minor}.{revision}"
451+
452+
def reset_tot_energy_and_run_hour_counter(self) -> None:
453+
"""Reset total energy + total run hour counters (excluding lifetime).
454+
455+
Raises:
456+
ModbusException: If failed to write to single register.
457+
"""
458+
self.write_register(self.EM511_REGISTER_RESET_TOT_ENERGY_AND_RUN_HOUR_COUNTER, 1)
459+
460+
def reset_partial_energy_and_hour_counter(self) -> None:
461+
"""Reset partial energy + partial run hour counters.
462+
463+
Raises:
464+
ModbusException: If failed to write to single register.
465+
"""
466+
self.write_register(self.EM511_REGISTER_RESET_PARTIAL_ENERGY_AND_HOUR_COUNTER, 1)
467+
468+
def reset_dmd_and_dmd_max(self) -> None:
469+
"""Reset DMD and DMD max values.
470+
471+
Raises:
472+
ModbusException: If failed to write to single register.
473+
"""
474+
self.write_register(self.EM511_REGISTER_RESET_DMD_AND_DMD_MAX, 1)
475+
476+
def reset_to_factory_settings(self) -> None:
477+
"""Factory Restore (Default settings).
478+
479+
Write 0x0A0A=2570, then within 1s write 0xC1A0=49568 to trigger reset.
480+
481+
Raises:
482+
ModbusException: If failed to write to single register.
483+
"""
484+
self.write_register(self.EM511_REGISTER_RESET_TO_FACTORY_SETTINGS, 0x0A0A)
485+
self.write_register(self.EM511_REGISTER_RESET_TO_FACTORY_SETTINGS, 0xC1A0)

src/em511/em511.pyi

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,45 @@ from decimal import Decimal
33
from pymodbus.client import ModbusSerialClient
44

55
class Em511:
6+
EM511_REGISTER_RESET_TOT_ENERGY_AND_RUN_HOUR_COUNTER: int
7+
EM511_REGISTER_RESET_PARTIAL_ENERGY_AND_HOUR_COUNTER: int
8+
EM511_REGISTER_RESET_DMD_AND_DMD_MAX: int
9+
EM511_REGISTER_RESET_TO_FACTORY_SETTINGS: int
10+
611
def __init__(self, device_address: int, client: ModbusSerialClient) -> None: ...
712
V: Decimal
813
A: Decimal
14+
W: Decimal
15+
W_dmd: Decimal
16+
W_dmd_peak: Decimal
17+
A_dmd: Decimal
18+
A_dmd_peak: Decimal
19+
Hz: Decimal
20+
kwh_tot: Decimal
21+
kwh_partial: Decimal
22+
hour_counter: Decimal
23+
lifetime_counter: Decimal
24+
hour_counter_part: Decimal
925
password: int
26+
alarm_status: int
27+
alarm_mode: int
28+
alarm_delay: int
29+
dmd_integration_time: int
30+
device_id: int
31+
baud_rate: int
32+
parity: int
33+
stop_bit: int
34+
reply_delay: int
35+
identification_code: int
36+
measure_mode: int
1037

1138
def _unpack(self, registers: list[int], address: int) -> int: ...
1239
def _write_register(self, address: int, value: int) -> None: ...
1340
def _read_register(self, register_name: str) -> Decimal | int: ...
1441
def _read_input_registers(self, address: int, count: int) -> list[int]: ...
42+
def write_register(self, address: int, value: int) -> None: ...
43+
def reset_tot_energy_and_run_hour_counter(self) -> None: ...
44+
def reset_partial_energy_and_hour_counter(self) -> None: ...
45+
def reset_dmd_and_dmd_max(self) -> None: ...
46+
def reset_to_factory_settings(self) -> None: ...
47+
def firmware_and_revision_code(self) -> str: ...

0 commit comments

Comments
 (0)