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.
- {aiocomfoconnect-0.2.0 → aiocomfoconnect-0.3.0}/PKG-INFO +5 -7
- {aiocomfoconnect-0.2.0 → aiocomfoconnect-0.3.0}/README.md +2 -2
- {aiocomfoconnect-0.2.0 → aiocomfoconnect-0.3.0}/aiocomfoconnect/bridge.py +13 -2
- {aiocomfoconnect-0.2.0 → aiocomfoconnect-0.3.0}/aiocomfoconnect/comfoconnect.py +14 -3
- {aiocomfoconnect-0.2.0 → aiocomfoconnect-0.3.0}/aiocomfoconnect/exceptions.py +4 -0
- {aiocomfoconnect-0.2.0 → aiocomfoconnect-0.3.0}/pyproject.toml +5 -5
- {aiocomfoconnect-0.2.0 → aiocomfoconnect-0.3.0}/LICENSE +0 -0
- {aiocomfoconnect-0.2.0 → aiocomfoconnect-0.3.0}/aiocomfoconnect/__init__.py +0 -0
- {aiocomfoconnect-0.2.0 → aiocomfoconnect-0.3.0}/aiocomfoconnect/__main__.py +0 -0
- {aiocomfoconnect-0.2.0 → aiocomfoconnect-0.3.0}/aiocomfoconnect/const.py +0 -0
- {aiocomfoconnect-0.2.0 → aiocomfoconnect-0.3.0}/aiocomfoconnect/discovery.py +0 -0
- {aiocomfoconnect-0.2.0 → aiocomfoconnect-0.3.0}/aiocomfoconnect/properties.py +0 -0
- {aiocomfoconnect-0.2.0 → aiocomfoconnect-0.3.0}/aiocomfoconnect/protobuf/nanopb_pb2.py +0 -0
- {aiocomfoconnect-0.2.0 → aiocomfoconnect-0.3.0}/aiocomfoconnect/protobuf/zehnder_pb2.py +0 -0
- {aiocomfoconnect-0.2.0 → aiocomfoconnect-0.3.0}/aiocomfoconnect/sensors.py +0 -0
- {aiocomfoconnect-0.2.0 → aiocomfoconnect-0.3.0}/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.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.
|
|
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.
|
|
@@ -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.
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|