aiocomfoconnect 0.2.0__tar.gz → 0.3.0__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.0
3
+ Version: 0.3.0
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.
@@ -13,9 +13,10 @@ 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, PdoType
17
17
  from .exceptions import (
18
18
  AioComfoConnectNotConnected,
19
+ AioComfoConnectNotReachable,
19
20
  AioComfoConnectTimeout,
20
21
  ComfoConnectBadRequest,
21
22
  ComfoConnectError,
@@ -133,6 +134,7 @@ class Bridge:
133
134
  self._ventilation_node_found: Optional[asyncio.Event] = None
134
135
 
135
136
  self.__sensor_callback_fn: Optional[Callable[[int, int], None]] = None
137
+ self._sensor_pdo_types: Dict[int, int] = {}
136
138
  self.__alarm_callback_fn: Optional[Callable[[int, ProtobufMessage], None]] = None
137
139
 
138
140
  self._loop: Optional[asyncio.AbstractEventLoop] = loop
@@ -189,8 +191,13 @@ class Bridge:
189
191
  try:
190
192
  self._reader, self._writer = await asyncio.wait_for(asyncio.open_connection(self.host, self.PORT), TIMEOUT)
191
193
  except asyncio.TimeoutError as exc:
194
+ # Keep this before OSError, since TimeoutError is a subclass of it.
192
195
  _LOGGER.warning("Timeout while connecting to bridge %s", self.host)
193
196
  raise AioComfoConnectTimeout("Timeout while connecting to bridge") from exc
197
+ except OSError as exc:
198
+ # The bridge refused the connection, is gone from the network, or its hostname doesn't resolve.
199
+ _LOGGER.warning("Could not connect to bridge %s: %s", self.host, exc)
200
+ raise AioComfoConnectNotReachable(f"Could not connect to bridge: {exc}") from exc
194
201
 
195
202
  self._reference = itertools.count(1)
196
203
  self._local_uuid = uuid
@@ -418,7 +425,9 @@ class Bridge:
418
425
  # pylint: disable=no-member
419
426
  if message.cmd.type == zehnder_pb2.GatewayOperation.CnRpdoNotificationType:
420
427
  if self.__sensor_callback_fn:
421
- self.__sensor_callback_fn(message.msg.pdid, int.from_bytes(message.msg.data, byteorder="little", signed=True))
428
+ pdo_type = self._sensor_pdo_types.get(message.msg.pdid)
429
+ signed = pdo_type not in (PdoType.TYPE_CN_UINT8, PdoType.TYPE_CN_UINT16, PdoType.TYPE_CN_UINT32)
430
+ self.__sensor_callback_fn(message.msg.pdid, int.from_bytes(message.msg.data, byteorder="little", signed=signed))
422
431
  else:
423
432
  _LOGGER.info("Unhandled CnRpdoNotificationType since no callback is registered.")
424
433
 
@@ -566,6 +575,8 @@ class Bridge:
566
575
  def cmd_rpdo_request(self, pdid: int, pdo_type: int = 1, zone: int = 1, timeout=None) -> Awaitable[Message]:
567
576
  """Register a RPDO request."""
568
577
  _LOGGER.debug("CnRpdoRequest")
578
+ # Notifications can arrive before the subscription acknowledgement.
579
+ self._sensor_pdo_types[pdid] = pdo_type
569
580
  # pylint: disable=no-member
570
581
  return self._send(
571
582
  zehnder_pb2.CnRpdoRequest,
@@ -31,6 +31,7 @@ from aiocomfoconnect.const import (
31
31
  )
32
32
  from aiocomfoconnect.exceptions import (
33
33
  AioComfoConnectNotConnected,
34
+ AioComfoConnectNotReachable,
34
35
  AioComfoConnectTimeout,
35
36
  ComfoConnectNotAllowed,
36
37
  VentilationUnitNotFoundException,
@@ -180,6 +181,10 @@ class ComfoConnect(Bridge):
180
181
  _LOGGER.warning("Connection timeout, retrying in 5 seconds...")
181
182
  await asyncio.sleep(5)
182
183
 
184
+ except AioComfoConnectNotReachable as exc:
185
+ _LOGGER.warning("%s. Retrying in 5 seconds...", exc)
186
+ await asyncio.sleep(5)
187
+
183
188
  except VentilationUnitNotFoundException as exc:
184
189
  _LOGGER.warning("%s, retrying in 5 seconds...", exc)
185
190
  await asyncio.sleep(5)
@@ -296,9 +301,14 @@ class ComfoConnect(Bridge):
296
301
 
297
302
  self._sensors_values[sensor_id] = sensor_value
298
303
 
299
- # Don't emit sensor values until we have received all the initial values.
304
+ # The bridge sends invalid (zero) values right after we subscribe to a sensor, so we hold them back.
305
+ # A non-zero value is a real value, so we can release the hold and emit it right away.
300
306
  if sensor_id in self._sensor_holds:
301
- return
307
+ if not sensor_value:
308
+ return
309
+
310
+ _LOGGER.debug("Got a valid value for sensor %s, releasing the hold", sensor_id)
311
+ self._sensor_holds.pop(sensor_id).cancel()
302
312
 
303
313
  if sensor.value_fn:
304
314
  val = sensor.value_fn(sensor_value)
@@ -330,9 +340,10 @@ class ComfoConnect(Bridge):
330
340
  return VentilationMode.MANUAL if mode == 1 else VentilationMode.AUTO
331
341
 
332
342
  async def set_mode(self, mode: Literal["auto", "manual"]):
333
- """Set the ventilation mode (auto / manual)."""
343
+ """Set ventilation mode, cancelling the temporary speed override for auto."""
334
344
  if mode == VentilationMode.AUTO:
335
345
  await self.cmd_rmi_request(bytes([0x85, UNIT_SCHEDULE, SUBUNIT_08, 0x01]))
346
+ await self.cmd_rmi_request(bytes([0x85, UNIT_SCHEDULE, SUBUNIT_01, 0x01]))
336
347
  elif mode == VentilationMode.MANUAL:
337
348
  await self.cmd_rmi_request(bytes([0x84, UNIT_SCHEDULE, SUBUNIT_08, 0x01, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01]))
338
349
  else:
@@ -50,6 +50,10 @@ class AioComfoConnectTimeout(Exception):
50
50
  """An error occurred because the bridge didn't reply in time."""
51
51
 
52
52
 
53
+ class AioComfoConnectNotReachable(Exception):
54
+ """An error occurred because the bridge could not be reached, for example when it moved to another address."""
55
+
56
+
53
57
  class BridgeNotFoundException(Exception):
54
58
  """Exception raised when no bridge is found."""
55
59
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "aiocomfoconnect"
3
- version = "0.2.0"
3
+ version = "0.3.0"
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]
File without changes