python-qube-heatpump 1.2.2__py3-none-any.whl → 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- python_qube_heatpump/__init__.py +37 -1
- python_qube_heatpump/client.py +282 -4
- python_qube_heatpump/const.py +36 -0
- python_qube_heatpump/entities/__init__.py +16 -0
- python_qube_heatpump/entities/base.py +77 -0
- python_qube_heatpump/entities/binary_sensors.py +269 -0
- python_qube_heatpump/entities/sensors.py +468 -0
- python_qube_heatpump/entities/switches.py +65 -0
- python_qube_heatpump/models.py +41 -23
- {python_qube_heatpump-1.2.2.dist-info → python_qube_heatpump-1.3.0.dist-info}/METADATA +1 -1
- python_qube_heatpump-1.3.0.dist-info/RECORD +13 -0
- python_qube_heatpump-1.2.2.dist-info/RECORD +0 -8
- {python_qube_heatpump-1.2.2.dist-info → python_qube_heatpump-1.3.0.dist-info}/WHEEL +0 -0
- {python_qube_heatpump-1.2.2.dist-info → python_qube_heatpump-1.3.0.dist-info}/licenses/LICENSE +0 -0
python_qube_heatpump/__init__.py
CHANGED
|
@@ -1,3 +1,39 @@
|
|
|
1
|
+
"""Python library for Qube Heat Pump Modbus communication."""
|
|
2
|
+
|
|
1
3
|
from .client import QubeClient
|
|
4
|
+
from .const import (
|
|
5
|
+
DataType,
|
|
6
|
+
ModbusType,
|
|
7
|
+
StatusCode,
|
|
8
|
+
STATUS_CODE_MAP,
|
|
9
|
+
get_status_code,
|
|
10
|
+
)
|
|
11
|
+
from .entities import (
|
|
12
|
+
BINARY_SENSORS,
|
|
13
|
+
EntityDef,
|
|
14
|
+
InputType,
|
|
15
|
+
Platform,
|
|
16
|
+
SENSORS,
|
|
17
|
+
SWITCHES,
|
|
18
|
+
)
|
|
19
|
+
from .models import QubeState
|
|
2
20
|
|
|
3
|
-
__all__ = [
|
|
21
|
+
__all__ = [
|
|
22
|
+
# Client
|
|
23
|
+
"QubeClient",
|
|
24
|
+
# State
|
|
25
|
+
"QubeState",
|
|
26
|
+
# Entity definitions
|
|
27
|
+
"BINARY_SENSORS",
|
|
28
|
+
"EntityDef",
|
|
29
|
+
"InputType",
|
|
30
|
+
"Platform",
|
|
31
|
+
"SENSORS",
|
|
32
|
+
"SWITCHES",
|
|
33
|
+
# Constants
|
|
34
|
+
"DataType",
|
|
35
|
+
"ModbusType",
|
|
36
|
+
"StatusCode",
|
|
37
|
+
"STATUS_CODE_MAP",
|
|
38
|
+
"get_status_code",
|
|
39
|
+
]
|
python_qube_heatpump/client.py
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
"""Client for Qube Heat Pump."""
|
|
2
2
|
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
3
5
|
import logging
|
|
4
6
|
import struct
|
|
5
|
-
from typing import
|
|
7
|
+
from typing import Any
|
|
6
8
|
|
|
7
9
|
from pymodbus.client import AsyncModbusTcpClient
|
|
8
10
|
|
|
9
11
|
from . import const
|
|
12
|
+
from .entities import BINARY_SENSORS, SENSORS, SWITCHES, EntityDef
|
|
13
|
+
from .entities.base import DataType, InputType
|
|
10
14
|
from .models import QubeState
|
|
11
15
|
|
|
12
16
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -46,15 +50,37 @@ class QubeClient:
|
|
|
46
50
|
async def _read(const_def):
|
|
47
51
|
return await self.read_value(const_def)
|
|
48
52
|
|
|
49
|
-
# Fetch
|
|
53
|
+
# Fetch temperature sensors
|
|
50
54
|
state.temp_supply = await _read(const.TEMP_SUPPLY)
|
|
51
55
|
state.temp_return = await _read(const.TEMP_RETURN)
|
|
52
|
-
state.
|
|
56
|
+
state.temp_source_in = await _read(const.TEMP_SOURCE_IN)
|
|
57
|
+
state.temp_source_out = await _read(const.TEMP_SOURCE_OUT)
|
|
58
|
+
state.temp_room = await _read(const.TEMP_ROOM)
|
|
53
59
|
state.temp_dhw = await _read(const.TEMP_DHW)
|
|
60
|
+
state.temp_outside = await _read(const.TEMP_OUTSIDE)
|
|
61
|
+
|
|
62
|
+
# Fetch power and energy sensors
|
|
63
|
+
state.power_thermic = await _read(const.POWER_THERMIC)
|
|
64
|
+
state.power_electric = await _read(const.POWER_ELECTRIC_CALC)
|
|
65
|
+
state.energy_total_electric = await _read(const.ENERGY_ELECTRIC_TOTAL)
|
|
66
|
+
state.energy_total_thermic = await _read(const.ENERGY_THERMIC_TOTAL)
|
|
67
|
+
state.cop_calc = await _read(const.COP_CALC)
|
|
68
|
+
|
|
69
|
+
# Fetch operation sensors
|
|
70
|
+
state.status_code = await _read(const.STATUS_CODE)
|
|
71
|
+
state.compressor_speed = await _read(const.COMPRESSOR_SPEED)
|
|
72
|
+
state.flow_rate = await _read(const.FLOW_RATE)
|
|
73
|
+
|
|
74
|
+
# Fetch setpoints (holding registers)
|
|
75
|
+
state.setpoint_room_heat_day = await _read(const.SETPOINT_HEAT_DAY)
|
|
76
|
+
state.setpoint_room_heat_night = await _read(const.SETPOINT_HEAT_NIGHT)
|
|
77
|
+
state.setpoint_room_cool_day = await _read(const.SETPOINT_COOL_DAY)
|
|
78
|
+
state.setpoint_room_cool_night = await _read(const.SETPOINT_COOL_NIGHT)
|
|
79
|
+
state.setpoint_dhw = await _read(const.USER_DHW_SETPOINT)
|
|
54
80
|
|
|
55
81
|
return state
|
|
56
82
|
|
|
57
|
-
async def read_value(self, definition: tuple) ->
|
|
83
|
+
async def read_value(self, definition: tuple) -> float | None:
|
|
58
84
|
"""Read a single value based on the constant definition."""
|
|
59
85
|
address, reg_type, data_type, scale, offset = definition
|
|
60
86
|
|
|
@@ -134,3 +160,255 @@ class QubeClient:
|
|
|
134
160
|
except Exception as e:
|
|
135
161
|
_LOGGER.error("Exception reading address %s: %s", address, e)
|
|
136
162
|
return None
|
|
163
|
+
|
|
164
|
+
async def read_entity(self, entity: EntityDef) -> Any:
|
|
165
|
+
"""Read a single entity value based on EntityDef.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
entity: The entity definition to read.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
The read value (float, int, or bool depending on entity type).
|
|
172
|
+
"""
|
|
173
|
+
# Determine register count based on data type
|
|
174
|
+
if entity.data_type in (DataType.FLOAT32, DataType.UINT32, DataType.INT32):
|
|
175
|
+
count = 2
|
|
176
|
+
else:
|
|
177
|
+
count = 1
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
# Read based on input type
|
|
181
|
+
if entity.input_type == InputType.COIL:
|
|
182
|
+
result = await self._client.read_coils(
|
|
183
|
+
entity.address, count=1, slave=self.unit
|
|
184
|
+
)
|
|
185
|
+
if result.isError():
|
|
186
|
+
_LOGGER.warning("Error reading coil %s", entity.address)
|
|
187
|
+
return None
|
|
188
|
+
return bool(result.bits[0])
|
|
189
|
+
|
|
190
|
+
if entity.input_type == InputType.DISCRETE_INPUT:
|
|
191
|
+
result = await self._client.read_discrete_inputs(
|
|
192
|
+
entity.address, count=1, slave=self.unit
|
|
193
|
+
)
|
|
194
|
+
if result.isError():
|
|
195
|
+
_LOGGER.warning("Error reading discrete input %s", entity.address)
|
|
196
|
+
return None
|
|
197
|
+
return bool(result.bits[0])
|
|
198
|
+
|
|
199
|
+
if entity.input_type == InputType.INPUT_REGISTER:
|
|
200
|
+
result = await self._client.read_input_registers(
|
|
201
|
+
entity.address, count, slave=self.unit
|
|
202
|
+
)
|
|
203
|
+
else: # HOLDING_REGISTER
|
|
204
|
+
result = await self._client.read_holding_registers(
|
|
205
|
+
entity.address, count, slave=self.unit
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
if result.isError():
|
|
209
|
+
_LOGGER.warning("Error reading address %s", entity.address)
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
regs = result.registers
|
|
213
|
+
val: float | int = 0
|
|
214
|
+
|
|
215
|
+
# Decode based on data type
|
|
216
|
+
if entity.data_type == DataType.FLOAT32:
|
|
217
|
+
int_val = (regs[1] << 16) | regs[0]
|
|
218
|
+
val = struct.unpack(">f", struct.pack(">I", int_val))[0]
|
|
219
|
+
elif entity.data_type == DataType.INT16:
|
|
220
|
+
val = regs[0]
|
|
221
|
+
if val > 32767:
|
|
222
|
+
val -= 65536
|
|
223
|
+
elif entity.data_type == DataType.UINT16:
|
|
224
|
+
val = regs[0]
|
|
225
|
+
elif entity.data_type == DataType.UINT32:
|
|
226
|
+
val = (regs[1] << 16) | regs[0]
|
|
227
|
+
elif entity.data_type == DataType.INT32:
|
|
228
|
+
val = (regs[1] << 16) | regs[0]
|
|
229
|
+
if val > 2147483647:
|
|
230
|
+
val -= 4294967296
|
|
231
|
+
|
|
232
|
+
# Apply scale and offset
|
|
233
|
+
if entity.scale is not None:
|
|
234
|
+
val = val * entity.scale
|
|
235
|
+
if entity.offset is not None:
|
|
236
|
+
val = val + entity.offset
|
|
237
|
+
|
|
238
|
+
return val
|
|
239
|
+
|
|
240
|
+
except Exception as e:
|
|
241
|
+
_LOGGER.error("Exception reading entity %s: %s", entity.key, e)
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
async def read_sensor(self, key: str) -> float | int | None:
|
|
245
|
+
"""Read a sensor value by key.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
key: The sensor key (e.g., 'temp_supply').
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
The sensor value, or None if not found or error.
|
|
252
|
+
"""
|
|
253
|
+
entity = SENSORS.get(key)
|
|
254
|
+
if entity is None:
|
|
255
|
+
_LOGGER.warning("Unknown sensor key: %s", key)
|
|
256
|
+
return None
|
|
257
|
+
return await self.read_entity(entity)
|
|
258
|
+
|
|
259
|
+
async def read_binary_sensor(self, key: str) -> bool | None:
|
|
260
|
+
"""Read a binary sensor value by key.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
key: The binary sensor key (e.g., 'dout_srcpmp_val').
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
The binary sensor value, or None if not found or error.
|
|
267
|
+
"""
|
|
268
|
+
entity = BINARY_SENSORS.get(key)
|
|
269
|
+
if entity is None:
|
|
270
|
+
_LOGGER.warning("Unknown binary sensor key: %s", key)
|
|
271
|
+
return None
|
|
272
|
+
return await self.read_entity(entity)
|
|
273
|
+
|
|
274
|
+
async def read_switch(self, key: str) -> bool | None:
|
|
275
|
+
"""Read a switch state by key.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
key: The switch key (e.g., 'bms_summerwinter').
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
The switch state, or None if not found or error.
|
|
282
|
+
"""
|
|
283
|
+
entity = SWITCHES.get(key)
|
|
284
|
+
if entity is None:
|
|
285
|
+
_LOGGER.warning("Unknown switch key: %s", key)
|
|
286
|
+
return None
|
|
287
|
+
return await self.read_entity(entity)
|
|
288
|
+
|
|
289
|
+
async def read_all_sensors(self) -> dict[str, Any]:
|
|
290
|
+
"""Read all sensor values.
|
|
291
|
+
|
|
292
|
+
Returns:
|
|
293
|
+
Dictionary mapping sensor keys to their values.
|
|
294
|
+
"""
|
|
295
|
+
result: dict[str, Any] = {}
|
|
296
|
+
for key, entity in SENSORS.items():
|
|
297
|
+
result[key] = await self.read_entity(entity)
|
|
298
|
+
return result
|
|
299
|
+
|
|
300
|
+
async def read_all_binary_sensors(self) -> dict[str, bool | None]:
|
|
301
|
+
"""Read all binary sensor values.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Dictionary mapping binary sensor keys to their values.
|
|
305
|
+
"""
|
|
306
|
+
result: dict[str, bool | None] = {}
|
|
307
|
+
for key, entity in BINARY_SENSORS.items():
|
|
308
|
+
result[key] = await self.read_entity(entity)
|
|
309
|
+
return result
|
|
310
|
+
|
|
311
|
+
async def read_all_switches(self) -> dict[str, bool | None]:
|
|
312
|
+
"""Read all switch states.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Dictionary mapping switch keys to their states.
|
|
316
|
+
"""
|
|
317
|
+
result: dict[str, bool | None] = {}
|
|
318
|
+
for key, entity in SWITCHES.items():
|
|
319
|
+
result[key] = await self.read_entity(entity)
|
|
320
|
+
return result
|
|
321
|
+
|
|
322
|
+
async def write_switch(self, key: str, value: bool) -> bool:
|
|
323
|
+
"""Write a switch state by key.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
key: The switch key (e.g., 'bms_summerwinter').
|
|
327
|
+
value: True to turn on, False to turn off.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
True if write succeeded, False otherwise.
|
|
331
|
+
"""
|
|
332
|
+
entity = SWITCHES.get(key)
|
|
333
|
+
if entity is None:
|
|
334
|
+
_LOGGER.warning("Unknown switch key: %s", key)
|
|
335
|
+
return False
|
|
336
|
+
|
|
337
|
+
if not entity.writable:
|
|
338
|
+
_LOGGER.warning("Switch %s is not writable", key)
|
|
339
|
+
return False
|
|
340
|
+
|
|
341
|
+
try:
|
|
342
|
+
result = await self._client.write_coil(
|
|
343
|
+
entity.address, value, slave=self.unit
|
|
344
|
+
)
|
|
345
|
+
if result.isError():
|
|
346
|
+
_LOGGER.warning("Error writing switch %s", key)
|
|
347
|
+
return False
|
|
348
|
+
return True
|
|
349
|
+
except Exception as e:
|
|
350
|
+
_LOGGER.error("Exception writing switch %s: %s", key, e)
|
|
351
|
+
return False
|
|
352
|
+
|
|
353
|
+
async def write_setpoint(self, key: str, value: float) -> bool:
|
|
354
|
+
"""Write a setpoint value by key.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
key: The sensor key for the setpoint (e.g., 'setpoint_dhw').
|
|
358
|
+
value: The value to write.
|
|
359
|
+
|
|
360
|
+
Returns:
|
|
361
|
+
True if write succeeded, False otherwise.
|
|
362
|
+
"""
|
|
363
|
+
entity = SENSORS.get(key)
|
|
364
|
+
if entity is None:
|
|
365
|
+
_LOGGER.warning("Unknown sensor key: %s", key)
|
|
366
|
+
return False
|
|
367
|
+
|
|
368
|
+
if not entity.writable:
|
|
369
|
+
_LOGGER.warning("Sensor %s is not writable", key)
|
|
370
|
+
return False
|
|
371
|
+
|
|
372
|
+
if entity.input_type != InputType.HOLDING_REGISTER:
|
|
373
|
+
_LOGGER.warning("Sensor %s is not a holding register", key)
|
|
374
|
+
return False
|
|
375
|
+
|
|
376
|
+
try:
|
|
377
|
+
# Reverse scale/offset if needed
|
|
378
|
+
write_value = value
|
|
379
|
+
if entity.offset is not None:
|
|
380
|
+
write_value = write_value - entity.offset
|
|
381
|
+
if entity.scale is not None:
|
|
382
|
+
write_value = write_value / entity.scale
|
|
383
|
+
|
|
384
|
+
# Encode based on data type
|
|
385
|
+
if entity.data_type == DataType.FLOAT32:
|
|
386
|
+
# Pack as big-endian float, then split into two registers
|
|
387
|
+
packed = struct.pack(">f", write_value)
|
|
388
|
+
int_val = struct.unpack(">I", packed)[0]
|
|
389
|
+
regs = [int_val & 0xFFFF, (int_val >> 16) & 0xFFFF]
|
|
390
|
+
result = await self._client.write_registers(
|
|
391
|
+
entity.address, regs, slave=self.unit
|
|
392
|
+
)
|
|
393
|
+
elif entity.data_type == DataType.INT16:
|
|
394
|
+
if write_value < 0:
|
|
395
|
+
write_value = int(write_value) + 65536
|
|
396
|
+
result = await self._client.write_register(
|
|
397
|
+
entity.address, int(write_value), slave=self.unit
|
|
398
|
+
)
|
|
399
|
+
elif entity.data_type == DataType.UINT16:
|
|
400
|
+
result = await self._client.write_register(
|
|
401
|
+
entity.address, int(write_value), slave=self.unit
|
|
402
|
+
)
|
|
403
|
+
else:
|
|
404
|
+
_LOGGER.warning("Unsupported data type for writing: %s", entity.data_type)
|
|
405
|
+
return False
|
|
406
|
+
|
|
407
|
+
if result.isError():
|
|
408
|
+
_LOGGER.warning("Error writing setpoint %s", key)
|
|
409
|
+
return False
|
|
410
|
+
return True
|
|
411
|
+
|
|
412
|
+
except Exception as e:
|
|
413
|
+
_LOGGER.error("Exception writing setpoint %s: %s", key, e)
|
|
414
|
+
return False
|
python_qube_heatpump/const.py
CHANGED
|
@@ -20,6 +20,42 @@ class DataType(str, Enum):
|
|
|
20
20
|
UINT32 = "uint32"
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
class StatusCode(str, Enum):
|
|
24
|
+
"""Heat pump status codes."""
|
|
25
|
+
|
|
26
|
+
STANDBY = "standby"
|
|
27
|
+
ALARM = "alarm"
|
|
28
|
+
KEYBOARD_OFF = "keyboard_off"
|
|
29
|
+
COMPRESSOR_STARTUP = "compressor_startup"
|
|
30
|
+
COMPRESSOR_SHUTDOWN = "compressor_shutdown"
|
|
31
|
+
COOLING = "cooling"
|
|
32
|
+
HEATING = "heating"
|
|
33
|
+
START_FAIL = "start_fail"
|
|
34
|
+
HEATING_DHW = "heating_dhw"
|
|
35
|
+
UNKNOWN = "unknown"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Map numeric status codes to StatusCode enum values
|
|
39
|
+
STATUS_CODE_MAP: dict[int, StatusCode] = {
|
|
40
|
+
1: StatusCode.STANDBY,
|
|
41
|
+
2: StatusCode.ALARM,
|
|
42
|
+
6: StatusCode.KEYBOARD_OFF,
|
|
43
|
+
8: StatusCode.COMPRESSOR_STARTUP,
|
|
44
|
+
9: StatusCode.COMPRESSOR_SHUTDOWN,
|
|
45
|
+
14: StatusCode.STANDBY,
|
|
46
|
+
15: StatusCode.COOLING,
|
|
47
|
+
16: StatusCode.HEATING,
|
|
48
|
+
17: StatusCode.START_FAIL,
|
|
49
|
+
18: StatusCode.STANDBY,
|
|
50
|
+
22: StatusCode.HEATING_DHW,
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_status_code(code: int) -> StatusCode:
|
|
55
|
+
"""Convert numeric status code to StatusCode enum."""
|
|
56
|
+
return STATUS_CODE_MAP.get(code, StatusCode.UNKNOWN)
|
|
57
|
+
|
|
58
|
+
|
|
23
59
|
# Register definitions (Address, Type, Data Type, Scale, Offset)
|
|
24
60
|
# Scale/Offset are None if not used.
|
|
25
61
|
# Format: KEY = (Address, ModbusType, DataType, Scale, Offset)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Entity definitions for Qube Heat Pump."""
|
|
2
|
+
|
|
3
|
+
from .base import DataType, EntityDef, InputType, Platform
|
|
4
|
+
from .binary_sensors import BINARY_SENSORS
|
|
5
|
+
from .sensors import SENSORS
|
|
6
|
+
from .switches import SWITCHES
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"BINARY_SENSORS",
|
|
10
|
+
"DataType",
|
|
11
|
+
"EntityDef",
|
|
12
|
+
"InputType",
|
|
13
|
+
"Platform",
|
|
14
|
+
"SENSORS",
|
|
15
|
+
"SWITCHES",
|
|
16
|
+
]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Base classes and enums for entity definitions."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class InputType(str, Enum):
|
|
8
|
+
"""Modbus input type for reading values."""
|
|
9
|
+
|
|
10
|
+
COIL = "coil"
|
|
11
|
+
DISCRETE_INPUT = "discrete_input"
|
|
12
|
+
INPUT_REGISTER = "input"
|
|
13
|
+
HOLDING_REGISTER = "holding"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DataType(str, Enum):
|
|
17
|
+
"""Data type for register values."""
|
|
18
|
+
|
|
19
|
+
FLOAT32 = "float32"
|
|
20
|
+
INT16 = "int16"
|
|
21
|
+
UINT16 = "uint16"
|
|
22
|
+
INT32 = "int32"
|
|
23
|
+
UINT32 = "uint32"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Platform(str, Enum):
|
|
27
|
+
"""Home Assistant platform type."""
|
|
28
|
+
|
|
29
|
+
SENSOR = "sensor"
|
|
30
|
+
BINARY_SENSOR = "binary_sensor"
|
|
31
|
+
SWITCH = "switch"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class EntityDef:
|
|
36
|
+
"""Definition of a Qube heat pump entity.
|
|
37
|
+
|
|
38
|
+
This dataclass defines the protocol-level properties of an entity.
|
|
39
|
+
Home Assistant-specific metadata (device_class, state_class, etc.)
|
|
40
|
+
should be added by the integration, not here.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
# Identity
|
|
44
|
+
key: str
|
|
45
|
+
"""Unique identifier, e.g., 'temp_supply'."""
|
|
46
|
+
|
|
47
|
+
name: str
|
|
48
|
+
"""Human-readable name, e.g., 'Supply temperature'."""
|
|
49
|
+
|
|
50
|
+
# Modbus specifics
|
|
51
|
+
address: int
|
|
52
|
+
"""Register or coil address."""
|
|
53
|
+
|
|
54
|
+
input_type: InputType
|
|
55
|
+
"""How to read from device (coil, discrete_input, input, holding)."""
|
|
56
|
+
|
|
57
|
+
data_type: DataType | None = None
|
|
58
|
+
"""Data type for registers. None for coils/discrete inputs."""
|
|
59
|
+
|
|
60
|
+
# Platform hint
|
|
61
|
+
platform: Platform = Platform.SENSOR
|
|
62
|
+
"""Which HA platform this entity belongs to."""
|
|
63
|
+
|
|
64
|
+
# Value transformation
|
|
65
|
+
scale: float | None = None
|
|
66
|
+
"""Multiply raw value by this factor."""
|
|
67
|
+
|
|
68
|
+
offset: float | None = None
|
|
69
|
+
"""Add this to the scaled value."""
|
|
70
|
+
|
|
71
|
+
# Unit (protocol-level)
|
|
72
|
+
unit: str | None = None
|
|
73
|
+
"""Unit of measurement, e.g., '°C', 'kWh', 'W'."""
|
|
74
|
+
|
|
75
|
+
# Write capability
|
|
76
|
+
writable: bool = False
|
|
77
|
+
"""Whether this entity can be written to."""
|