python-qube-heatpump 1.2.3__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 +258 -2
- 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.3.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.3.dist-info/RECORD +0 -8
- {python_qube_heatpump-1.2.3.dist-info → python_qube_heatpump-1.3.0.dist-info}/WHEEL +0 -0
- {python_qube_heatpump-1.2.3.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__)
|
|
@@ -76,7 +80,7 @@ class QubeClient:
|
|
|
76
80
|
|
|
77
81
|
return state
|
|
78
82
|
|
|
79
|
-
async def read_value(self, definition: tuple) ->
|
|
83
|
+
async def read_value(self, definition: tuple) -> float | None:
|
|
80
84
|
"""Read a single value based on the constant definition."""
|
|
81
85
|
address, reg_type, data_type, scale, offset = definition
|
|
82
86
|
|
|
@@ -156,3 +160,255 @@ class QubeClient:
|
|
|
156
160
|
except Exception as e:
|
|
157
161
|
_LOGGER.error("Exception reading address %s: %s", address, e)
|
|
158
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."""
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""Binary sensor entity definitions for Qube Heat Pump."""
|
|
2
|
+
|
|
3
|
+
from .base import EntityDef, InputType, Platform
|
|
4
|
+
|
|
5
|
+
_BINARY_SENSOR_DEFS: tuple[EntityDef, ...] = (
|
|
6
|
+
EntityDef(
|
|
7
|
+
key="dout_srcpmp_val",
|
|
8
|
+
name="Source pump power supply",
|
|
9
|
+
address=0,
|
|
10
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
11
|
+
platform=Platform.BINARY_SENSOR,
|
|
12
|
+
),
|
|
13
|
+
EntityDef(
|
|
14
|
+
key="dout_usrpmp_val",
|
|
15
|
+
name="User pump power supply",
|
|
16
|
+
address=1,
|
|
17
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
18
|
+
platform=Platform.BINARY_SENSOR,
|
|
19
|
+
),
|
|
20
|
+
EntityDef(
|
|
21
|
+
key="dout_fourwayvlv_val",
|
|
22
|
+
name="Four-way valve control",
|
|
23
|
+
address=2,
|
|
24
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
25
|
+
platform=Platform.BINARY_SENSOR,
|
|
26
|
+
),
|
|
27
|
+
EntityDef(
|
|
28
|
+
key="dout_cooling_val",
|
|
29
|
+
name="Cooling active output",
|
|
30
|
+
address=3,
|
|
31
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
32
|
+
platform=Platform.BINARY_SENSOR,
|
|
33
|
+
),
|
|
34
|
+
EntityDef(
|
|
35
|
+
key="dout_threewayvlv_val",
|
|
36
|
+
name="Three-way valve output (CV/DHW)",
|
|
37
|
+
address=4,
|
|
38
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
39
|
+
platform=Platform.BINARY_SENSOR,
|
|
40
|
+
),
|
|
41
|
+
EntityDef(
|
|
42
|
+
key="dout_bufferpmp_val",
|
|
43
|
+
name="Buffer pump output",
|
|
44
|
+
address=5,
|
|
45
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
46
|
+
platform=Platform.BINARY_SENSOR,
|
|
47
|
+
),
|
|
48
|
+
EntityDef(
|
|
49
|
+
key="dout_heaterstep1_val",
|
|
50
|
+
name="Heater 1 output",
|
|
51
|
+
address=6,
|
|
52
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
53
|
+
platform=Platform.BINARY_SENSOR,
|
|
54
|
+
),
|
|
55
|
+
EntityDef(
|
|
56
|
+
key="dout_heaterstep2_val",
|
|
57
|
+
name="Heater 2 output",
|
|
58
|
+
address=7,
|
|
59
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
60
|
+
platform=Platform.BINARY_SENSOR,
|
|
61
|
+
),
|
|
62
|
+
EntityDef(
|
|
63
|
+
key="dout_heaterstep3_val",
|
|
64
|
+
name="Heater 3 output",
|
|
65
|
+
address=8,
|
|
66
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
67
|
+
platform=Platform.BINARY_SENSOR,
|
|
68
|
+
),
|
|
69
|
+
EntityDef(
|
|
70
|
+
key="keybonoff",
|
|
71
|
+
name="Heat pump keypad status",
|
|
72
|
+
address=9,
|
|
73
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
74
|
+
platform=Platform.BINARY_SENSOR,
|
|
75
|
+
),
|
|
76
|
+
EntityDef(
|
|
77
|
+
key="al_maxtime_antileg_active",
|
|
78
|
+
name="Alarm: Max time anti-legionella exceeded",
|
|
79
|
+
address=10,
|
|
80
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
81
|
+
platform=Platform.BINARY_SENSOR,
|
|
82
|
+
),
|
|
83
|
+
EntityDef(
|
|
84
|
+
key="al_maxtime_dhw_active",
|
|
85
|
+
name="Alarm: Max time DHW exceeded",
|
|
86
|
+
address=11,
|
|
87
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
88
|
+
platform=Platform.BINARY_SENSOR,
|
|
89
|
+
),
|
|
90
|
+
EntityDef(
|
|
91
|
+
key="al_dewpoint_active",
|
|
92
|
+
name="Alarm: Dewpoint activated",
|
|
93
|
+
address=12,
|
|
94
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
95
|
+
platform=Platform.BINARY_SENSOR,
|
|
96
|
+
),
|
|
97
|
+
EntityDef(
|
|
98
|
+
key="al_underfloorsafety_active",
|
|
99
|
+
name="Alarm: Supply too hot (CV)",
|
|
100
|
+
address=13,
|
|
101
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
102
|
+
platform=Platform.BINARY_SENSOR,
|
|
103
|
+
),
|
|
104
|
+
EntityDef(
|
|
105
|
+
key="alrm_flw",
|
|
106
|
+
name="Alarm: Flow CV",
|
|
107
|
+
address=15,
|
|
108
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
109
|
+
platform=Platform.BINARY_SENSOR,
|
|
110
|
+
),
|
|
111
|
+
EntityDef(
|
|
112
|
+
key="usralrms",
|
|
113
|
+
name="Alarm: Collective alarm CV",
|
|
114
|
+
address=16,
|
|
115
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
116
|
+
platform=Platform.BINARY_SENSOR,
|
|
117
|
+
),
|
|
118
|
+
EntityDef(
|
|
119
|
+
key="coolingalrms",
|
|
120
|
+
name="Alarm: Collective alarm cooling",
|
|
121
|
+
address=17,
|
|
122
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
123
|
+
platform=Platform.BINARY_SENSOR,
|
|
124
|
+
),
|
|
125
|
+
EntityDef(
|
|
126
|
+
key="heatingalrms",
|
|
127
|
+
name="Alarm: Collective alarm heating",
|
|
128
|
+
address=18,
|
|
129
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
130
|
+
platform=Platform.BINARY_SENSOR,
|
|
131
|
+
),
|
|
132
|
+
EntityDef(
|
|
133
|
+
key="alarmmng_al_workinghour",
|
|
134
|
+
name="Alarm: Working hours",
|
|
135
|
+
address=19,
|
|
136
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
137
|
+
platform=Platform.BINARY_SENSOR,
|
|
138
|
+
),
|
|
139
|
+
EntityDef(
|
|
140
|
+
key="srsalrm",
|
|
141
|
+
name="Alarm: Collective alarm source",
|
|
142
|
+
address=20,
|
|
143
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
144
|
+
platform=Platform.BINARY_SENSOR,
|
|
145
|
+
),
|
|
146
|
+
EntityDef(
|
|
147
|
+
key="glbal",
|
|
148
|
+
name="Global Alarm",
|
|
149
|
+
address=21,
|
|
150
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
151
|
+
platform=Platform.BINARY_SENSOR,
|
|
152
|
+
),
|
|
153
|
+
EntityDef(
|
|
154
|
+
key="alarmmng_al_pwrplus",
|
|
155
|
+
name="Alarm: Collective alarm compressor",
|
|
156
|
+
address=22,
|
|
157
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
158
|
+
platform=Platform.BINARY_SENSOR,
|
|
159
|
+
),
|
|
160
|
+
EntityDef(
|
|
161
|
+
key="roomprb_en",
|
|
162
|
+
name="Room sensor activated",
|
|
163
|
+
address=23,
|
|
164
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
165
|
+
platform=Platform.BINARY_SENSOR,
|
|
166
|
+
),
|
|
167
|
+
EntityDef(
|
|
168
|
+
key="plantdemand",
|
|
169
|
+
name="Plant sensor demand",
|
|
170
|
+
address=25,
|
|
171
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
172
|
+
platform=Platform.BINARY_SENSOR,
|
|
173
|
+
),
|
|
174
|
+
EntityDef(
|
|
175
|
+
key="en_dhwpid",
|
|
176
|
+
name="DHW controller enabled",
|
|
177
|
+
address=26,
|
|
178
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
179
|
+
platform=Platform.BINARY_SENSOR,
|
|
180
|
+
),
|
|
181
|
+
EntityDef(
|
|
182
|
+
key="plantprb_en",
|
|
183
|
+
name="Plant sensor activated",
|
|
184
|
+
address=27,
|
|
185
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
186
|
+
platform=Platform.BINARY_SENSOR,
|
|
187
|
+
),
|
|
188
|
+
EntityDef(
|
|
189
|
+
key="bufferprb_en",
|
|
190
|
+
name="Buffer sensor activated",
|
|
191
|
+
address=28,
|
|
192
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
193
|
+
platform=Platform.BINARY_SENSOR,
|
|
194
|
+
),
|
|
195
|
+
EntityDef(
|
|
196
|
+
key="id_demand",
|
|
197
|
+
name="Digital input demand",
|
|
198
|
+
address=29,
|
|
199
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
200
|
+
platform=Platform.BINARY_SENSOR,
|
|
201
|
+
),
|
|
202
|
+
EntityDef(
|
|
203
|
+
key="id_summerwinter",
|
|
204
|
+
name="Digital input summer mode activated",
|
|
205
|
+
address=30,
|
|
206
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
207
|
+
platform=Platform.BINARY_SENSOR,
|
|
208
|
+
),
|
|
209
|
+
EntityDef(
|
|
210
|
+
key="dewpoint",
|
|
211
|
+
name="Digital input dewpoint activated",
|
|
212
|
+
address=31,
|
|
213
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
214
|
+
platform=Platform.BINARY_SENSOR,
|
|
215
|
+
),
|
|
216
|
+
EntityDef(
|
|
217
|
+
key="boostersecurity",
|
|
218
|
+
name="Digital input booster activated",
|
|
219
|
+
address=32,
|
|
220
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
221
|
+
platform=Platform.BINARY_SENSOR,
|
|
222
|
+
),
|
|
223
|
+
EntityDef(
|
|
224
|
+
key="srcflw",
|
|
225
|
+
name="Digital input source flow activated",
|
|
226
|
+
address=34,
|
|
227
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
228
|
+
platform=Platform.BINARY_SENSOR,
|
|
229
|
+
),
|
|
230
|
+
EntityDef(
|
|
231
|
+
key="daynightmode",
|
|
232
|
+
name="Day/night mode status",
|
|
233
|
+
address=37,
|
|
234
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
235
|
+
platform=Platform.BINARY_SENSOR,
|
|
236
|
+
),
|
|
237
|
+
EntityDef(
|
|
238
|
+
key="thermostatdemand",
|
|
239
|
+
name="Internal thermostat demand",
|
|
240
|
+
address=38,
|
|
241
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
242
|
+
platform=Platform.BINARY_SENSOR,
|
|
243
|
+
),
|
|
244
|
+
EntityDef(
|
|
245
|
+
key="req_antileg_1",
|
|
246
|
+
name="Anti-legionella enabled",
|
|
247
|
+
address=39,
|
|
248
|
+
input_type=InputType.DISCRETE_INPUT,
|
|
249
|
+
platform=Platform.BINARY_SENSOR,
|
|
250
|
+
),
|
|
251
|
+
# Coil-based binary sensors
|
|
252
|
+
EntityDef(
|
|
253
|
+
key="bms_demand",
|
|
254
|
+
name="Heat demand active",
|
|
255
|
+
address=19,
|
|
256
|
+
input_type=InputType.COIL,
|
|
257
|
+
platform=Platform.BINARY_SENSOR,
|
|
258
|
+
),
|
|
259
|
+
EntityDef(
|
|
260
|
+
key="surplus_pv",
|
|
261
|
+
name="PV surplus active",
|
|
262
|
+
address=64,
|
|
263
|
+
input_type=InputType.COIL,
|
|
264
|
+
platform=Platform.BINARY_SENSOR,
|
|
265
|
+
),
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# Export as dict for easy lookup by key
|
|
269
|
+
BINARY_SENSORS: dict[str, EntityDef] = {e.key: e for e in _BINARY_SENSOR_DEFS}
|
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
"""Sensor entity definitions for Qube Heat Pump."""
|
|
2
|
+
|
|
3
|
+
from .base import DataType, EntityDef, InputType, Platform
|
|
4
|
+
|
|
5
|
+
_SENSOR_DEFS: tuple[EntityDef, ...] = (
|
|
6
|
+
# Holding register sensors (configuration/setpoints readable as sensors)
|
|
7
|
+
EntityDef(
|
|
8
|
+
key="thermostat_heatsetp_day",
|
|
9
|
+
name="Room setpoint heating (day)",
|
|
10
|
+
address=27,
|
|
11
|
+
input_type=InputType.HOLDING_REGISTER,
|
|
12
|
+
data_type=DataType.FLOAT32,
|
|
13
|
+
platform=Platform.SENSOR,
|
|
14
|
+
unit="°C",
|
|
15
|
+
),
|
|
16
|
+
EntityDef(
|
|
17
|
+
key="thermostat_heatsetp_night",
|
|
18
|
+
name="Room setpoint heating (night)",
|
|
19
|
+
address=29,
|
|
20
|
+
input_type=InputType.HOLDING_REGISTER,
|
|
21
|
+
data_type=DataType.FLOAT32,
|
|
22
|
+
platform=Platform.SENSOR,
|
|
23
|
+
unit="°C",
|
|
24
|
+
),
|
|
25
|
+
EntityDef(
|
|
26
|
+
key="thermostat_coolsetp_day",
|
|
27
|
+
name="Room setpoint cooling (day)",
|
|
28
|
+
address=31,
|
|
29
|
+
input_type=InputType.HOLDING_REGISTER,
|
|
30
|
+
data_type=DataType.FLOAT32,
|
|
31
|
+
platform=Platform.SENSOR,
|
|
32
|
+
unit="°C",
|
|
33
|
+
),
|
|
34
|
+
EntityDef(
|
|
35
|
+
key="thermostat_coolsetp_night",
|
|
36
|
+
name="Room setpoint cooling (night)",
|
|
37
|
+
address=33,
|
|
38
|
+
input_type=InputType.HOLDING_REGISTER,
|
|
39
|
+
data_type=DataType.FLOAT32,
|
|
40
|
+
platform=Platform.SENSOR,
|
|
41
|
+
unit="°C",
|
|
42
|
+
),
|
|
43
|
+
EntityDef(
|
|
44
|
+
key="tapw_timeprogram_dt_bms",
|
|
45
|
+
name="dT flow temperature DHW",
|
|
46
|
+
address=43,
|
|
47
|
+
input_type=InputType.HOLDING_REGISTER,
|
|
48
|
+
data_type=DataType.INT16,
|
|
49
|
+
platform=Platform.SENSOR,
|
|
50
|
+
unit="°C",
|
|
51
|
+
),
|
|
52
|
+
EntityDef(
|
|
53
|
+
key="tapw_timeprogram_dhws",
|
|
54
|
+
name="Minimum temperature DHW",
|
|
55
|
+
address=44,
|
|
56
|
+
input_type=InputType.HOLDING_REGISTER,
|
|
57
|
+
data_type=DataType.FLOAT32,
|
|
58
|
+
platform=Platform.SENSOR,
|
|
59
|
+
unit="°C",
|
|
60
|
+
),
|
|
61
|
+
EntityDef(
|
|
62
|
+
key="tapw_timeprogram_dhws_prog",
|
|
63
|
+
name="DHW temperature (active program)",
|
|
64
|
+
address=46,
|
|
65
|
+
input_type=InputType.HOLDING_REGISTER,
|
|
66
|
+
data_type=DataType.FLOAT32,
|
|
67
|
+
platform=Platform.SENSOR,
|
|
68
|
+
unit="°C",
|
|
69
|
+
),
|
|
70
|
+
EntityDef(
|
|
71
|
+
key="regulation_buffersetp_min",
|
|
72
|
+
name="Minimum setpoint buffer regulation",
|
|
73
|
+
address=99,
|
|
74
|
+
input_type=InputType.HOLDING_REGISTER,
|
|
75
|
+
data_type=DataType.FLOAT32,
|
|
76
|
+
platform=Platform.SENSOR,
|
|
77
|
+
unit="°C",
|
|
78
|
+
),
|
|
79
|
+
EntityDef(
|
|
80
|
+
key="usr_pid_heatsetp",
|
|
81
|
+
name="Heat setpoint (no curve)",
|
|
82
|
+
address=101,
|
|
83
|
+
input_type=InputType.HOLDING_REGISTER,
|
|
84
|
+
data_type=DataType.FLOAT32,
|
|
85
|
+
platform=Platform.SENSOR,
|
|
86
|
+
unit="°C",
|
|
87
|
+
),
|
|
88
|
+
EntityDef(
|
|
89
|
+
key="usr_pid_coolsetp",
|
|
90
|
+
name="Cooling setpoint (no curve)",
|
|
91
|
+
address=103,
|
|
92
|
+
input_type=InputType.HOLDING_REGISTER,
|
|
93
|
+
data_type=DataType.FLOAT32,
|
|
94
|
+
platform=Platform.SENSOR,
|
|
95
|
+
unit="°C",
|
|
96
|
+
),
|
|
97
|
+
EntityDef(
|
|
98
|
+
key="regulation_buffersetp_max",
|
|
99
|
+
name="Max buffer setpoint (cooling)",
|
|
100
|
+
address=169,
|
|
101
|
+
input_type=InputType.HOLDING_REGISTER,
|
|
102
|
+
data_type=DataType.FLOAT32,
|
|
103
|
+
platform=Platform.SENSOR,
|
|
104
|
+
unit="°C",
|
|
105
|
+
),
|
|
106
|
+
EntityDef(
|
|
107
|
+
key="tapw_timeprogram_dhwsetp_nolinq",
|
|
108
|
+
name="User-defined DHW setpoint",
|
|
109
|
+
address=173,
|
|
110
|
+
input_type=InputType.HOLDING_REGISTER,
|
|
111
|
+
data_type=DataType.FLOAT32,
|
|
112
|
+
platform=Platform.SENSOR,
|
|
113
|
+
unit="°C",
|
|
114
|
+
writable=True, # This setpoint can be written via Modbus
|
|
115
|
+
),
|
|
116
|
+
# Input register sensors (read-only)
|
|
117
|
+
EntityDef(
|
|
118
|
+
key="aout_usrpmp_val",
|
|
119
|
+
name="User (CV) pump control %",
|
|
120
|
+
address=4,
|
|
121
|
+
input_type=InputType.INPUT_REGISTER,
|
|
122
|
+
data_type=DataType.FLOAT32,
|
|
123
|
+
platform=Platform.SENSOR,
|
|
124
|
+
unit="%",
|
|
125
|
+
scale=-1.0,
|
|
126
|
+
offset=100.0,
|
|
127
|
+
),
|
|
128
|
+
EntityDef(
|
|
129
|
+
key="aout_srcpmp_val",
|
|
130
|
+
name="Source pump control %",
|
|
131
|
+
address=6,
|
|
132
|
+
input_type=InputType.INPUT_REGISTER,
|
|
133
|
+
data_type=DataType.FLOAT32,
|
|
134
|
+
platform=Platform.SENSOR,
|
|
135
|
+
unit="%",
|
|
136
|
+
scale=-1.0,
|
|
137
|
+
offset=100.0,
|
|
138
|
+
),
|
|
139
|
+
EntityDef(
|
|
140
|
+
key="aout_srcvalve_val",
|
|
141
|
+
name="Source valve control %",
|
|
142
|
+
address=8,
|
|
143
|
+
input_type=InputType.INPUT_REGISTER,
|
|
144
|
+
data_type=DataType.FLOAT32,
|
|
145
|
+
platform=Platform.SENSOR,
|
|
146
|
+
unit="%",
|
|
147
|
+
),
|
|
148
|
+
EntityDef(
|
|
149
|
+
key="dhw_regreq",
|
|
150
|
+
name="DHW regulation demand %",
|
|
151
|
+
address=14,
|
|
152
|
+
input_type=InputType.INPUT_REGISTER,
|
|
153
|
+
data_type=DataType.FLOAT32,
|
|
154
|
+
platform=Platform.SENSOR,
|
|
155
|
+
unit="%",
|
|
156
|
+
),
|
|
157
|
+
EntityDef(
|
|
158
|
+
key="comppwrreq",
|
|
159
|
+
name="Compressor demand %",
|
|
160
|
+
address=16,
|
|
161
|
+
input_type=InputType.INPUT_REGISTER,
|
|
162
|
+
data_type=DataType.FLOAT32,
|
|
163
|
+
platform=Platform.SENSOR,
|
|
164
|
+
unit="%",
|
|
165
|
+
),
|
|
166
|
+
EntityDef(
|
|
167
|
+
key="flow",
|
|
168
|
+
name="Measured Flow",
|
|
169
|
+
address=18,
|
|
170
|
+
input_type=InputType.INPUT_REGISTER,
|
|
171
|
+
data_type=DataType.FLOAT32,
|
|
172
|
+
platform=Platform.SENSOR,
|
|
173
|
+
unit="L/min",
|
|
174
|
+
),
|
|
175
|
+
# Core temperature sensors (mapped to QubeState typed fields)
|
|
176
|
+
EntityDef(
|
|
177
|
+
key="temp_supply",
|
|
178
|
+
name="Supply temperature CV",
|
|
179
|
+
address=20,
|
|
180
|
+
input_type=InputType.INPUT_REGISTER,
|
|
181
|
+
data_type=DataType.FLOAT32,
|
|
182
|
+
platform=Platform.SENSOR,
|
|
183
|
+
unit="°C",
|
|
184
|
+
),
|
|
185
|
+
EntityDef(
|
|
186
|
+
key="temp_return",
|
|
187
|
+
name="Return temperature CV",
|
|
188
|
+
address=22,
|
|
189
|
+
input_type=InputType.INPUT_REGISTER,
|
|
190
|
+
data_type=DataType.FLOAT32,
|
|
191
|
+
platform=Platform.SENSOR,
|
|
192
|
+
unit="°C",
|
|
193
|
+
),
|
|
194
|
+
EntityDef(
|
|
195
|
+
key="temp_source_in",
|
|
196
|
+
name="Source temperature from roof",
|
|
197
|
+
address=24,
|
|
198
|
+
input_type=InputType.INPUT_REGISTER,
|
|
199
|
+
data_type=DataType.FLOAT32,
|
|
200
|
+
platform=Platform.SENSOR,
|
|
201
|
+
unit="°C",
|
|
202
|
+
),
|
|
203
|
+
EntityDef(
|
|
204
|
+
key="temp_source_out",
|
|
205
|
+
name="Source temperature to roof",
|
|
206
|
+
address=26,
|
|
207
|
+
input_type=InputType.INPUT_REGISTER,
|
|
208
|
+
data_type=DataType.FLOAT32,
|
|
209
|
+
platform=Platform.SENSOR,
|
|
210
|
+
unit="°C",
|
|
211
|
+
),
|
|
212
|
+
EntityDef(
|
|
213
|
+
key="temp_room",
|
|
214
|
+
name="Room temperature",
|
|
215
|
+
address=28,
|
|
216
|
+
input_type=InputType.INPUT_REGISTER,
|
|
217
|
+
data_type=DataType.FLOAT32,
|
|
218
|
+
platform=Platform.SENSOR,
|
|
219
|
+
unit="°C",
|
|
220
|
+
),
|
|
221
|
+
EntityDef(
|
|
222
|
+
key="temp_dhw",
|
|
223
|
+
name="DHW temperature",
|
|
224
|
+
address=30,
|
|
225
|
+
input_type=InputType.INPUT_REGISTER,
|
|
226
|
+
data_type=DataType.FLOAT32,
|
|
227
|
+
platform=Platform.SENSOR,
|
|
228
|
+
unit="°C",
|
|
229
|
+
),
|
|
230
|
+
EntityDef(
|
|
231
|
+
key="temp_outside",
|
|
232
|
+
name="Outside temperature",
|
|
233
|
+
address=32,
|
|
234
|
+
input_type=InputType.INPUT_REGISTER,
|
|
235
|
+
data_type=DataType.FLOAT32,
|
|
236
|
+
platform=Platform.SENSOR,
|
|
237
|
+
unit="°C",
|
|
238
|
+
),
|
|
239
|
+
EntityDef(
|
|
240
|
+
key="cop_calc",
|
|
241
|
+
name="COP (calculated)",
|
|
242
|
+
address=34,
|
|
243
|
+
input_type=InputType.INPUT_REGISTER,
|
|
244
|
+
data_type=DataType.FLOAT32,
|
|
245
|
+
platform=Platform.SENSOR,
|
|
246
|
+
),
|
|
247
|
+
EntityDef(
|
|
248
|
+
key="power_thermic",
|
|
249
|
+
name="Current power",
|
|
250
|
+
address=36,
|
|
251
|
+
input_type=InputType.INPUT_REGISTER,
|
|
252
|
+
data_type=DataType.FLOAT32,
|
|
253
|
+
platform=Platform.SENSOR,
|
|
254
|
+
unit="W",
|
|
255
|
+
),
|
|
256
|
+
EntityDef(
|
|
257
|
+
key="status_code",
|
|
258
|
+
name="Status code",
|
|
259
|
+
address=38,
|
|
260
|
+
input_type=InputType.INPUT_REGISTER,
|
|
261
|
+
data_type=DataType.UINT16,
|
|
262
|
+
platform=Platform.SENSOR,
|
|
263
|
+
),
|
|
264
|
+
EntityDef(
|
|
265
|
+
key="regsetp",
|
|
266
|
+
name="Calculated heat pump setpoint",
|
|
267
|
+
address=39,
|
|
268
|
+
input_type=InputType.INPUT_REGISTER,
|
|
269
|
+
data_type=DataType.FLOAT32,
|
|
270
|
+
platform=Platform.SENSOR,
|
|
271
|
+
unit="°C",
|
|
272
|
+
),
|
|
273
|
+
EntityDef(
|
|
274
|
+
key="coolsetp_1",
|
|
275
|
+
name="Cooling setpoint",
|
|
276
|
+
address=41,
|
|
277
|
+
input_type=InputType.INPUT_REGISTER,
|
|
278
|
+
data_type=DataType.FLOAT32,
|
|
279
|
+
platform=Platform.SENSOR,
|
|
280
|
+
unit="°C",
|
|
281
|
+
),
|
|
282
|
+
EntityDef(
|
|
283
|
+
key="heatsetp_1",
|
|
284
|
+
name="Heating setpoint",
|
|
285
|
+
address=43,
|
|
286
|
+
input_type=InputType.INPUT_REGISTER,
|
|
287
|
+
data_type=DataType.FLOAT32,
|
|
288
|
+
platform=Platform.SENSOR,
|
|
289
|
+
unit="°C",
|
|
290
|
+
),
|
|
291
|
+
EntityDef(
|
|
292
|
+
key="compressor_speed",
|
|
293
|
+
name="Current compressor speed",
|
|
294
|
+
address=45,
|
|
295
|
+
input_type=InputType.INPUT_REGISTER,
|
|
296
|
+
data_type=DataType.FLOAT32,
|
|
297
|
+
platform=Platform.SENSOR,
|
|
298
|
+
unit="rpm",
|
|
299
|
+
scale=60.0,
|
|
300
|
+
),
|
|
301
|
+
EntityDef(
|
|
302
|
+
key="dhw_setp",
|
|
303
|
+
name="DHW calculated setpoint",
|
|
304
|
+
address=47,
|
|
305
|
+
input_type=InputType.INPUT_REGISTER,
|
|
306
|
+
data_type=DataType.FLOAT32,
|
|
307
|
+
platform=Platform.SENSOR,
|
|
308
|
+
unit="°C",
|
|
309
|
+
),
|
|
310
|
+
EntityDef(
|
|
311
|
+
key="workinghours_dhw_hrsret",
|
|
312
|
+
name="Working hours DHW",
|
|
313
|
+
address=50,
|
|
314
|
+
input_type=InputType.INPUT_REGISTER,
|
|
315
|
+
data_type=DataType.INT16,
|
|
316
|
+
platform=Platform.SENSOR,
|
|
317
|
+
unit="h",
|
|
318
|
+
),
|
|
319
|
+
EntityDef(
|
|
320
|
+
key="workinghours_heat_hrsret",
|
|
321
|
+
name="Working hours heating",
|
|
322
|
+
address=52,
|
|
323
|
+
input_type=InputType.INPUT_REGISTER,
|
|
324
|
+
data_type=DataType.INT16,
|
|
325
|
+
platform=Platform.SENSOR,
|
|
326
|
+
unit="h",
|
|
327
|
+
),
|
|
328
|
+
EntityDef(
|
|
329
|
+
key="workinghours_cool_hrsret",
|
|
330
|
+
name="Working hours cooling",
|
|
331
|
+
address=54,
|
|
332
|
+
input_type=InputType.INPUT_REGISTER,
|
|
333
|
+
data_type=DataType.INT16,
|
|
334
|
+
platform=Platform.SENSOR,
|
|
335
|
+
unit="h",
|
|
336
|
+
),
|
|
337
|
+
EntityDef(
|
|
338
|
+
key="workinghours_heater1_hrs",
|
|
339
|
+
name="Working hours heater 1",
|
|
340
|
+
address=56,
|
|
341
|
+
input_type=InputType.INPUT_REGISTER,
|
|
342
|
+
data_type=DataType.INT16,
|
|
343
|
+
platform=Platform.SENSOR,
|
|
344
|
+
unit="h",
|
|
345
|
+
),
|
|
346
|
+
EntityDef(
|
|
347
|
+
key="workinghours_heater2_hrs",
|
|
348
|
+
name="Working hours heater 2",
|
|
349
|
+
address=58,
|
|
350
|
+
input_type=InputType.INPUT_REGISTER,
|
|
351
|
+
data_type=DataType.INT16,
|
|
352
|
+
platform=Platform.SENSOR,
|
|
353
|
+
unit="h",
|
|
354
|
+
),
|
|
355
|
+
EntityDef(
|
|
356
|
+
key="workinghours_heater3_hrs",
|
|
357
|
+
name="Working hours heater 3",
|
|
358
|
+
address=60,
|
|
359
|
+
input_type=InputType.INPUT_REGISTER,
|
|
360
|
+
data_type=DataType.INT16,
|
|
361
|
+
platform=Platform.SENSOR,
|
|
362
|
+
unit="h",
|
|
363
|
+
),
|
|
364
|
+
EntityDef(
|
|
365
|
+
key="power_electric",
|
|
366
|
+
name="Total electric power (calculated)",
|
|
367
|
+
address=61,
|
|
368
|
+
input_type=InputType.INPUT_REGISTER,
|
|
369
|
+
data_type=DataType.FLOAT32,
|
|
370
|
+
platform=Platform.SENSOR,
|
|
371
|
+
unit="W",
|
|
372
|
+
),
|
|
373
|
+
EntityDef(
|
|
374
|
+
key="plantsetp",
|
|
375
|
+
name="Plant regulation setpoint",
|
|
376
|
+
address=65,
|
|
377
|
+
input_type=InputType.INPUT_REGISTER,
|
|
378
|
+
data_type=DataType.FLOAT32,
|
|
379
|
+
platform=Platform.SENSOR,
|
|
380
|
+
unit="°C",
|
|
381
|
+
),
|
|
382
|
+
EntityDef(
|
|
383
|
+
key="energy_total_electric",
|
|
384
|
+
name="Total electric consumption (excl. standby)",
|
|
385
|
+
address=69,
|
|
386
|
+
input_type=InputType.INPUT_REGISTER,
|
|
387
|
+
data_type=DataType.FLOAT32,
|
|
388
|
+
platform=Platform.SENSOR,
|
|
389
|
+
unit="kWh",
|
|
390
|
+
),
|
|
391
|
+
EntityDef(
|
|
392
|
+
key="energy_total_thermic",
|
|
393
|
+
name="Total thermic yield",
|
|
394
|
+
address=71,
|
|
395
|
+
input_type=InputType.INPUT_REGISTER,
|
|
396
|
+
data_type=DataType.FLOAT32,
|
|
397
|
+
platform=Platform.SENSOR,
|
|
398
|
+
unit="kWh",
|
|
399
|
+
),
|
|
400
|
+
EntityDef(
|
|
401
|
+
key="modbus_roomtemp",
|
|
402
|
+
name="Linq room temperature",
|
|
403
|
+
address=75,
|
|
404
|
+
input_type=InputType.INPUT_REGISTER,
|
|
405
|
+
data_type=DataType.FLOAT32,
|
|
406
|
+
platform=Platform.SENSOR,
|
|
407
|
+
unit="°C",
|
|
408
|
+
),
|
|
409
|
+
# Aliases for QubeState compatibility (same addresses as core sensors)
|
|
410
|
+
EntityDef(
|
|
411
|
+
key="flow_rate",
|
|
412
|
+
name="Flow rate",
|
|
413
|
+
address=18,
|
|
414
|
+
input_type=InputType.INPUT_REGISTER,
|
|
415
|
+
data_type=DataType.FLOAT32,
|
|
416
|
+
platform=Platform.SENSOR,
|
|
417
|
+
unit="L/min",
|
|
418
|
+
),
|
|
419
|
+
EntityDef(
|
|
420
|
+
key="setpoint_room_heat_day",
|
|
421
|
+
name="Room setpoint heating (day)",
|
|
422
|
+
address=27,
|
|
423
|
+
input_type=InputType.HOLDING_REGISTER,
|
|
424
|
+
data_type=DataType.FLOAT32,
|
|
425
|
+
platform=Platform.SENSOR,
|
|
426
|
+
unit="°C",
|
|
427
|
+
),
|
|
428
|
+
EntityDef(
|
|
429
|
+
key="setpoint_room_heat_night",
|
|
430
|
+
name="Room setpoint heating (night)",
|
|
431
|
+
address=29,
|
|
432
|
+
input_type=InputType.HOLDING_REGISTER,
|
|
433
|
+
data_type=DataType.FLOAT32,
|
|
434
|
+
platform=Platform.SENSOR,
|
|
435
|
+
unit="°C",
|
|
436
|
+
),
|
|
437
|
+
EntityDef(
|
|
438
|
+
key="setpoint_room_cool_day",
|
|
439
|
+
name="Room setpoint cooling (day)",
|
|
440
|
+
address=31,
|
|
441
|
+
input_type=InputType.HOLDING_REGISTER,
|
|
442
|
+
data_type=DataType.FLOAT32,
|
|
443
|
+
platform=Platform.SENSOR,
|
|
444
|
+
unit="°C",
|
|
445
|
+
),
|
|
446
|
+
EntityDef(
|
|
447
|
+
key="setpoint_room_cool_night",
|
|
448
|
+
name="Room setpoint cooling (night)",
|
|
449
|
+
address=33,
|
|
450
|
+
input_type=InputType.HOLDING_REGISTER,
|
|
451
|
+
data_type=DataType.FLOAT32,
|
|
452
|
+
platform=Platform.SENSOR,
|
|
453
|
+
unit="°C",
|
|
454
|
+
),
|
|
455
|
+
EntityDef(
|
|
456
|
+
key="setpoint_dhw",
|
|
457
|
+
name="User-defined DHW setpoint",
|
|
458
|
+
address=173,
|
|
459
|
+
input_type=InputType.HOLDING_REGISTER,
|
|
460
|
+
data_type=DataType.FLOAT32,
|
|
461
|
+
platform=Platform.SENSOR,
|
|
462
|
+
unit="°C",
|
|
463
|
+
writable=True,
|
|
464
|
+
),
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
# Export as dict for easy lookup by key
|
|
468
|
+
SENSORS: dict[str, EntityDef] = {e.key: e for e in _SENSOR_DEFS}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Switch entity definitions for Qube Heat Pump."""
|
|
2
|
+
|
|
3
|
+
from .base import EntityDef, InputType, Platform
|
|
4
|
+
|
|
5
|
+
_SWITCH_DEFS: tuple[EntityDef, ...] = (
|
|
6
|
+
EntityDef(
|
|
7
|
+
key="bms_summerwinter",
|
|
8
|
+
name="Enable summer mode (cooling)",
|
|
9
|
+
address=22,
|
|
10
|
+
input_type=InputType.COIL,
|
|
11
|
+
platform=Platform.SWITCH,
|
|
12
|
+
writable=True,
|
|
13
|
+
),
|
|
14
|
+
EntityDef(
|
|
15
|
+
key="tapw_timeprogram_bms_forced",
|
|
16
|
+
name="Start DHW heating",
|
|
17
|
+
address=23,
|
|
18
|
+
input_type=InputType.COIL,
|
|
19
|
+
platform=Platform.SWITCH,
|
|
20
|
+
writable=True,
|
|
21
|
+
),
|
|
22
|
+
EntityDef(
|
|
23
|
+
key="antilegionella_frcstart_ant",
|
|
24
|
+
name="Start anti-legionella",
|
|
25
|
+
address=45,
|
|
26
|
+
input_type=InputType.COIL,
|
|
27
|
+
platform=Platform.SWITCH,
|
|
28
|
+
writable=True,
|
|
29
|
+
),
|
|
30
|
+
EntityDef(
|
|
31
|
+
key="en_plantsetp_compens",
|
|
32
|
+
name="Enable heating curve",
|
|
33
|
+
address=62,
|
|
34
|
+
input_type=InputType.COIL,
|
|
35
|
+
platform=Platform.SWITCH,
|
|
36
|
+
writable=True,
|
|
37
|
+
),
|
|
38
|
+
EntityDef(
|
|
39
|
+
key="bms_sgready_a",
|
|
40
|
+
name="SG Ready A",
|
|
41
|
+
address=65,
|
|
42
|
+
input_type=InputType.COIL,
|
|
43
|
+
platform=Platform.SWITCH,
|
|
44
|
+
writable=True,
|
|
45
|
+
),
|
|
46
|
+
EntityDef(
|
|
47
|
+
key="bms_sgready_b",
|
|
48
|
+
name="SG Ready B",
|
|
49
|
+
address=66,
|
|
50
|
+
input_type=InputType.COIL,
|
|
51
|
+
platform=Platform.SWITCH,
|
|
52
|
+
writable=True,
|
|
53
|
+
),
|
|
54
|
+
EntityDef(
|
|
55
|
+
key="modbus_demand",
|
|
56
|
+
name="Activate heat demand",
|
|
57
|
+
address=67,
|
|
58
|
+
input_type=InputType.COIL,
|
|
59
|
+
platform=Platform.SWITCH,
|
|
60
|
+
writable=True,
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# Export as dict for easy lookup by key
|
|
65
|
+
SWITCHES: dict[str, EntityDef] = {e.key: e for e in _SWITCH_DEFS}
|
python_qube_heatpump/models.py
CHANGED
|
@@ -1,37 +1,55 @@
|
|
|
1
1
|
"""Models for Qube Heat Pump."""
|
|
2
2
|
|
|
3
|
-
from dataclasses import dataclass
|
|
4
|
-
from typing import
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Any
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
@dataclass
|
|
8
8
|
class QubeState:
|
|
9
|
-
"""Representation of the Qube Heat Pump state.
|
|
9
|
+
"""Representation of the Qube Heat Pump state.
|
|
10
|
+
|
|
11
|
+
Typed fields for core sensors (used by official HA integration).
|
|
12
|
+
Extended dict for additional entities (used by HACS integration).
|
|
13
|
+
"""
|
|
10
14
|
|
|
11
15
|
# Temperatures
|
|
12
|
-
temp_supply:
|
|
13
|
-
temp_return:
|
|
14
|
-
temp_source_in:
|
|
15
|
-
temp_source_out:
|
|
16
|
-
temp_room:
|
|
17
|
-
temp_dhw:
|
|
18
|
-
temp_outside:
|
|
16
|
+
temp_supply: float | None = None
|
|
17
|
+
temp_return: float | None = None
|
|
18
|
+
temp_source_in: float | None = None
|
|
19
|
+
temp_source_out: float | None = None
|
|
20
|
+
temp_room: float | None = None
|
|
21
|
+
temp_dhw: float | None = None
|
|
22
|
+
temp_outside: float | None = None
|
|
19
23
|
|
|
20
24
|
# Power/Energy
|
|
21
|
-
power_thermic:
|
|
22
|
-
power_electric:
|
|
23
|
-
energy_total_electric:
|
|
24
|
-
energy_total_thermic:
|
|
25
|
-
cop_calc:
|
|
25
|
+
power_thermic: float | None = None
|
|
26
|
+
power_electric: float | None = None
|
|
27
|
+
energy_total_electric: float | None = None
|
|
28
|
+
energy_total_thermic: float | None = None
|
|
29
|
+
cop_calc: float | None = None
|
|
26
30
|
|
|
27
31
|
# Operation
|
|
28
|
-
status_code:
|
|
29
|
-
compressor_speed:
|
|
30
|
-
flow_rate:
|
|
32
|
+
status_code: int | None = None
|
|
33
|
+
compressor_speed: float | None = None
|
|
34
|
+
flow_rate: float | None = None
|
|
31
35
|
|
|
32
36
|
# Setpoints (Read/Write)
|
|
33
|
-
setpoint_room_heat_day:
|
|
34
|
-
setpoint_room_heat_night:
|
|
35
|
-
setpoint_room_cool_day:
|
|
36
|
-
setpoint_room_cool_night:
|
|
37
|
-
setpoint_dhw:
|
|
37
|
+
setpoint_room_heat_day: float | None = None
|
|
38
|
+
setpoint_room_heat_night: float | None = None
|
|
39
|
+
setpoint_room_cool_day: float | None = None
|
|
40
|
+
setpoint_room_cool_night: float | None = None
|
|
41
|
+
setpoint_dhw: float | None = None
|
|
42
|
+
|
|
43
|
+
# Extended dict for additional entities not covered by typed fields
|
|
44
|
+
_extended: dict[str, Any] = field(default_factory=dict)
|
|
45
|
+
|
|
46
|
+
def get(self, key: str, default: Any = None) -> Any:
|
|
47
|
+
"""Get a value by key, checking typed fields first, then _extended."""
|
|
48
|
+
if hasattr(self, key) and key != "_extended":
|
|
49
|
+
value = getattr(self, key)
|
|
50
|
+
return value if value is not None else default
|
|
51
|
+
return self._extended.get(key, default)
|
|
52
|
+
|
|
53
|
+
def set_extended(self, key: str, value: Any) -> None:
|
|
54
|
+
"""Set a value in the extended dict."""
|
|
55
|
+
self._extended[key] = value
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-qube-heatpump
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Async Modbus client for Qube Heat Pumps
|
|
5
5
|
Project-URL: Homepage, https://github.com/MattieGit/python-qube-heatpump
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/MattieGit/python-qube-heatpump/issues
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
python_qube_heatpump/__init__.py,sha256=VnHFpnpnRzpxmNPAGM29FQkrcPcBAaIniWjHu68-7WA,669
|
|
2
|
+
python_qube_heatpump/client.py,sha256=wg02PX5bIhXhgUKeJKtHKw6wPQOFUiZUoEVYxHEE30U,15158
|
|
3
|
+
python_qube_heatpump/const.py,sha256=oAwKCIpmXPh5dRwX1dhdmGTJZKMlTVDnspUqAgYGZk0,4651
|
|
4
|
+
python_qube_heatpump/models.py,sha256=vbJdY93JkQ6ea30qhGjuCknUxRJqTiz0mTjPsb2r4xk,1841
|
|
5
|
+
python_qube_heatpump/entities/__init__.py,sha256=rGEqLY-KcsIlmEaM7LbriDMEqedn7NLA4sgKDI6sYM8,342
|
|
6
|
+
python_qube_heatpump/entities/base.py,sha256=5itTSm6cxklHsPr1qz3UWmr1kYBeCVOAugk182ybf3s,1867
|
|
7
|
+
python_qube_heatpump/entities/binary_sensors.py,sha256=lsYC0MEHjJ7JmJUgYzdHndkrZ1Ekxz4j9HqYDMEE_IA,7649
|
|
8
|
+
python_qube_heatpump/entities/sensors.py,sha256=Lros4QS1xzapR26n40GeAUDyiBipKGPuHyWDUbzgIiw,13176
|
|
9
|
+
python_qube_heatpump/entities/switches.py,sha256=MAaK-63dgVZEm9Ch_CW7umYM_mPqMoPYl8i0ZpZIaYE,1676
|
|
10
|
+
python_qube_heatpump-1.3.0.dist-info/METADATA,sha256=QLiTaA-h8TeWQ7eFSIJKzftmMPGzp1-Rv7vhM1ezVDU,950
|
|
11
|
+
python_qube_heatpump-1.3.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
12
|
+
python_qube_heatpump-1.3.0.dist-info/licenses/LICENSE,sha256=qpuQXN7QwpILG9GYcmqrd3Ax5CxBZUBoT295xTgKnOM,1062
|
|
13
|
+
python_qube_heatpump-1.3.0.dist-info/RECORD,,
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
python_qube_heatpump/__init__.py,sha256=eW8_tyAweg7YTOI599pi8gf3mt7aXW0SCw5ZyrBVJpU,57
|
|
2
|
-
python_qube_heatpump/client.py,sha256=BgCJbQBV57cfsWMfakjbs2VXgrQ8BZH8KnX1pnTke7k,6083
|
|
3
|
-
python_qube_heatpump/const.py,sha256=sXvXRcDb1uvBgzH2lV67Me_XtUErKA3fXMLO9gEGrlQ,3680
|
|
4
|
-
python_qube_heatpump/models.py,sha256=3I5U9_rEe1r73hwY4sNAMXfIxyPjBOtx06c_mAURLDk,1141
|
|
5
|
-
python_qube_heatpump-1.2.3.dist-info/METADATA,sha256=P-_IjdAl1jsiU4xYN_6oqlYK6gPvN8mvxM3qwU5tGmY,950
|
|
6
|
-
python_qube_heatpump-1.2.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
7
|
-
python_qube_heatpump-1.2.3.dist-info/licenses/LICENSE,sha256=qpuQXN7QwpILG9GYcmqrd3Ax5CxBZUBoT295xTgKnOM,1062
|
|
8
|
-
python_qube_heatpump-1.2.3.dist-info/RECORD,,
|
|
File without changes
|
{python_qube_heatpump-1.2.3.dist-info → python_qube_heatpump-1.3.0.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|