Skip to content

Commit b24c57d

Browse files
committed
Use mypy for type checking
1 parent 1738b04 commit b24c57d

File tree

13 files changed

+216
-91
lines changed

13 files changed

+216
-91
lines changed

.github/workflows/test-python-package.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ jobs:
1919

2020
steps:
2121
- uses: actions/checkout@v4
22+
- name: Install jq
23+
run: sudo apt-get update && sudo apt-get install -y jq
2224
- name: Install uv
2325
uses: astral-sh/setup-uv@e92bafb6253dcd438e0484186d7669ea7a8ca1cc # v6.4.3
2426
with:
@@ -27,6 +29,8 @@ jobs:
2729
run: uv sync --locked --all-extras --dev
2830
- name: Run Ruff
2931
run: uvx ruff check . --output-format=github
32+
- name: Run type checks
33+
run: uvx mypy pystiebeleltron --output=json | jq -r '"::error title=Mypy issue,file=\(.file),line=\(.line),col=\(.column)::\(.message)"'
3034
- name: Run tests
3135
run: |
3236
uv run pytest \

pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ dev = [
3232
"pytest-asyncio>=0.26.0",
3333
"pytest-mock>=3.14.0",
3434
"ruff>=0.12.0",
35+
"mypy>=1.9.0",
3536
]
3637

3738
[tool.hatch.build.targets.sdist]
@@ -61,3 +62,7 @@ testpaths = [
6162
"test"
6263
]
6364
asyncio_default_fixture_loop_scope = "function"
65+
66+
[tool.mypy]
67+
python_version = "3.10"
68+
strict = true

pystiebeleltron/__init__.py

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
1+
from __future__ import annotations
2+
13
import asyncio
24
import logging
35
from dataclasses import dataclass
46
from enum import Enum
57

68
from pymodbus.client import AsyncModbusTcpClient
9+
from pymodbus.pdu import ModbusPDU
710

811
__version__ = "0.2.3"
912

10-
_LOGGER: logging.Logger = logging.getLogger(__package__)
11-
13+
_LOGGER = logging.getLogger(__package__)
1214

1315
ENERGY_DATA_BLOCK_NAME = "Energy Data"
1416
VIRTUAL_REGISTER_OFFSET = 100000
@@ -72,11 +74,11 @@ class ModbusRegisterBlock:
7274
base_address: int
7375
count: int
7476
name: str
75-
registers: dict
77+
registers: dict[IsgRegisters, ModbusRegister]
7678
register_type: RegisterType
7779

7880

79-
ENERGY_MANAGEMENT_SETTINGS_REGISTERS = {
81+
ENERGY_MANAGEMENT_SETTINGS_REGISTERS: dict[IsgRegisters, ModbusRegister] = {
8082
EnergyManagementSettingsRegisters.SWITCH_SG_READY_ON_AND_OFF: ModbusRegister(
8183
address=4001, name="SWITCH SG READY ON AND OFF", unit="", min=0.0, max=1.0, data_type=6, key=EnergyManagementSettingsRegisters.SWITCH_SG_READY_ON_AND_OFF
8284
),
@@ -88,7 +90,7 @@ class ModbusRegisterBlock:
8890
),
8991
}
9092

91-
ENERGY_SYSTEM_INFORMATION_REGISTERS = {
93+
ENERGY_SYSTEM_INFORMATION_REGISTERS: dict[IsgRegisters, ModbusRegister] = {
9294
EnergySystemInformationRegisters.SG_READY_OPERATING_STATE: ModbusRegister(
9395
address=5001, name="SG READY OPERATING STATE", unit="", min=1.0, max=4.0, data_type=6, key=EnergySystemInformationRegisters.SG_READY_OPERATING_STATE
9496
),
@@ -125,7 +127,7 @@ class ControllerModel(Enum):
125127
WPMsystem = 449
126128

127129

128-
async def get_controller_model(host, port) -> ControllerModel:
130+
async def get_controller_model(host: str, port: int) -> ControllerModel:
129131
"""Read the model of the controller.
130132
131133
LWA and LWZ controllers have model ids 103 and 104.
@@ -166,9 +168,9 @@ def __init__(
166168
self._client: AsyncModbusTcpClient = AsyncModbusTcpClient(host=host, port=port)
167169
self._lock = asyncio.Lock()
168170
self._register_blocks = register_blocks
169-
self._data = {}
170-
self._previous_data = {}
171-
self._modbus_data = {} # store raw data from modbus for debug purpose
171+
self._data: dict[IsgRegisters, float | int | None] = {}
172+
self._previous_data: dict[IsgRegisters, float | int | None] = {}
173+
self._modbus_data: dict[str, ModbusPDU | None] = {} # store raw data from modbus for debug purpose
172174

173175
async def close(self) -> None:
174176
"""Disconnect client."""
@@ -205,7 +207,7 @@ def get_register_descriptor(self, register: IsgRegisters) -> ModbusRegister | No
205207
return descriptor
206208
return None
207209

208-
def get_register_value(self, register: IsgRegisters) -> float:
210+
def get_register_value(self, register: IsgRegisters) -> float | int | None:
209211
"""Get a value form the registers. The async_udpate needs to be called first."""
210212
return self._data[register]
211213

@@ -222,19 +224,19 @@ async def write_register_value(self, register: IsgRegisters, value: int | float)
222224
else:
223225
raise ValueError("invalid register")
224226

225-
async def read_input_registers(self, device_id, address, count):
227+
async def read_input_registers(self, device_id: int, address: int, count: int) -> ModbusPDU:
226228
"""Read input registers."""
227229
_LOGGER.debug(f"Reading {count} input registers from {address} with device_id {device_id}")
228230
async with self._lock:
229231
return await self._client.read_input_registers(address, count=count, device_id=device_id)
230232

231-
async def read_holding_registers(self, device_id, address, count):
233+
async def read_holding_registers(self, device_id: int, address: int, count: int) -> ModbusPDU:
232234
"""Read holding registers."""
233235
_LOGGER.debug(f"Reading {count} holding registers from {address} with device_id {device_id}")
234236
async with self._lock:
235237
return await self._client.read_holding_registers(address, count=count, device_id=device_id)
236238

237-
def convert_value_from_modbus(self, register, register_description: ModbusRegister) -> float | int | None:
239+
def convert_value_from_modbus(self, register: int, register_description: ModbusRegister) -> float | int | None:
238240
"""Convert a modbus value to a python value."""
239241
if register_description.data_type == 2:
240242
value = self._client.convert_from_registers([register], self._client.DATATYPE.INT16)
@@ -279,9 +281,9 @@ def convert_value_to_modbus(self, value: int | float, register_description: Modb
279281
else:
280282
raise ValueError("invalid register type")
281283

282-
async def async_update(self):
284+
async def async_update(self) -> None:
283285
"""Request current values from heat pump."""
284-
result: dict = {}
286+
result: dict[IsgRegisters, float | int | None] = {}
285287
for registerblock in self._register_blocks:
286288
heat_pump_data = None
287289
if registerblock.register_type == RegisterType.INPUT_REGISTER:

pystiebeleltron/lwz.py

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Modbus api for stiebel eltron heat pumps. This file is generated. Do not modify it manually."""
22

3+
from __future__ import annotations
4+
35
from enum import Enum
46

57
from . import (
@@ -150,7 +152,7 @@ class LwzEnergyDataRegisters(IsgRegisters):
150152
ELEC_BOOSTER_DHW = 3032
151153

152154

153-
LWZ_SYSTEM_VALUES_REGISTERS = {
155+
LWZ_SYSTEM_VALUES_REGISTERS: dict[IsgRegisters, ModbusRegister] = {
154156
LwzSystemValuesRegisters.ACTUAL_ROOM_T_HC1: ModbusRegister(address=1, name="ACTUAL ROOM T HC1", unit="°C", min=-20.0, max=60.0, data_type=2, key=LwzSystemValuesRegisters.ACTUAL_ROOM_T_HC1),
155157
LwzSystemValuesRegisters.SET_ROOM_TEMPERATURE_HC1: ModbusRegister(
156158
address=2, name="SET ROOM TEMPERATURE HC1", unit="°C", min=-20.0, max=60.0, data_type=2, key=LwzSystemValuesRegisters.SET_ROOM_TEMPERATURE_HC1
@@ -211,7 +213,7 @@ class LwzEnergyDataRegisters(IsgRegisters):
211213
),
212214
}
213215

214-
LWZ_SYSTEM_PARAMETERS_REGISTERS = {
216+
LWZ_SYSTEM_PARAMETERS_REGISTERS: dict[IsgRegisters, ModbusRegister] = {
215217
LwzSystemParametersRegisters.OPERATING_MODE: ModbusRegister(address=1001, name="OPERATING MODE", unit="", min=0.0, max=14.0, data_type=8, key=LwzSystemParametersRegisters.OPERATING_MODE),
216218
LwzSystemParametersRegisters.ROOM_TEMPERATURE_DAY_HK1: ModbusRegister(
217219
address=1002, name="ROOM TEMPERATURE DAY", unit="°C", min=10.0, max=30.0, data_type=2, key=LwzSystemParametersRegisters.ROOM_TEMPERATURE_DAY_HK1
@@ -257,15 +259,15 @@ class LwzEnergyDataRegisters(IsgRegisters):
257259
LwzSystemParametersRegisters.RESTART_ISG: ModbusRegister(address=1027, name="RESTART ISG", unit="", min=0.0, max=2.0, data_type=6, key=LwzSystemParametersRegisters.RESTART_ISG),
258260
}
259261

260-
LWZ_SYSTEM_STATE_REGISTERS = {
262+
LWZ_SYSTEM_STATE_REGISTERS: dict[IsgRegisters, ModbusRegister] = {
261263
LwzSystemStateRegisters.OPERATING_STATUS: ModbusRegister(address=2001, name="OPERATING STATUS", unit="", min=0.0, max=65535.0, data_type=6, key=LwzSystemStateRegisters.OPERATING_STATUS),
262264
LwzSystemStateRegisters.FAULT_STATUS: ModbusRegister(address=2002, name="FAULT STATUS", unit="", min=0.0, max=1.0, data_type=6, key=LwzSystemStateRegisters.FAULT_STATUS),
263265
LwzSystemStateRegisters.BUS_STATUS: ModbusRegister(address=2003, name="BUS STATUS", unit="", min=-4.0, max=0.0, data_type=6, key=LwzSystemStateRegisters.BUS_STATUS),
264266
LwzSystemStateRegisters.DEFROST_INITIATED: ModbusRegister(address=2004, name="DEFROST INITIATED", unit="", min=0.0, max=1.0, data_type=6, key=LwzSystemStateRegisters.DEFROST_INITIATED),
265267
LwzSystemStateRegisters.OPERATING_STATUS_2: ModbusRegister(address=2005, name="OPERATING STATUS 2", unit="", min=0.0, max=65535.0, data_type=6, key=LwzSystemStateRegisters.OPERATING_STATUS_2),
266268
}
267269

268-
LWZ_ENERGY_DATA_REGISTERS = {
270+
LWZ_ENERGY_DATA_REGISTERS: dict[IsgRegisters, ModbusRegister] = {
269271
LwzEnergyDataRegisters.HEAT_METER_HTG_DAY: ModbusRegister(address=3001, name="HEAT METER HTG DAY", unit="kWh", min=0.0, max=65535.0, data_type=6, key=LwzEnergyDataRegisters.HEAT_METER_HTG_DAY),
270272
LwzEnergyDataRegisters.HEAT_METER_HTG_TTL_LOW: ModbusRegister(
271273
address=3002, name="HEAT METER HTG TTL", unit="kWh", min=0.0, max=999.0, data_type=6, key=LwzEnergyDataRegisters.HEAT_METER_HTG_TTL_LOW
@@ -364,7 +366,7 @@ def __init__(self, host: str, port: int = 502, device_id: int = 1) -> None:
364366
device_id,
365367
)
366368

367-
async def async_update(self):
369+
async def async_update(self) -> None:
368370
"""Request current values from heat pump."""
369371
await super().async_update()
370372
for registerblock in self._register_blocks:
@@ -402,42 +404,53 @@ async def async_update(self):
402404
else:
403405
self._data[LwzSystemValuesRegisters.COMPRESSOR_STARTS] = compressor_starts_high * 1000 + compressor_starts_low
404406

405-
def get_current_temp(self):
407+
def get_current_temp(self) -> float | None:
406408
"""Get the current room temperature."""
407409
return self.get_register_value(LwzSystemValuesRegisters.ACTUAL_ROOM_T_HC1)
408410

409-
def get_target_temp(self):
411+
def get_target_temp(self) -> float | None:
410412
"""Get the target room temperature."""
411413
return self.get_register_value(LwzSystemParametersRegisters.ROOM_TEMPERATURE_DAY_HK1)
412414

413-
async def set_target_temp(self, temp):
415+
async def set_target_temp(self, temp: float) -> None:
414416
"""Set the target room temperature (day)(HC1)."""
415417
await self.write_register_value(LwzSystemParametersRegisters.ROOM_TEMPERATURE_DAY_HK1, temp)
416418

417-
def get_current_humidity(self):
419+
def get_current_humidity(self) -> float | None:
418420
"""Get the current room humidity."""
419421
return self.get_register_value(LwzSystemValuesRegisters.RELATIVE_HUMIDITY_HC1)
420422

421423
# Handle operation mode
422424

423425
def get_operation(self) -> OperatingMode:
424426
"""Return the current mode of operation."""
425-
op_mode = int(self.get_register_value(LwzSystemParametersRegisters.OPERATING_MODE))
426-
return OperatingMode(op_mode)
427+
op_mode = self.get_register_value(LwzSystemParametersRegisters.OPERATING_MODE)
428+
if op_mode is None:
429+
return OperatingMode.EMERGENCY_OPERATION
430+
return OperatingMode(int(op_mode))
427431

428-
async def set_operation(self, mode: OperatingMode):
432+
async def set_operation(self, mode: OperatingMode) -> None:
429433
"""Set the operation mode."""
430434
await self.write_register_value(LwzSystemParametersRegisters.OPERATING_MODE, mode.value)
431435

432436
def get_heating_status(self) -> bool:
433437
"""Return heater status."""
434-
return bool(int(self.get_register_value(LwzSystemStateRegisters.OPERATING_STATUS)) & (1 << 2))
438+
value = self.get_register_value(LwzSystemStateRegisters.OPERATING_STATUS)
439+
if value is None:
440+
return False
441+
return bool(int(value) & (1 << 2))
435442

436443
def get_cooling_status(self) -> bool:
437444
"""Cooling status."""
438-
return bool(int(self.get_register_value(LwzSystemStateRegisters.OPERATING_STATUS)) & (1 << 3))
445+
value = self.get_register_value(LwzSystemStateRegisters.OPERATING_STATUS)
446+
if value is None:
447+
return False
448+
return bool(int(value) & (1 << 3))
439449

440450
def get_filter_alarm_status(self) -> bool:
441451
"""Return filter alarm."""
452+
value = self.get_register_value(LwzSystemStateRegisters.OPERATING_STATUS)
453+
if value is None:
454+
return False
442455
filter_mask = (1 << 8) | (1 << 12) | (1 << 13)
443-
return bool(int(self.get_register_value(LwzSystemStateRegisters.OPERATING_STATUS)) & filter_mask)
456+
return bool(int(value) & filter_mask)

pystiebeleltron/pystiebeleltron.py

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@
1717
8 | 0 to 255 | 1 | 1 | No | 1 | 5
1818
"""
1919

20+
from __future__ import annotations
21+
2022
from pymodbus.client.mixin import ModbusClientMixin
23+
from pymodbus.pdu import ModbusPDU
2124

2225
# Error - sensor lead is missing or disconnected.
2326
ERROR_NOTAVAILABLE = -60
@@ -156,7 +159,7 @@
156159
class StiebelEltronAPI:
157160
"""Stiebel Eltron API."""
158161

159-
def __init__(self, conn: ModbusClientMixin, device_id=1, update_on_read=False):
162+
def __init__(self, conn: ModbusClientMixin[ModbusPDU], device_id: int = 1, update_on_read: bool = False) -> None:
160163
"""Initialize Stiebel Eltron communication."""
161164
self._conn = conn
162165
self._block_1_input_regs = B1_REGMAP_INPUT
@@ -165,7 +168,7 @@ def __init__(self, conn: ModbusClientMixin, device_id=1, update_on_read=False):
165168
self._device_id = device_id
166169
self._update_on_read = update_on_read
167170

168-
def update(self):
171+
def update(self) -> bool:
169172
"""Request current values from heat pump."""
170173
ret = True
171174
try:
@@ -189,7 +192,7 @@ def update(self):
189192

190193
return ret
191194

192-
def get_conv_val(self, name: str):
195+
def get_conv_val(self, name: str) -> float | int | None:
193196
"""Read and convert value.
194197
195198
Args:
@@ -234,60 +237,72 @@ def get_conv_val(self, name: str):
234237

235238
# Handle room temperature & humidity
236239

237-
def get_current_temp(self):
240+
def get_current_temp(self) -> float | None:
238241
"""Get the current room temperature."""
239242
if self._update_on_read:
240243
self.update()
241244
return self.get_conv_val("ACTUAL_ROOM_TEMPERATURE_HC1")
242245

243-
def get_target_temp(self):
246+
def get_target_temp(self) -> float | None:
244247
"""Get the target room temperature."""
245248
if self._update_on_read:
246249
self.update()
247250
return self.get_conv_val("ROOM_TEMP_HEAT_DAY_HC1")
248251

249-
def set_target_temp(self, temp: float):
252+
def set_target_temp(self, temp: float) -> None:
250253
"""Set the target room temperature (day)(HC1)."""
251254
self._conn.write_register(device_id=self._device_id, address=(self._block_2_holding_regs["ROOM_TEMP_HEAT_DAY_HC1"]["addr"]), value=round(temp * 10.0))
252255

253-
def get_current_humidity(self):
256+
def get_current_humidity(self) -> float | None:
254257
"""Get the current room humidity."""
255258
if self._update_on_read:
256259
self.update()
257260
return self.get_conv_val("RELATIVE_HUMIDITY_HC1")
258261

259262
# Handle operation mode
260263

261-
def get_operation(self):
264+
def get_operation(self) -> str:
262265
"""Return the current mode of operation."""
263266
if self._update_on_read:
264267
self.update()
265268

266269
op_mode = self.get_conv_val("OPERATING_MODE")
267-
return B2_OPERATING_MODE_READ.get(op_mode, "UNKNOWN")
270+
return B2_OPERATING_MODE_READ.get(int(op_mode) if op_mode is not None else -1, "UNKNOWN")
268271

269-
def set_operation(self, mode: str):
272+
def set_operation(self, mode: str) -> None:
270273
"""Set the operation mode."""
271-
self._conn.write_register(device_id=self._device_id, address=(self._block_2_holding_regs["OPERATING_MODE"]["addr"]), value=B2_OPERATING_MODE_WRITE.get(mode))
274+
value = B2_OPERATING_MODE_WRITE.get(mode)
275+
if value is None:
276+
raise ValueError(f"Invalid operation mode: {mode}")
277+
self._conn.write_register(device_id=self._device_id, address=(self._block_2_holding_regs["OPERATING_MODE"]["addr"]), value=value)
272278

273279
# Handle device status
274280

275-
def get_heating_status(self):
281+
def get_heating_status(self) -> bool:
276282
"""Return heater status."""
277283
if self._update_on_read:
278284
self.update()
279-
return bool(self.get_conv_val("OPERATING_STATUS") & B3_OPERATING_STATUS["HEATING"])
285+
val = self.get_conv_val("OPERATING_STATUS")
286+
if val is None:
287+
return False
288+
return bool(int(val) & B3_OPERATING_STATUS["HEATING"])
280289

281-
def get_cooling_status(self):
290+
def get_cooling_status(self) -> bool:
282291
"""Cooling status."""
283292
if self._update_on_read:
284293
self.update()
285-
return bool(self.get_conv_val("OPERATING_STATUS") & B3_OPERATING_STATUS["COOLING"])
294+
val = self.get_conv_val("OPERATING_STATUS")
295+
if val is None:
296+
return False
297+
return bool(int(val) & B3_OPERATING_STATUS["COOLING"])
286298

287-
def get_filter_alarm_status(self):
299+
def get_filter_alarm_status(self) -> bool:
288300
"""Return filter alarm."""
289301
if self._update_on_read:
290302
self.update()
291303

292304
filter_mask = B3_OPERATING_STATUS["FILTER"] | B3_OPERATING_STATUS["FILTER_EXTRACT_AIR"] | B3_OPERATING_STATUS["FILTER_VENTILATION_AIR"]
293-
return bool(self.get_conv_val("OPERATING_STATUS") & filter_mask)
305+
val = self.get_conv_val("OPERATING_STATUS")
306+
if val is None:
307+
return False
308+
return bool(int(val) & filter_mask)

0 commit comments

Comments
 (0)