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.
@@ -1,17 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aiocomfoconnect
3
- Version: 0.2.1
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.10,<4.0
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.10 and higher.
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.10 and higher.
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.__sensor_callback_fn(message.msg.pdid, int.from_bytes(message.msg.data, byteorder="little", signed=True))
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
- # Don't emit sensor values until we have received all the initial values.
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
- return
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 the ventilation mode (auto / manual)."""
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: "celcius" if x == 0 else "farenheit"),
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: "m3ph" if x == 3 else "lps"),
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.2.1"
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.10"
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.2.0"
24
- pytest-asyncio = "^0.23.0"
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 = ["py310"]
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