aiocomfoconnect 0.2.1__tar.gz → 0.3.1__tar.gz
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.
- {aiocomfoconnect-0.2.1 → aiocomfoconnect-0.3.1}/PKG-INFO +5 -7
- {aiocomfoconnect-0.2.1 → aiocomfoconnect-0.3.1}/README.md +2 -2
- {aiocomfoconnect-0.2.1 → aiocomfoconnect-0.3.1}/aiocomfoconnect/__main__.py +71 -0
- {aiocomfoconnect-0.2.1 → aiocomfoconnect-0.3.1}/aiocomfoconnect/bridge.py +24 -2
- {aiocomfoconnect-0.2.1 → aiocomfoconnect-0.3.1}/aiocomfoconnect/comfoconnect.py +32 -3
- {aiocomfoconnect-0.2.1 → aiocomfoconnect-0.3.1}/aiocomfoconnect/const.py +48 -0
- {aiocomfoconnect-0.2.1 → aiocomfoconnect-0.3.1}/aiocomfoconnect/exceptions.py +4 -0
- aiocomfoconnect-0.3.1/aiocomfoconnect/properties.py +57 -0
- {aiocomfoconnect-0.2.1 → aiocomfoconnect-0.3.1}/aiocomfoconnect/sensors.py +35 -2
- {aiocomfoconnect-0.2.1 → aiocomfoconnect-0.3.1}/pyproject.toml +5 -5
- aiocomfoconnect-0.2.1/aiocomfoconnect/properties.py +0 -31
- {aiocomfoconnect-0.2.1 → aiocomfoconnect-0.3.1}/LICENSE +0 -0
- {aiocomfoconnect-0.2.1 → aiocomfoconnect-0.3.1}/aiocomfoconnect/__init__.py +0 -0
- {aiocomfoconnect-0.2.1 → aiocomfoconnect-0.3.1}/aiocomfoconnect/discovery.py +0 -0
- {aiocomfoconnect-0.2.1 → aiocomfoconnect-0.3.1}/aiocomfoconnect/protobuf/nanopb_pb2.py +0 -0
- {aiocomfoconnect-0.2.1 → aiocomfoconnect-0.3.1}/aiocomfoconnect/protobuf/zehnder_pb2.py +0 -0
- {aiocomfoconnect-0.2.1 → aiocomfoconnect-0.3.1}/aiocomfoconnect/util.py +0 -0
|
@@ -1,17 +1,15 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiocomfoconnect
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: aiocomfoconnect is an asyncio Python 3 library for communicating with a Zehnder ComfoAir Q350/450/600 ventilation system
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Author: Michaël Arnauts
|
|
7
7
|
Author-email: michael.arnauts@gmail.com
|
|
8
|
-
Requires-Python: >=3.
|
|
8
|
+
Requires-Python: >=3.13,<4.0
|
|
9
9
|
Classifier: Programming Language :: Python :: 3
|
|
10
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
13
10
|
Classifier: Programming Language :: Python :: 3.13
|
|
14
11
|
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
15
13
|
Requires-Dist: aiohttp (>=3.8.0,<4.0.0)
|
|
16
14
|
Requires-Dist: protobuf (>=6.31,<8)
|
|
17
15
|
Project-URL: Homepage, https://github.com/michaelarnauts/aiocomfoconnect
|
|
@@ -24,7 +22,7 @@ Description-Content-Type: text/markdown
|
|
|
24
22
|
|
|
25
23
|
The [home-assistant-comfoconnect](https://github.com/michaelarnauts/home-assistant-comfoconnect) is using this library.
|
|
26
24
|
|
|
27
|
-
It's compatible with Python 3.
|
|
25
|
+
It's compatible with Python 3.13 and higher.
|
|
28
26
|
|
|
29
27
|
## Installation
|
|
30
28
|
|
|
@@ -65,7 +63,7 @@ $ python -m aiocomfoconnect get-property --host 192.168.1.213 1 1 8 9 # Unit 0x
|
|
|
65
63
|
- `async register_sensor(sensor)`: Register a sensor.
|
|
66
64
|
- `async deregister_sensor(sensor)`: Deregister a sensor.
|
|
67
65
|
- `async get_mode()`: Get the ventilation mode.
|
|
68
|
-
- `async set_mode(mode)`: Set the ventilation mode. (auto / manual)
|
|
66
|
+
- `async set_mode(mode)`: Set the ventilation mode. (auto / manual) Selecting `auto` also cancels the temporary fan-speed override and returns control to the unit's schedule.
|
|
69
67
|
- `async get_comfocool_mode()`: Get Comfocool mode
|
|
70
68
|
- `async set_comfocool_mode()`: Set Comfocool mode. (auto / off)
|
|
71
69
|
- `async get_speed()`: Get the ventilation speed.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
The [home-assistant-comfoconnect](https://github.com/michaelarnauts/home-assistant-comfoconnect) is using this library.
|
|
7
7
|
|
|
8
|
-
It's compatible with Python 3.
|
|
8
|
+
It's compatible with Python 3.13 and higher.
|
|
9
9
|
|
|
10
10
|
## Installation
|
|
11
11
|
|
|
@@ -46,7 +46,7 @@ $ python -m aiocomfoconnect get-property --host 192.168.1.213 1 1 8 9 # Unit 0x
|
|
|
46
46
|
- `async register_sensor(sensor)`: Register a sensor.
|
|
47
47
|
- `async deregister_sensor(sensor)`: Deregister a sensor.
|
|
48
48
|
- `async get_mode()`: Get the ventilation mode.
|
|
49
|
-
- `async set_mode(mode)`: Set the ventilation mode. (auto / manual)
|
|
49
|
+
- `async set_mode(mode)`: Set the ventilation mode. (auto / manual) Selecting `auto` also cancels the temporary fan-speed override and returns control to the unit's schedule.
|
|
50
50
|
- `async get_comfocool_mode()`: Get Comfocool mode
|
|
51
51
|
- `async set_comfocool_mode()`: Set Comfocool mode. (auto / off)
|
|
52
52
|
- `async get_speed()`: Get the ventilation speed.
|
|
@@ -11,6 +11,7 @@ from typing import Literal, Optional
|
|
|
11
11
|
|
|
12
12
|
from aiocomfoconnect import DEFAULT_NAME, DEFAULT_PIN, DEFAULT_UUID
|
|
13
13
|
from aiocomfoconnect.comfoconnect import ComfoConnect
|
|
14
|
+
from aiocomfoconnect.const import ProductId
|
|
14
15
|
from aiocomfoconnect.discovery import discover_bridges
|
|
15
16
|
from aiocomfoconnect.exceptions import (
|
|
16
17
|
AioComfoConnectNotConnected,
|
|
@@ -24,6 +25,8 @@ from aiocomfoconnect.sensors import SENSORS
|
|
|
24
25
|
|
|
25
26
|
_LOGGER = logging.getLogger(__name__)
|
|
26
27
|
|
|
28
|
+
PRODUCT_NAMES = {value: name.lower() for name, value in vars(ProductId).items() if not name.startswith("_")}
|
|
29
|
+
|
|
27
30
|
|
|
28
31
|
async def main(args):
|
|
29
32
|
"""Main function."""
|
|
@@ -57,6 +60,12 @@ async def main(args):
|
|
|
57
60
|
elif args.action == "get-property":
|
|
58
61
|
await run_get_property(args.host, args.uuid, args.node_id, args.unit, args.subunit, args.property_id, args.property_type)
|
|
59
62
|
|
|
63
|
+
elif args.action == "set-property":
|
|
64
|
+
await run_set_property(args.host, args.uuid, args.node_id, args.unit, args.subunit, args.property_id, args.property_type, args.value)
|
|
65
|
+
|
|
66
|
+
elif args.action == "list-nodes":
|
|
67
|
+
await run_list_nodes(args.host, args.uuid)
|
|
68
|
+
|
|
60
69
|
elif args.action == "get-flow-for-speed":
|
|
61
70
|
await run_get_flow_for_speed(args.host, args.uuid, args.speed)
|
|
62
71
|
|
|
@@ -341,6 +350,54 @@ async def run_get_property(host: str, uuid: str, node_id: Optional[int], unit: i
|
|
|
341
350
|
await comfoconnect.disconnect()
|
|
342
351
|
|
|
343
352
|
|
|
353
|
+
async def run_set_property(host: str, uuid: str, node_id: Optional[int], unit: int, subunit: int, property_id: int, property_type: int, value: int):
|
|
354
|
+
"""Set a property and print the value that is read back."""
|
|
355
|
+
# Discover bridge so we know the UUID
|
|
356
|
+
bridges = await discover_bridges(host)
|
|
357
|
+
if not bridges:
|
|
358
|
+
raise BridgeNotFoundException("No bridge found")
|
|
359
|
+
|
|
360
|
+
# Connect to the bridge
|
|
361
|
+
comfoconnect = ComfoConnect(bridges[0].host, bridges[0].uuid)
|
|
362
|
+
try:
|
|
363
|
+
await comfoconnect.connect(uuid)
|
|
364
|
+
except ComfoConnectNotAllowed:
|
|
365
|
+
print("Could not connect to bridge. Please register first.")
|
|
366
|
+
sys.exit(1)
|
|
367
|
+
|
|
368
|
+
await comfoconnect.set_property_typed(unit, subunit, property_id, value, property_type, node_id=node_id)
|
|
369
|
+
print(await comfoconnect.get_property(Property(unit, subunit, property_id, property_type), node_id))
|
|
370
|
+
|
|
371
|
+
await comfoconnect.disconnect()
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
async def run_list_nodes(host: str, uuid: str):
|
|
375
|
+
"""List the nodes that the bridge sees on the ComfoNet bus."""
|
|
376
|
+
# Discover bridge so we know the UUID
|
|
377
|
+
bridges = await discover_bridges(host)
|
|
378
|
+
if not bridges:
|
|
379
|
+
raise BridgeNotFoundException("No bridge found")
|
|
380
|
+
|
|
381
|
+
# Connect to the bridge
|
|
382
|
+
comfoconnect = ComfoConnect(bridges[0].host, bridges[0].uuid)
|
|
383
|
+
try:
|
|
384
|
+
await comfoconnect.connect(uuid)
|
|
385
|
+
except ComfoConnectNotAllowed:
|
|
386
|
+
print("Could not connect to bridge. Please register first.")
|
|
387
|
+
sys.exit(1)
|
|
388
|
+
|
|
389
|
+
await comfoconnect.cmd_node_request()
|
|
390
|
+
# Nodes arrive as unsolicited notifications, so give the bridge a moment to report them all.
|
|
391
|
+
await asyncio.sleep(2)
|
|
392
|
+
|
|
393
|
+
print("Nodes on the ComfoNet bus:")
|
|
394
|
+
for node in sorted(comfoconnect.nodes.values(), key=lambda n: n.node_id):
|
|
395
|
+
product = PRODUCT_NAMES.get(node.product_id, "unknown")
|
|
396
|
+
print(f"* node {node.node_id}: product {node.product_id} ({product}), zone {node.zone_id}, mode {node.mode}")
|
|
397
|
+
|
|
398
|
+
await comfoconnect.disconnect()
|
|
399
|
+
|
|
400
|
+
|
|
344
401
|
async def run_get_flow_for_speed(host: str, uuid: str, speed: Literal["away", "low", "medium", "high"]):
|
|
345
402
|
"""Get the configured airflow for the specified speed."""
|
|
346
403
|
# Discover bridge so we know the UUID
|
|
@@ -444,6 +501,20 @@ def main_cli():
|
|
|
444
501
|
p_sensor.add_argument("--host", help="Host address of the bridge")
|
|
445
502
|
p_sensor.add_argument("--uuid", help="UUID of this app", default=DEFAULT_UUID)
|
|
446
503
|
|
|
504
|
+
p_set_property = subparsers.add_parser("set-property", help="set a property value")
|
|
505
|
+
p_set_property.add_argument("unit", help="The Unit of the property", type=int)
|
|
506
|
+
p_set_property.add_argument("subunit", help="The Subunit of the property", type=int)
|
|
507
|
+
p_set_property.add_argument("property_id", help="The id of the property", type=int)
|
|
508
|
+
p_set_property.add_argument("property_type", help="The type of the property", type=int)
|
|
509
|
+
p_set_property.add_argument("value", help="The raw value to write", type=int)
|
|
510
|
+
p_set_property.add_argument("--node_id", help="The Node ID to write to (defaults to the discovered ventilation unit)", type=int, default=None)
|
|
511
|
+
p_set_property.add_argument("--host", help="Host address of the bridge")
|
|
512
|
+
p_set_property.add_argument("--uuid", help="UUID of this app", default=DEFAULT_UUID)
|
|
513
|
+
|
|
514
|
+
p_list_nodes = subparsers.add_parser("list-nodes", help="list the nodes on the ComfoNet bus")
|
|
515
|
+
p_list_nodes.add_argument("--host", help="Host address of the bridge")
|
|
516
|
+
p_list_nodes.add_argument("--uuid", help="UUID of this app", default=DEFAULT_UUID)
|
|
517
|
+
|
|
447
518
|
p_get_flow_speed = subparsers.add_parser("get-flow-for-speed", help="Get m³/h for given speed")
|
|
448
519
|
p_get_flow_speed.add_argument("speed", help="Fan speed", choices=["low", "medium", "high", "away"])
|
|
449
520
|
p_get_flow_speed.add_argument("--host", help="Host address of the bridge")
|
|
@@ -13,7 +13,7 @@ from typing import Awaitable, Callable, Dict, Iterator, Optional, Set
|
|
|
13
13
|
from google.protobuf.message import DecodeError
|
|
14
14
|
from google.protobuf.message import Message as ProtobufMessage
|
|
15
15
|
|
|
16
|
-
from .const import VENTILATION_UNIT_PRODUCT_IDS
|
|
16
|
+
from .const import VENTILATION_UNIT_PRODUCT_IDS, ProductId, PdoType
|
|
17
17
|
from .exceptions import (
|
|
18
18
|
AioComfoConnectNotConnected,
|
|
19
19
|
AioComfoConnectNotReachable,
|
|
@@ -67,6 +67,11 @@ class Node:
|
|
|
67
67
|
"""Returns True if this node is the ventilation unit that handles RMI commands."""
|
|
68
68
|
return self.product_id in VENTILATION_UNIT_PRODUCT_IDS
|
|
69
69
|
|
|
70
|
+
@property
|
|
71
|
+
def is_comfoclime(self) -> bool:
|
|
72
|
+
"""Returns True if this node is a ComfoClime heat pump module."""
|
|
73
|
+
return self.product_id == ProductId.COMFOCLIME
|
|
74
|
+
|
|
70
75
|
|
|
71
76
|
class EventBus:
|
|
72
77
|
"""An event bus for async replies."""
|
|
@@ -134,6 +139,7 @@ class Bridge:
|
|
|
134
139
|
self._ventilation_node_found: Optional[asyncio.Event] = None
|
|
135
140
|
|
|
136
141
|
self.__sensor_callback_fn: Optional[Callable[[int, int], None]] = None
|
|
142
|
+
self._sensor_pdo_types: Dict[int, int] = {}
|
|
137
143
|
self.__alarm_callback_fn: Optional[Callable[[int, ProtobufMessage], None]] = None
|
|
138
144
|
|
|
139
145
|
self._loop: Optional[asyncio.AbstractEventLoop] = loop
|
|
@@ -253,6 +259,18 @@ class Bridge:
|
|
|
253
259
|
candidates = [node.node_id for node in self._nodes.values() if node.is_ventilation_unit]
|
|
254
260
|
return min(candidates) if candidates else None
|
|
255
261
|
|
|
262
|
+
@property
|
|
263
|
+
def comfoclime_node_id(self) -> Optional[int]:
|
|
264
|
+
"""The node id of the ComfoClime, or None when the bridge hasn't announced one."""
|
|
265
|
+
candidates = [node.node_id for node in self._nodes.values() if node.is_comfoclime]
|
|
266
|
+
return min(candidates) if candidates else None
|
|
267
|
+
|
|
268
|
+
@property
|
|
269
|
+
def comfoclime_zone_id(self) -> Optional[int]:
|
|
270
|
+
"""The zone id of the ComfoClime, or None when the bridge hasn't announced one."""
|
|
271
|
+
candidates = [node for node in self._nodes.values() if node.is_comfoclime]
|
|
272
|
+
return min(candidates, key=lambda node: node.node_id).zone_id if candidates else None
|
|
273
|
+
|
|
256
274
|
async def wait_for_ventilation_node(self, timeout: float = None) -> int:
|
|
257
275
|
"""Wait until the bridge has announced the ventilation unit and return its node id.
|
|
258
276
|
|
|
@@ -424,7 +442,9 @@ class Bridge:
|
|
|
424
442
|
# pylint: disable=no-member
|
|
425
443
|
if message.cmd.type == zehnder_pb2.GatewayOperation.CnRpdoNotificationType:
|
|
426
444
|
if self.__sensor_callback_fn:
|
|
427
|
-
self.
|
|
445
|
+
pdo_type = self._sensor_pdo_types.get(message.msg.pdid)
|
|
446
|
+
signed = pdo_type not in (PdoType.TYPE_CN_UINT8, PdoType.TYPE_CN_UINT16, PdoType.TYPE_CN_UINT32)
|
|
447
|
+
self.__sensor_callback_fn(message.msg.pdid, int.from_bytes(message.msg.data, byteorder="little", signed=signed))
|
|
428
448
|
else:
|
|
429
449
|
_LOGGER.info("Unhandled CnRpdoNotificationType since no callback is registered.")
|
|
430
450
|
|
|
@@ -572,6 +592,8 @@ class Bridge:
|
|
|
572
592
|
def cmd_rpdo_request(self, pdid: int, pdo_type: int = 1, zone: int = 1, timeout=None) -> Awaitable[Message]:
|
|
573
593
|
"""Register a RPDO request."""
|
|
574
594
|
_LOGGER.debug("CnRpdoRequest")
|
|
595
|
+
# Notifications can arrive before the subscription acknowledgement.
|
|
596
|
+
self._sensor_pdo_types[pdid] = pdo_type
|
|
575
597
|
# pylint: disable=no-member
|
|
576
598
|
return self._send(
|
|
577
599
|
zehnder_pb2.CnRpdoRequest,
|
|
@@ -33,6 +33,7 @@ from aiocomfoconnect.exceptions import (
|
|
|
33
33
|
AioComfoConnectNotConnected,
|
|
34
34
|
AioComfoConnectNotReachable,
|
|
35
35
|
AioComfoConnectTimeout,
|
|
36
|
+
ComfoClimeNotFoundException,
|
|
36
37
|
ComfoConnectNotAllowed,
|
|
37
38
|
VentilationUnitNotFoundException,
|
|
38
39
|
)
|
|
@@ -289,6 +290,28 @@ class ComfoConnect(Bridge):
|
|
|
289
290
|
|
|
290
291
|
return result.message
|
|
291
292
|
|
|
293
|
+
def _require_comfoclime_node_id(self) -> int:
|
|
294
|
+
"""Return the node id of the ComfoClime, or raise when it isn't on the bus."""
|
|
295
|
+
node_id = self.comfoclime_node_id
|
|
296
|
+
if node_id is None:
|
|
297
|
+
raise ComfoClimeNotFoundException("The bridge has not announced a ComfoClime")
|
|
298
|
+
return node_id
|
|
299
|
+
|
|
300
|
+
async def get_comfoclime_property(self, prop: Property) -> Any:
|
|
301
|
+
"""Get a property from the ComfoClime node."""
|
|
302
|
+
return await self.get_property(prop, node_id=self._require_comfoclime_node_id())
|
|
303
|
+
|
|
304
|
+
async def set_comfoclime_property(self, prop: Property, value: int) -> Any:
|
|
305
|
+
"""Set a property on the ComfoClime node."""
|
|
306
|
+
return await self.set_property_typed(
|
|
307
|
+
prop.unit,
|
|
308
|
+
prop.subunit,
|
|
309
|
+
prop.property_id,
|
|
310
|
+
value,
|
|
311
|
+
prop.property_type,
|
|
312
|
+
node_id=self._require_comfoclime_node_id(),
|
|
313
|
+
)
|
|
314
|
+
|
|
292
315
|
def _sensor_callback(self, sensor_id, sensor_value):
|
|
293
316
|
"""Callback function for sensor updates."""
|
|
294
317
|
if self._sensor_callback_fn is None:
|
|
@@ -301,9 +324,14 @@ class ComfoConnect(Bridge):
|
|
|
301
324
|
|
|
302
325
|
self._sensors_values[sensor_id] = sensor_value
|
|
303
326
|
|
|
304
|
-
#
|
|
327
|
+
# The bridge sends invalid (zero) values right after we subscribe to a sensor, so we hold them back.
|
|
328
|
+
# A non-zero value is a real value, so we can release the hold and emit it right away.
|
|
305
329
|
if sensor_id in self._sensor_holds:
|
|
306
|
-
|
|
330
|
+
if not sensor_value:
|
|
331
|
+
return
|
|
332
|
+
|
|
333
|
+
_LOGGER.debug("Got a valid value for sensor %s, releasing the hold", sensor_id)
|
|
334
|
+
self._sensor_holds.pop(sensor_id).cancel()
|
|
307
335
|
|
|
308
336
|
if sensor.value_fn:
|
|
309
337
|
val = sensor.value_fn(sensor_value)
|
|
@@ -335,9 +363,10 @@ class ComfoConnect(Bridge):
|
|
|
335
363
|
return VentilationMode.MANUAL if mode == 1 else VentilationMode.AUTO
|
|
336
364
|
|
|
337
365
|
async def set_mode(self, mode: Literal["auto", "manual"]):
|
|
338
|
-
"""Set
|
|
366
|
+
"""Set ventilation mode, cancelling the temporary speed override for auto."""
|
|
339
367
|
if mode == VentilationMode.AUTO:
|
|
340
368
|
await self.cmd_rmi_request(bytes([0x85, UNIT_SCHEDULE, SUBUNIT_08, 0x01]))
|
|
369
|
+
await self.cmd_rmi_request(bytes([0x85, UNIT_SCHEDULE, SUBUNIT_01, 0x01]))
|
|
341
370
|
elif mode == VentilationMode.MANUAL:
|
|
342
371
|
await self.cmd_rmi_request(bytes([0x84, UNIT_SCHEDULE, SUBUNIT_08, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01]))
|
|
343
372
|
else:
|
|
@@ -72,6 +72,11 @@ UNIT_POSTHEATER = 0x27
|
|
|
72
72
|
UNIT_COMFOFOND = 0x28
|
|
73
73
|
UNIT_CO2SENSOR = 0x2B
|
|
74
74
|
UNIT_SERVICEPRINT = 0x2C
|
|
75
|
+
|
|
76
|
+
# Units that only exist on a ComfoClime node. Unit numbers are reused across product types,
|
|
77
|
+
# so 0x16 / 0x17 mean UNIT_VALVE / UNIT_FAN on a ComfoAir node.
|
|
78
|
+
UNIT_CLIME_TEMPCONFIG = 0x16
|
|
79
|
+
UNIT_CLIME_HEATPUMP = 0x17
|
|
75
80
|
# UNIT_COOLER(21) = 0x15
|
|
76
81
|
# UNIT_CC_TEMPERATURESENSOR(22) = 0x16
|
|
77
82
|
# UNIT_IOSENSOR(21) = 0x15
|
|
@@ -245,3 +250,46 @@ class ComfoCoolMode:
|
|
|
245
250
|
|
|
246
251
|
AUTO = "auto"
|
|
247
252
|
OFF = "off"
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class ComfoClimeSeason:
|
|
256
|
+
"""Enum for ComfoClime seasons."""
|
|
257
|
+
|
|
258
|
+
TRANSITION = "transition"
|
|
259
|
+
HEATING = "heating"
|
|
260
|
+
COOLING = "cooling"
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
class ComfoClimeTemperatureProfile:
|
|
264
|
+
"""Enum for ComfoClime temperature profiles."""
|
|
265
|
+
|
|
266
|
+
COMFORT = "comfort"
|
|
267
|
+
POWER = "power"
|
|
268
|
+
ECO = "eco"
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class ComfoClimeDeviceMode:
|
|
272
|
+
"""Enum for the ComfoClime device mode."""
|
|
273
|
+
|
|
274
|
+
OFF = "off"
|
|
275
|
+
HEATING = "heating"
|
|
276
|
+
COOLING = "cooling"
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
COMFOCLIME_SEASONS = {
|
|
280
|
+
0: ComfoClimeSeason.TRANSITION,
|
|
281
|
+
1: ComfoClimeSeason.HEATING,
|
|
282
|
+
2: ComfoClimeSeason.COOLING,
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
COMFOCLIME_TEMPERATURE_PROFILES = {
|
|
286
|
+
0: ComfoClimeTemperatureProfile.COMFORT,
|
|
287
|
+
1: ComfoClimeTemperatureProfile.POWER,
|
|
288
|
+
2: ComfoClimeTemperatureProfile.ECO,
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
COMFOCLIME_DEVICE_MODES = {
|
|
292
|
+
0: ComfoClimeDeviceMode.OFF,
|
|
293
|
+
1: ComfoClimeDeviceMode.HEATING,
|
|
294
|
+
2: ComfoClimeDeviceMode.COOLING,
|
|
295
|
+
}
|
|
@@ -62,5 +62,9 @@ class VentilationUnitNotFoundException(Exception):
|
|
|
62
62
|
"""Exception raised when the bridge didn't announce a ventilation unit on the ComfoNet bus."""
|
|
63
63
|
|
|
64
64
|
|
|
65
|
+
class ComfoClimeNotFoundException(Exception):
|
|
66
|
+
"""Exception raised when the bridge didn't announce a ComfoClime on the ComfoNet bus."""
|
|
67
|
+
|
|
68
|
+
|
|
65
69
|
class UnknownActionException(Exception):
|
|
66
70
|
"""Exception raised when an unknown action is provided."""
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
""" Property definitions """
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from .const import (
|
|
8
|
+
UNIT_CLIME_HEATPUMP,
|
|
9
|
+
UNIT_CLIME_TEMPCONFIG,
|
|
10
|
+
UNIT_NODE,
|
|
11
|
+
UNIT_NODECONFIGURATION,
|
|
12
|
+
UNIT_TEMPHUMCONTROL,
|
|
13
|
+
PdoType,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class Property:
|
|
19
|
+
"""Dataclass for a Property"""
|
|
20
|
+
|
|
21
|
+
unit: int
|
|
22
|
+
subunit: int
|
|
23
|
+
property_id: int
|
|
24
|
+
property_type: int
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
PROPERTY_SERIAL_NUMBER = Property(UNIT_NODE, 0x01, 0x04, PdoType.TYPE_CN_STRING)
|
|
28
|
+
PROPERTY_FIRMWARE_VERSION = Property(UNIT_NODE, 0x01, 0x06, PdoType.TYPE_CN_UINT32)
|
|
29
|
+
PROPERTY_MODEL = Property(UNIT_NODE, 0x01, 0x08, PdoType.TYPE_CN_STRING)
|
|
30
|
+
PROPERTY_ARTICLE = Property(UNIT_NODE, 0x01, 0x0B, PdoType.TYPE_CN_STRING)
|
|
31
|
+
PROPERTY_COUNTRY = Property(UNIT_NODE, 0x01, 0x0D, PdoType.TYPE_CN_STRING)
|
|
32
|
+
PROPERTY_NAME = Property(UNIT_NODE, 0x01, 0x14, PdoType.TYPE_CN_STRING)
|
|
33
|
+
|
|
34
|
+
PROPERTY_MAINTAINER_PASSWORD = Property(UNIT_NODECONFIGURATION, 0x01, 0x03, PdoType.TYPE_CN_STRING)
|
|
35
|
+
|
|
36
|
+
PROPERTY_SENSOR_VENTILATION_TEMP_PASSIVE = Property(UNIT_TEMPHUMCONTROL, 0x01, 0x04, PdoType.TYPE_CN_UINT32)
|
|
37
|
+
PROPERTY_SENSOR_VENTILATION_HUMIDITY_COMFORT = Property(UNIT_TEMPHUMCONTROL, 0x01, 0x06, PdoType.TYPE_CN_UINT32)
|
|
38
|
+
PROPERTY_SENSOR_VENTILATION_HUMIDITY_PROTECTION = Property(UNIT_TEMPHUMCONTROL, 0x01, 0x07, PdoType.TYPE_CN_UINT32)
|
|
39
|
+
|
|
40
|
+
# Temperatures are stored in tenths of a degree.
|
|
41
|
+
PROPERTY_RMOT_HEATING_THRESHOLD = Property(UNIT_TEMPHUMCONTROL, 0x01, 0x02, PdoType.TYPE_CN_INT16)
|
|
42
|
+
PROPERTY_RMOT_COOLING_THRESHOLD = Property(UNIT_TEMPHUMCONTROL, 0x01, 0x03, PdoType.TYPE_CN_INT16)
|
|
43
|
+
|
|
44
|
+
# ComfoClime properties. These live on the ComfoClime node, not on the ventilation unit.
|
|
45
|
+
PROPERTY_CLIME_AUTO_SEASON = Property(UNIT_CLIME_TEMPCONFIG, 0x01, 0x02, PdoType.TYPE_CN_UINT8)
|
|
46
|
+
PROPERTY_CLIME_SEASON = Property(UNIT_CLIME_TEMPCONFIG, 0x01, 0x03, PdoType.TYPE_CN_UINT8)
|
|
47
|
+
PROPERTY_CLIME_HEATING_KNEE_POINT = Property(UNIT_CLIME_TEMPCONFIG, 0x01, 0x04, PdoType.TYPE_CN_INT16)
|
|
48
|
+
PROPERTY_CLIME_COOLING_KNEE_POINT = Property(UNIT_CLIME_TEMPCONFIG, 0x01, 0x05, PdoType.TYPE_CN_INT16)
|
|
49
|
+
PROPERTY_CLIME_HEATING_COMFORT_TEMP = Property(UNIT_CLIME_TEMPCONFIG, 0x01, 0x09, PdoType.TYPE_CN_INT16)
|
|
50
|
+
PROPERTY_CLIME_COOLING_COMFORT_TEMP = Property(UNIT_CLIME_TEMPCONFIG, 0x01, 0x0A, PdoType.TYPE_CN_INT16)
|
|
51
|
+
PROPERTY_CLIME_COOLING_TEMP_LIMIT = Property(UNIT_CLIME_TEMPCONFIG, 0x01, 0x0B, PdoType.TYPE_CN_INT16)
|
|
52
|
+
PROPERTY_CLIME_HEATING_DELTA = Property(UNIT_CLIME_TEMPCONFIG, 0x01, 0x0C, PdoType.TYPE_CN_INT16)
|
|
53
|
+
PROPERTY_CLIME_MANUAL_TARGET_TEMP = Property(UNIT_CLIME_TEMPCONFIG, 0x01, 0x0D, PdoType.TYPE_CN_INT16)
|
|
54
|
+
PROPERTY_CLIME_TEMPERATURE_PROFILE = Property(UNIT_CLIME_TEMPCONFIG, 0x01, 0x1D, PdoType.TYPE_CN_UINT8)
|
|
55
|
+
PROPERTY_CLIME_HEATPUMP_STANDBY = Property(UNIT_CLIME_TEMPCONFIG, 0x01, 0x15, PdoType.TYPE_CN_UINT8)
|
|
56
|
+
PROPERTY_CLIME_HEATPUMP_MAX_TEMP = Property(UNIT_CLIME_HEATPUMP, 0x01, 0x03, PdoType.TYPE_CN_INT16)
|
|
57
|
+
PROPERTY_CLIME_HEATPUMP_MIN_TEMP = Property(UNIT_CLIME_HEATPUMP, 0x01, 0x04, PdoType.TYPE_CN_INT16)
|
|
@@ -78,6 +78,20 @@ SENSOR_UNIT_TEMPERATURE = 208
|
|
|
78
78
|
SENSOR_COMFOCOOL_STATE = 784
|
|
79
79
|
SENSOR_COMFOCOOL_CONDENSOR_TEMP = 802
|
|
80
80
|
|
|
81
|
+
# ComfoClime sensors. These are only reported when a ComfoClime is present on the ComfoNet bus.
|
|
82
|
+
SENSOR_COMFOCLIME_TPMA_TEMPERATURE = 4145
|
|
83
|
+
SENSOR_COMFOCLIME_TARGET_TEMPERATURE = 4148
|
|
84
|
+
SENSOR_COMFOCLIME_DEVICE_MODE = 4149
|
|
85
|
+
SENSOR_COMFOCLIME_COMFORT_TEMPERATURE = 4151
|
|
86
|
+
SENSOR_COMFOCLIME_INDOOR_TEMPERATURE = 4154
|
|
87
|
+
SENSOR_COMFOCLIME_TEMPERATURE_SUPPLY = 4193
|
|
88
|
+
SENSOR_COMFOCLIME_TEMPERATURE_EXHAUST = 4194
|
|
89
|
+
SENSOR_COMFOCLIME_TEMPERATURE_SUPPLY_COIL = 4195
|
|
90
|
+
SENSOR_COMFOCLIME_TEMPERATURE_EXHAUST_COIL = 4196
|
|
91
|
+
SENSOR_COMFOCLIME_TEMPERATURE_COMPRESSOR = 4197
|
|
92
|
+
SENSOR_COMFOCLIME_HEATPUMP_DUTY = 4198
|
|
93
|
+
SENSOR_COMFOCLIME_POWER_USAGE = 4201
|
|
94
|
+
|
|
81
95
|
UNIT_WATT = "W"
|
|
82
96
|
UNIT_KWH = "kWh"
|
|
83
97
|
UNIT_VOLT = "V"
|
|
@@ -133,7 +147,7 @@ SENSORS: Dict[int, Sensor] = {
|
|
|
133
147
|
SENSOR_PREHEATER_POWER: Sensor("Preheater Power Usage", UNIT_WATT, 146, PdoType.TYPE_CN_UINT16),
|
|
134
148
|
SENSOR_RF_PAIRING_MODE: Sensor("RF Pairing Mode", None, 176, PdoType.TYPE_CN_UINT8),
|
|
135
149
|
SENSOR_DAYS_TO_REPLACE_FILTER: Sensor("Days remaining to replace the filter", None, 192, PdoType.TYPE_CN_UINT16),
|
|
136
|
-
SENSOR_UNIT_TEMPERATURE: Sensor("Device Temperature Unit", None, 208, PdoType.TYPE_CN_UINT8, lambda x: "
|
|
150
|
+
SENSOR_UNIT_TEMPERATURE: Sensor("Device Temperature Unit", None, 208, PdoType.TYPE_CN_UINT8, lambda x: {0: "celsius", 1: "fahrenheit"}.get(x, x)),
|
|
137
151
|
SENSOR_RMOT: Sensor("Running Mean Outdoor Temperature (RMOT)", UNIT_CELCIUS, 209, PdoType.TYPE_CN_INT16, lambda x: x / 10),
|
|
138
152
|
SENSOR_SEASON_HEATING_ACTIVE: Sensor("Heating Season is active", None, 210, PdoType.TYPE_CN_BOOL, bool),
|
|
139
153
|
SENSOR_SEASON_COOLING_ACTIVE: Sensor("Cooling Season is active", None, 211, PdoType.TYPE_CN_BOOL, bool),
|
|
@@ -147,7 +161,7 @@ SENSORS: Dict[int, Sensor] = {
|
|
|
147
161
|
219: Sensor("sensor_219", None, 219, PdoType.TYPE_CN_UINT16),
|
|
148
162
|
220: Sensor("Outdoor Air Temperature (?)", None, 220, PdoType.TYPE_CN_INT16, lambda x: x / 10),
|
|
149
163
|
SENSOR_TEMPERATURE_SUPPLY: Sensor("Supply Air Temperature", UNIT_CELCIUS, 221, PdoType.TYPE_CN_INT16, lambda x: x / 10),
|
|
150
|
-
SENSOR_UNIT_AIRFLOW: Sensor("Device Airflow Unit", None, 224, PdoType.TYPE_CN_UINT8, lambda x: "
|
|
164
|
+
SENSOR_UNIT_AIRFLOW: Sensor("Device Airflow Unit", None, 224, PdoType.TYPE_CN_UINT8, lambda x: {1: "kgph", 2: "lps", 3: "m3ph"}.get(x, x)),
|
|
151
165
|
SENSOR_COMFORTCONTROL_MODE: Sensor("Sensor based ventilation mode", None, 225, PdoType.TYPE_CN_UINT8),
|
|
152
166
|
SENSOR_FAN_SPEED_MODE_MODULATED: Sensor("Fan Speed (modulated)", None, 226, PdoType.TYPE_CN_UINT16),
|
|
153
167
|
SENSOR_BYPASS_STATE: Sensor("Bypass State", UNIT_PERCENT, 227, PdoType.TYPE_CN_UINT8),
|
|
@@ -186,4 +200,23 @@ SENSORS: Dict[int, Sensor] = {
|
|
|
186
200
|
SENSOR_COMFOCOOL_STATE: Sensor("ComfoCool State", None, 784, PdoType.TYPE_CN_UINT8),
|
|
187
201
|
785: Sensor("sensor_785", None, 785, PdoType.TYPE_CN_BOOL),
|
|
188
202
|
SENSOR_COMFOCOOL_CONDENSOR_TEMP: Sensor("ComfoCool Condensor Temperature", None, 802, PdoType.TYPE_CN_INT16, lambda x: x / 10),
|
|
203
|
+
SENSOR_COMFOCLIME_TPMA_TEMPERATURE: Sensor("ComfoClime TPMA Temperature", UNIT_CELCIUS, 4145, PdoType.TYPE_CN_INT16, lambda x: x / 10),
|
|
204
|
+
SENSOR_COMFOCLIME_TARGET_TEMPERATURE: Sensor("ComfoClime Target Temperature", UNIT_CELCIUS, 4148, PdoType.TYPE_CN_INT16, lambda x: x / 10),
|
|
205
|
+
SENSOR_COMFOCLIME_DEVICE_MODE: Sensor("ComfoClime Device Mode", None, 4149, PdoType.TYPE_CN_UINT8),
|
|
206
|
+
SENSOR_COMFOCLIME_COMFORT_TEMPERATURE: Sensor("ComfoClime Comfort Temperature", UNIT_CELCIUS, 4151, PdoType.TYPE_CN_INT16, lambda x: x / 10),
|
|
207
|
+
SENSOR_COMFOCLIME_INDOOR_TEMPERATURE: Sensor("ComfoClime Indoor Temperature", UNIT_CELCIUS, 4154, PdoType.TYPE_CN_INT16, lambda x: x / 10),
|
|
208
|
+
SENSOR_COMFOCLIME_TEMPERATURE_SUPPLY: Sensor("ComfoClime Supply Air Temperature", UNIT_CELCIUS, 4193, PdoType.TYPE_CN_INT16, lambda x: x / 10),
|
|
209
|
+
SENSOR_COMFOCLIME_TEMPERATURE_EXHAUST: Sensor("ComfoClime Exhaust Air Temperature", UNIT_CELCIUS, 4194, PdoType.TYPE_CN_INT16, lambda x: x / 10),
|
|
210
|
+
SENSOR_COMFOCLIME_TEMPERATURE_SUPPLY_COIL: Sensor("ComfoClime Supply Coil Temperature", UNIT_CELCIUS, 4195, PdoType.TYPE_CN_INT16, lambda x: x / 10),
|
|
211
|
+
SENSOR_COMFOCLIME_TEMPERATURE_EXHAUST_COIL: Sensor("ComfoClime Exhaust Coil Temperature", UNIT_CELCIUS, 4196, PdoType.TYPE_CN_INT16, lambda x: x / 10),
|
|
212
|
+
SENSOR_COMFOCLIME_TEMPERATURE_COMPRESSOR: Sensor("ComfoClime Compressor Temperature", UNIT_CELCIUS, 4197, PdoType.TYPE_CN_INT16, lambda x: x / 10),
|
|
213
|
+
SENSOR_COMFOCLIME_HEATPUMP_DUTY: Sensor("ComfoClime Heat Pump Duty", UNIT_PERCENT, 4198, PdoType.TYPE_CN_UINT8),
|
|
214
|
+
SENSOR_COMFOCLIME_POWER_USAGE: Sensor("ComfoClime Power Usage", UNIT_WATT, 4201, PdoType.TYPE_CN_UINT16),
|
|
215
|
+
4202: Sensor("sensor_4202", None, 4202, PdoType.TYPE_CN_UINT16),
|
|
216
|
+
4203: Sensor("sensor_4203", None, 4203, PdoType.TYPE_CN_UINT16),
|
|
217
|
+
4204: Sensor("sensor_4204", None, 4204, PdoType.TYPE_CN_UINT16),
|
|
218
|
+
4205: Sensor("sensor_4205", None, 4205, PdoType.TYPE_CN_UINT16),
|
|
219
|
+
4206: Sensor("sensor_4206", None, 4206, PdoType.TYPE_CN_UINT16),
|
|
220
|
+
4207: Sensor("sensor_4207", None, 4207, PdoType.TYPE_CN_UINT16),
|
|
221
|
+
4208: Sensor("sensor_4208", None, 4208, PdoType.TYPE_CN_UINT16),
|
|
189
222
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "aiocomfoconnect"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.1"
|
|
4
4
|
description = "aiocomfoconnect is an asyncio Python 3 library for communicating with a Zehnder ComfoAir Q350/450/600 ventilation system"
|
|
5
5
|
authors = ["Michaël Arnauts <michael.arnauts@gmail.com>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -10,7 +10,7 @@ homepage = "https://github.com/michaelarnauts/aiocomfoconnect"
|
|
|
10
10
|
aiocomfoconnect = "aiocomfoconnect.__main__:main_cli"
|
|
11
11
|
|
|
12
12
|
[tool.poetry.dependencies]
|
|
13
|
-
python = "^3.
|
|
13
|
+
python = "^3.13"
|
|
14
14
|
aiohttp = "^3.8.0"
|
|
15
15
|
protobuf = ">=6.31,<8"
|
|
16
16
|
|
|
@@ -20,8 +20,8 @@ dpkt = "^1.9.8"
|
|
|
20
20
|
black = "^24.4.0"
|
|
21
21
|
isort = "^5.10.1"
|
|
22
22
|
pylint = "^3.2"
|
|
23
|
-
pytest = "^8.
|
|
24
|
-
pytest-asyncio = "^
|
|
23
|
+
pytest = "^8.4"
|
|
24
|
+
pytest-asyncio = "^1.4.0"
|
|
25
25
|
|
|
26
26
|
[build-system]
|
|
27
27
|
requires = ["poetry-core"]
|
|
@@ -31,7 +31,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
31
31
|
profile = "black"
|
|
32
32
|
|
|
33
33
|
[tool.black]
|
|
34
|
-
target-version = ["
|
|
34
|
+
target-version = ["py313"]
|
|
35
35
|
line-length = 180
|
|
36
36
|
|
|
37
37
|
[tool.pylint.MAIN]
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
""" Property definitions """
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from dataclasses import dataclass
|
|
6
|
-
|
|
7
|
-
from .const import UNIT_NODE, UNIT_NODECONFIGURATION, UNIT_TEMPHUMCONTROL, PdoType
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@dataclass
|
|
11
|
-
class Property:
|
|
12
|
-
"""Dataclass for a Property"""
|
|
13
|
-
|
|
14
|
-
unit: int
|
|
15
|
-
subunit: int
|
|
16
|
-
property_id: int
|
|
17
|
-
property_type: int
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
PROPERTY_SERIAL_NUMBER = Property(UNIT_NODE, 0x01, 0x04, PdoType.TYPE_CN_STRING)
|
|
21
|
-
PROPERTY_FIRMWARE_VERSION = Property(UNIT_NODE, 0x01, 0x06, PdoType.TYPE_CN_UINT32)
|
|
22
|
-
PROPERTY_MODEL = Property(UNIT_NODE, 0x01, 0x08, PdoType.TYPE_CN_STRING)
|
|
23
|
-
PROPERTY_ARTICLE = Property(UNIT_NODE, 0x01, 0x0B, PdoType.TYPE_CN_STRING)
|
|
24
|
-
PROPERTY_COUNTRY = Property(UNIT_NODE, 0x01, 0x0D, PdoType.TYPE_CN_STRING)
|
|
25
|
-
PROPERTY_NAME = Property(UNIT_NODE, 0x01, 0x14, PdoType.TYPE_CN_STRING)
|
|
26
|
-
|
|
27
|
-
PROPERTY_MAINTAINER_PASSWORD = Property(UNIT_NODECONFIGURATION, 0x01, 0x03, PdoType.TYPE_CN_STRING)
|
|
28
|
-
|
|
29
|
-
PROPERTY_SENSOR_VENTILATION_TEMP_PASSIVE = Property(UNIT_TEMPHUMCONTROL, 0x01, 0x04, PdoType.TYPE_CN_UINT32)
|
|
30
|
-
PROPERTY_SENSOR_VENTILATION_HUMIDITY_COMFORT = Property(UNIT_TEMPHUMCONTROL, 0x01, 0x06, PdoType.TYPE_CN_UINT32)
|
|
31
|
-
PROPERTY_SENSOR_VENTILATION_HUMIDITY_PROTECTION = Property(UNIT_TEMPHUMCONTROL, 0x01, 0x07, PdoType.TYPE_CN_UINT32)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|