Skip to content

Commit be383cb

Browse files
implement get-flow-for-speed and set-flow-for-speed (#15)
* Implement get-flow-for-speed method in lib and CLI * reuse get_single_property * implement set-flow-for-speed, make PdoTypes enum * simplify TYPE_CN_BOOL conversion * Bump dependency to Python 3.10 so we can use match --------- Co-authored-by: Michaël Arnauts <[email protected]>
1 parent c8bf1a7 commit be383cb

File tree

9 files changed

+247
-133
lines changed

9 files changed

+247
-133
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ jobs:
2121
fail-fast: false
2222
matrix:
2323
os: [ ubuntu-latest ]
24-
python-version: [ "3.8", "3.9", "3.10", "3.11" ]
24+
python-version: [ "3.10", "3.11" ]
2525
steps:
2626
- name: Check out ${{ github.sha }} from repository ${{ github.repository }}
2727
uses: actions/checkout@v3

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
`aiocomfoconnect` is an asyncio Python 3 library for communicating with a Zehnder ComfoAir Q350/450/600 ventilation system. It's the successor of
44
[comfoconnect](https://github.com/michaelarnauts/comfoconnect).
55

6-
It's compatible with Python 3.8 and higher.
6+
It's compatible with Python 3.10 and higher.
77

88
## Installation
99

aiocomfoconnect/__main__.py

+57
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,12 @@ async def main(args):
5151
elif args.action == "get-property":
5252
await run_get_property(args.host, args.uuid, args.node_id, args.unit, args.subunit, args.property_id, args.property_type)
5353

54+
elif args.action == "get-flow-for-speed":
55+
await run_get_flow_for_speed(args.host, args.uuid, args.speed)
56+
57+
elif args.action == "set-flow-for-speed":
58+
await run_set_flow_for_speed(args.host, args.uuid, args.speed, args.flow)
59+
5460
else:
5561
raise Exception("Unknown action: " + args.action)
5662

@@ -310,6 +316,46 @@ async def run_get_property(host: str, uuid: str, node_id: int, unit: int, subuni
310316
await comfoconnect.disconnect()
311317

312318

319+
async def run_get_flow_for_speed(host: str, uuid: str, speed: Literal["away", "low", "medium", "high"]):
320+
"""Get the configured airflow for the specified speed."""
321+
# Discover bridge so we know the UUID
322+
bridges = await discover_bridges(host)
323+
if not bridges:
324+
raise Exception("No bridge found")
325+
326+
# Connect to the bridge
327+
comfoconnect = ComfoConnect(bridges[0].host, bridges[0].uuid)
328+
try:
329+
await comfoconnect.connect(uuid)
330+
except ComfoConnectNotAllowed:
331+
print("Could not connect to bridge. Please register first.")
332+
sys.exit(1)
333+
334+
print(await comfoconnect.get_flow_for_speed(speed))
335+
336+
await comfoconnect.disconnect()
337+
338+
339+
async def run_set_flow_for_speed(host: str, uuid: str, speed: Literal["away", "low", "medium", "high"], desired_flow: int):
340+
"""Set the configured airflow for the specified speed."""
341+
# Discover bridge so we know the UUID
342+
bridges = await discover_bridges(host)
343+
if not bridges:
344+
raise Exception("No bridge found")
345+
346+
# Connect to the bridge
347+
comfoconnect = ComfoConnect(bridges[0].host, bridges[0].uuid)
348+
try:
349+
await comfoconnect.connect(uuid)
350+
except ComfoConnectNotAllowed:
351+
print("Could not connect to bridge. Please register first.")
352+
sys.exit(1)
353+
354+
await comfoconnect.set_flow_for_speed(speed, desired_flow)
355+
356+
await comfoconnect.disconnect()
357+
358+
313359
if __name__ == "__main__":
314360
parser = argparse.ArgumentParser()
315361
parser.add_argument("--debug", "-d", help="Enable debug logging", default=False, action="store_true")
@@ -365,6 +411,17 @@ async def run_get_property(host: str, uuid: str, node_id: int, unit: int, subuni
365411
p_sensor.add_argument("--host", help="Host address of the bridge")
366412
p_sensor.add_argument("--uuid", help="UUID of this app", default=DEFAULT_UUID)
367413

414+
p_get_flow_speed = subparsers.add_parser("get-flow-for-speed", help="Get m³/h for given speed")
415+
p_get_flow_speed.add_argument("speed", help="Fan speed", choices=["low", "medium", "high", "away"])
416+
p_get_flow_speed.add_argument("--host", help="Host address of the bridge")
417+
p_get_flow_speed.add_argument("--uuid", help="UUID of this app", default=DEFAULT_UUID)
418+
419+
p_set_flow_speed = subparsers.add_parser("set-flow-for-speed", help="Set m³/h for given speed")
420+
p_set_flow_speed.add_argument("speed", help="Fan speed", choices=["low", "medium", "high", "away"])
421+
p_set_flow_speed.add_argument("flow", help="Desired airflow in m³/h", type=int)
422+
p_set_flow_speed.add_argument("--host", help="Host address of the bridge")
423+
p_set_flow_speed.add_argument("--uuid", help="UUID of this app", default=DEFAULT_UUID)
424+
368425
arguments = parser.parse_args()
369426

370427
if arguments.debug:

aiocomfoconnect/comfoconnect.py

+46-13
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,11 @@
1515
SUBUNIT_06,
1616
SUBUNIT_07,
1717
SUBUNIT_08,
18-
TYPE_CN_BOOL,
19-
TYPE_CN_INT8,
20-
TYPE_CN_INT16,
21-
TYPE_CN_INT64,
22-
TYPE_CN_STRING,
23-
TYPE_CN_UINT8,
24-
TYPE_CN_UINT16,
25-
TYPE_CN_UINT32,
18+
PdoType,
2619
UNIT_ERROR,
2720
UNIT_SCHEDULE,
2821
UNIT_TEMPHUMCONTROL,
22+
UNIT_VENTILATIONCONFIG,
2923
VentilationBalance,
3024
VentilationMode,
3125
VentilationSetting,
@@ -34,7 +28,7 @@
3428
)
3529
from aiocomfoconnect.properties import Property
3630
from aiocomfoconnect.sensors import Sensor
37-
from aiocomfoconnect.util import bytearray_to_bits, bytestring
31+
from aiocomfoconnect.util import bytearray_to_bits, bytestring, encode_pdo_value
3832

3933
_LOGGER = logging.getLogger(__name__)
4034

@@ -102,13 +96,13 @@ async def get_single_property(self, unit: int, subunit: int, property_id: int, p
10296
"""Get a property and convert to the right type."""
10397
result = await self.cmd_rmi_request(bytes([0x01, unit, subunit, 0x10, property_id]), node_id=node_id)
10498

105-
if property_type == TYPE_CN_STRING:
99+
if property_type == PdoType.TYPE_CN_STRING:
106100
return result.message.decode("utf-8").rstrip("\x00")
107-
if property_type in [TYPE_CN_INT8, TYPE_CN_INT16, TYPE_CN_INT64]:
101+
if property_type in [PdoType.TYPE_CN_INT8, PdoType.TYPE_CN_INT16, PdoType.TYPE_CN_INT64]:
108102
return int.from_bytes(result.message, byteorder="little", signed=True)
109-
if property_type in [TYPE_CN_UINT8, TYPE_CN_UINT16, TYPE_CN_UINT32]:
103+
if property_type in [PdoType.TYPE_CN_UINT8, PdoType.TYPE_CN_UINT16, PdoType.TYPE_CN_UINT32]:
110104
return int.from_bytes(result.message, byteorder="little", signed=False)
111-
if property_type in [TYPE_CN_BOOL]:
105+
if property_type in [PdoType.TYPE_CN_BOOL]:
112106
return result.message[0] == 1
113107

114108
return result.message
@@ -125,6 +119,15 @@ async def set_property(self, unit: int, subunit: int, property_id: int, value: i
125119

126120
return result.message
127121

122+
async def set_property_typed(self, unit: int, subunit: int, property_id: int, value: int, pdo_type: PdoType, node_id=1) -> any:
123+
"""Set a typed property."""
124+
value_bytes = encode_pdo_value(value, pdo_type)
125+
message_bytes = bytes([0x03, unit, subunit, property_id]) + value_bytes
126+
127+
result = await self.cmd_rmi_request(message_bytes, node_id=node_id)
128+
129+
return result.message
130+
128131
def _sensor_callback(self, sensor_id, sensor_value):
129132
"""Callback function for sensor updates."""
130133
if self._sensor_callback_fn is None:
@@ -212,6 +215,36 @@ async def set_speed(self, speed: Literal["away", "low", "medium", "high"]):
212215
else:
213216
raise ValueError(f"Invalid speed: {speed}")
214217

218+
async def get_flow_for_speed(self, speed: Literal["away", "low", "medium", "high"]) -> int:
219+
"""Get the targeted airflow in m³/h for the given VentilationSpeed (away / low / medium / high)."""
220+
221+
match speed:
222+
case VentilationSpeed.AWAY:
223+
property_id = 3
224+
case VentilationSpeed.LOW:
225+
property_id = 4
226+
case VentilationSpeed.MEDIUM:
227+
property_id = 5
228+
case VentilationSpeed.HIGH:
229+
property_id = 6
230+
231+
return await self.get_single_property(UNIT_VENTILATIONCONFIG, SUBUNIT_01, property_id, PdoType.TYPE_CN_INT16)
232+
233+
async def set_flow_for_speed(self, speed: Literal["away", "low", "medium", "high"], desired_flow: int):
234+
"""Set the targeted airflow in m³/h for the given VentilationSpeed (away / low / medium / high)."""
235+
236+
match speed:
237+
case VentilationSpeed.AWAY:
238+
property_id = 3
239+
case VentilationSpeed.LOW:
240+
property_id = 4
241+
case VentilationSpeed.MEDIUM:
242+
property_id = 5
243+
case VentilationSpeed.HIGH:
244+
property_id = 6
245+
246+
await self.set_property_typed(UNIT_VENTILATIONCONFIG, SUBUNIT_01, property_id, desired_flow, PdoType.TYPE_CN_INT16)
247+
215248
async def get_bypass(self):
216249
"""Get the bypass mode (auto / on / off)."""
217250
result = await self.cmd_rmi_request(bytes([0x83, UNIT_SCHEDULE, SUBUNIT_02, 0x01]))

aiocomfoconnect/const.py

+13-10
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
""" Constants """
22

33
# PDO Types
4-
TYPE_CN_BOOL = 0x00
5-
TYPE_CN_UINT8 = 0x01
6-
TYPE_CN_UINT16 = 0x02
7-
TYPE_CN_UINT32 = 0x03
8-
TYPE_CN_INT8 = 0x05
9-
TYPE_CN_INT16 = 0x06
10-
TYPE_CN_INT64 = 0x08
11-
TYPE_CN_STRING = 0x09
12-
TYPE_CN_TIME = 0x10
13-
TYPE_CN_VERSION = 0x11
4+
class PdoType:
5+
"""Defines a PDO type."""
6+
7+
TYPE_CN_BOOL = 0x00
8+
TYPE_CN_UINT8 = 0x01
9+
TYPE_CN_UINT16 = 0x02
10+
TYPE_CN_UINT32 = 0x03
11+
TYPE_CN_INT8 = 0x05
12+
TYPE_CN_INT16 = 0x06
13+
TYPE_CN_INT64 = 0x08
14+
TYPE_CN_STRING = 0x09
15+
TYPE_CN_TIME = 0x10
16+
TYPE_CN_VERSION = 0x11
1417

1518

1619
# ComfoConnect Units

aiocomfoconnect/properties.py

+11-12
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@
44
from dataclasses import dataclass
55

66
from .const import (
7-
TYPE_CN_STRING,
8-
TYPE_CN_UINT32,
7+
PdoType,
98
UNIT_NODE,
109
UNIT_NODECONFIGURATION,
1110
UNIT_TEMPHUMCONTROL,
@@ -22,15 +21,15 @@ class Property:
2221
property_type: int
2322

2423

25-
PROPERTY_SERIAL_NUMBER = Property(UNIT_NODE, 0x01, 0x04, TYPE_CN_STRING)
26-
PROPERTY_FIRMWARE_VERSION = Property(UNIT_NODE, 0x01, 0x06, TYPE_CN_UINT32)
27-
PROPERTY_MODEL = Property(UNIT_NODE, 0x01, 0x08, TYPE_CN_STRING)
28-
PROPERTY_ARTICLE = Property(UNIT_NODE, 0x01, 0x0B, TYPE_CN_STRING)
29-
PROPERTY_COUNTRY = Property(UNIT_NODE, 0x01, 0x0D, TYPE_CN_STRING)
30-
PROPERTY_NAME = Property(UNIT_NODE, 0x01, 0x14, TYPE_CN_STRING)
24+
PROPERTY_SERIAL_NUMBER = Property(UNIT_NODE, 0x01, 0x04, PdoType.TYPE_CN_STRING)
25+
PROPERTY_FIRMWARE_VERSION = Property(UNIT_NODE, 0x01, 0x06, PdoType.TYPE_CN_UINT32)
26+
PROPERTY_MODEL = Property(UNIT_NODE, 0x01, 0x08, PdoType.TYPE_CN_STRING)
27+
PROPERTY_ARTICLE = Property(UNIT_NODE, 0x01, 0x0B, PdoType.TYPE_CN_STRING)
28+
PROPERTY_COUNTRY = Property(UNIT_NODE, 0x01, 0x0D, PdoType.TYPE_CN_STRING)
29+
PROPERTY_NAME = Property(UNIT_NODE, 0x01, 0x14, PdoType.TYPE_CN_STRING)
3130

32-
PROPERTY_MAINTAINER_PASSWORD = Property(UNIT_NODECONFIGURATION, 0x01, 0x03, TYPE_CN_STRING)
31+
PROPERTY_MAINTAINER_PASSWORD = Property(UNIT_NODECONFIGURATION, 0x01, 0x03, PdoType.TYPE_CN_STRING)
3332

34-
PROPERTY_SENSOR_VENTILATION_TEMP_PASSIVE = Property(UNIT_TEMPHUMCONTROL, 0x01, 0x04, TYPE_CN_UINT32)
35-
PROPERTY_SENSOR_VENTILATION_HUMIDITY_COMFORT = Property(UNIT_TEMPHUMCONTROL, 0x01, 0x06, TYPE_CN_UINT32)
36-
PROPERTY_SENSOR_VENTILATION_HUMIDITY_PROTECTION = Property(UNIT_TEMPHUMCONTROL, 0x01, 0x07, TYPE_CN_UINT32)
33+
PROPERTY_SENSOR_VENTILATION_TEMP_PASSIVE = Property(UNIT_TEMPHUMCONTROL, 0x01, 0x04, PdoType.TYPE_CN_UINT32)
34+
PROPERTY_SENSOR_VENTILATION_HUMIDITY_COMFORT = Property(UNIT_TEMPHUMCONTROL, 0x01, 0x06, PdoType.TYPE_CN_UINT32)
35+
PROPERTY_SENSOR_VENTILATION_HUMIDITY_PROTECTION = Property(UNIT_TEMPHUMCONTROL, 0x01, 0x07, PdoType.TYPE_CN_UINT32)

0 commit comments

Comments
 (0)