ramses-rf 0.51.7__py3-none-any.whl → 0.51.9__py3-none-any.whl
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.
- ramses_rf/__init__.py +5 -0
- ramses_rf/database.py +247 -69
- ramses_rf/device/hvac.py +561 -32
- ramses_rf/dispatcher.py +7 -5
- ramses_rf/entity_base.py +1 -1
- ramses_rf/exceptions.py +37 -3
- ramses_rf/gateway.py +1 -1
- ramses_rf/schemas.py +5 -2
- ramses_rf/version.py +1 -1
- {ramses_rf-0.51.7.dist-info → ramses_rf-0.51.9.dist-info}/METADATA +6 -6
- ramses_rf-0.51.9.dist-info/RECORD +55 -0
- ramses_tx/__init__.py +25 -4
- ramses_tx/address.py +1 -1
- ramses_tx/command.py +1449 -138
- ramses_tx/const.py +1 -1
- ramses_tx/frame.py +4 -4
- ramses_tx/gateway.py +1 -1
- ramses_tx/helpers.py +2 -2
- ramses_tx/message.py +20 -14
- ramses_tx/packet.py +1 -1
- ramses_tx/parsers.py +57 -35
- ramses_tx/protocol.py +2 -2
- ramses_tx/protocol_fsm.py +1 -1
- ramses_tx/ramses.py +46 -6
- ramses_tx/schemas.py +3 -0
- ramses_tx/transport.py +9 -7
- ramses_tx/version.py +1 -1
- ramses_rf-0.51.7.dist-info/RECORD +0 -55
- {ramses_rf-0.51.7.dist-info → ramses_rf-0.51.9.dist-info}/WHEEL +0 -0
- {ramses_rf-0.51.7.dist-info → ramses_rf-0.51.9.dist-info}/entry_points.txt +0 -0
- {ramses_rf-0.51.7.dist-info → ramses_rf-0.51.9.dist-info}/licenses/LICENSE +0 -0
ramses_rf/device/hvac.py
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
import logging
|
|
7
|
+
from collections.abc import Callable
|
|
7
8
|
from typing import Any, TypeVar
|
|
8
9
|
|
|
9
10
|
from ramses_rf import exceptions as exc
|
|
@@ -41,8 +42,6 @@ from ramses_rf.const import (
|
|
|
41
42
|
DevType,
|
|
42
43
|
)
|
|
43
44
|
from ramses_rf.entity_base import class_by_attr
|
|
44
|
-
from ramses_rf.helpers import shrink
|
|
45
|
-
from ramses_rf.schemas import SCH_VCS, SZ_REMOTES, SZ_SENSORS
|
|
46
45
|
from ramses_tx import Address, Command, Message, Packet, Priority
|
|
47
46
|
from ramses_tx.ramses import CODES_OF_HVAC_DOMAIN_ONLY, HVAC_KLASS_BY_VC_PAIR
|
|
48
47
|
|
|
@@ -76,10 +75,22 @@ _HvacSensorBaseT = TypeVar("_HvacSensorBaseT", bound="HvacSensorBase")
|
|
|
76
75
|
|
|
77
76
|
|
|
78
77
|
class HvacRemoteBase(DeviceHvac):
|
|
78
|
+
"""Base class for HVAC remote control devices.
|
|
79
|
+
|
|
80
|
+
This class serves as a base for all remote control devices in the HVAC domain.
|
|
81
|
+
It provides common functionality and interfaces for remote control operations.
|
|
82
|
+
"""
|
|
83
|
+
|
|
79
84
|
pass
|
|
80
85
|
|
|
81
86
|
|
|
82
87
|
class HvacSensorBase(DeviceHvac):
|
|
88
|
+
"""Base class for HVAC sensor devices.
|
|
89
|
+
|
|
90
|
+
This class serves as a base for all sensor devices in the HVAC domain.
|
|
91
|
+
It provides common functionality for sensor data collection and processing.
|
|
92
|
+
"""
|
|
93
|
+
|
|
83
94
|
pass
|
|
84
95
|
|
|
85
96
|
|
|
@@ -87,12 +98,22 @@ class CarbonDioxide(HvacSensorBase): # 1298
|
|
|
87
98
|
"""The CO2 sensor (cardinal code is 1298)."""
|
|
88
99
|
|
|
89
100
|
@property
|
|
90
|
-
def co2_level(self) -> int | None:
|
|
101
|
+
def co2_level(self) -> int | None:
|
|
102
|
+
"""Get the CO2 level in ppm.
|
|
103
|
+
|
|
104
|
+
:return: The CO2 level in parts per million (ppm), or None if not available
|
|
105
|
+
:rtype: int | None
|
|
106
|
+
"""
|
|
91
107
|
return self._msg_value(Code._1298, key=SZ_CO2_LEVEL)
|
|
92
108
|
|
|
93
109
|
@co2_level.setter
|
|
94
110
|
def co2_level(self, value: int | None) -> None:
|
|
95
|
-
"""
|
|
111
|
+
"""Set a fake CO2 level for the sensor.
|
|
112
|
+
|
|
113
|
+
:param value: The CO2 level in ppm to set, or None to clear the fake value
|
|
114
|
+
:type value: int | None
|
|
115
|
+
:raises TypeError: If the sensor is not in faked mode
|
|
116
|
+
"""
|
|
96
117
|
|
|
97
118
|
if not self.is_faked:
|
|
98
119
|
raise exc.DeviceNotFaked(f"{self}: Faking is not enabled")
|
|
@@ -102,6 +123,11 @@ class CarbonDioxide(HvacSensorBase): # 1298
|
|
|
102
123
|
|
|
103
124
|
@property
|
|
104
125
|
def status(self) -> dict[str, Any]:
|
|
126
|
+
"""Return the status of the CO2 sensor.
|
|
127
|
+
|
|
128
|
+
:return: A dictionary containing the sensor's status including CO2 level
|
|
129
|
+
:rtype: dict[str, Any]
|
|
130
|
+
"""
|
|
105
131
|
return {
|
|
106
132
|
**super().status,
|
|
107
133
|
SZ_CO2_LEVEL: self.co2_level,
|
|
@@ -112,12 +138,22 @@ class IndoorHumidity(HvacSensorBase): # 12A0
|
|
|
112
138
|
"""The relative humidity sensor (12A0)."""
|
|
113
139
|
|
|
114
140
|
@property
|
|
115
|
-
def indoor_humidity(self) -> float | None:
|
|
141
|
+
def indoor_humidity(self) -> float | None:
|
|
142
|
+
"""Get the indoor relative humidity.
|
|
143
|
+
|
|
144
|
+
:return: The indoor relative humidity as a percentage (0-100), or None if not available
|
|
145
|
+
:rtype: float | None
|
|
146
|
+
"""
|
|
116
147
|
return self._msg_value(Code._12A0, key=SZ_INDOOR_HUMIDITY)
|
|
117
148
|
|
|
118
149
|
@indoor_humidity.setter
|
|
119
150
|
def indoor_humidity(self, value: float | None) -> None:
|
|
120
|
-
"""
|
|
151
|
+
"""Set a fake indoor humidity value for the sensor.
|
|
152
|
+
|
|
153
|
+
:param value: The humidity percentage to set (0-100), or None to clear the fake value
|
|
154
|
+
:type value: float | None
|
|
155
|
+
:raises TypeError: If the sensor is not in faked mode
|
|
156
|
+
"""
|
|
121
157
|
|
|
122
158
|
if not self.is_faked:
|
|
123
159
|
raise exc.DeviceNotFaked(f"{self}: Faking is not enabled")
|
|
@@ -127,6 +163,11 @@ class IndoorHumidity(HvacSensorBase): # 12A0
|
|
|
127
163
|
|
|
128
164
|
@property
|
|
129
165
|
def status(self) -> dict[str, Any]:
|
|
166
|
+
"""Return the status of the indoor humidity sensor.
|
|
167
|
+
|
|
168
|
+
:return: A dictionary containing the sensor's status including humidity level
|
|
169
|
+
:rtype: dict[str, Any]
|
|
170
|
+
"""
|
|
130
171
|
return {
|
|
131
172
|
**super().status,
|
|
132
173
|
SZ_INDOOR_HUMIDITY: self.indoor_humidity,
|
|
@@ -142,11 +183,21 @@ class PresenceDetect(HvacSensorBase): # 2E10
|
|
|
142
183
|
|
|
143
184
|
@property
|
|
144
185
|
def presence_detected(self) -> bool | None:
|
|
186
|
+
"""Get the presence detection status.
|
|
187
|
+
|
|
188
|
+
:return: True if presence is detected, False if not, None if status is unknown
|
|
189
|
+
:rtype: bool | None
|
|
190
|
+
"""
|
|
145
191
|
return self._msg_value(Code._2E10, key=SZ_PRESENCE_DETECTED)
|
|
146
192
|
|
|
147
193
|
@presence_detected.setter
|
|
148
194
|
def presence_detected(self, value: bool | None) -> None:
|
|
149
|
-
"""
|
|
195
|
+
"""Set a fake presence detection state for the sensor.
|
|
196
|
+
|
|
197
|
+
:param value: The presence state to set (True/False), or None to clear the fake value
|
|
198
|
+
:type value: bool | None
|
|
199
|
+
:raises TypeError: If the sensor is not in faked mode
|
|
200
|
+
"""
|
|
150
201
|
|
|
151
202
|
if not self.is_faked:
|
|
152
203
|
raise exc.DeviceNotFaked(f"{self}: Faking is not enabled")
|
|
@@ -156,6 +207,11 @@ class PresenceDetect(HvacSensorBase): # 2E10
|
|
|
156
207
|
|
|
157
208
|
@property
|
|
158
209
|
def status(self) -> dict[str, Any]:
|
|
210
|
+
"""Return the status of the presence sensor.
|
|
211
|
+
|
|
212
|
+
:return: A dictionary containing the sensor's status including presence detection state
|
|
213
|
+
:rtype: dict[str, Any]
|
|
214
|
+
"""
|
|
159
215
|
return {
|
|
160
216
|
**super().status,
|
|
161
217
|
SZ_PRESENCE_DETECTED: self.presence_detected,
|
|
@@ -166,6 +222,7 @@ class FilterChange(DeviceHvac): # FAN: 10D0
|
|
|
166
222
|
"""The filter state sensor (10D0)."""
|
|
167
223
|
|
|
168
224
|
def _setup_discovery_cmds(self) -> None:
|
|
225
|
+
"""Set up the discovery commands for the filter change sensor."""
|
|
169
226
|
super()._setup_discovery_cmds()
|
|
170
227
|
|
|
171
228
|
self._add_discovery_cmd(
|
|
@@ -174,12 +231,22 @@ class FilterChange(DeviceHvac): # FAN: 10D0
|
|
|
174
231
|
|
|
175
232
|
@property
|
|
176
233
|
def filter_remaining(self) -> int | None:
|
|
234
|
+
"""Return the remaining days until filter change is needed.
|
|
235
|
+
|
|
236
|
+
:return: Number of days remaining until filter change, or None if not available
|
|
237
|
+
:rtype: int | None
|
|
238
|
+
"""
|
|
177
239
|
_val = self._msg_value(Code._10D0, key=SZ_REMAINING_DAYS)
|
|
178
240
|
assert isinstance(_val, (int | type(None)))
|
|
179
241
|
return _val
|
|
180
242
|
|
|
181
243
|
@property
|
|
182
244
|
def filter_remaining_percent(self) -> float | None:
|
|
245
|
+
"""Return the remaining filter life as a percentage.
|
|
246
|
+
|
|
247
|
+
:return: Percentage of filter life remaining (0-100), or None if not available
|
|
248
|
+
:rtype: float | None
|
|
249
|
+
"""
|
|
183
250
|
_val = self._msg_value(Code._10D0, key=SZ_REMAINING_PERCENT)
|
|
184
251
|
assert isinstance(_val, (float | type(None)))
|
|
185
252
|
return _val
|
|
@@ -191,6 +258,11 @@ class RfsGateway(DeviceHvac): # RFS: (spIDer gateway)
|
|
|
191
258
|
_SLUG: str = DevType.RFS
|
|
192
259
|
|
|
193
260
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
261
|
+
"""Initialize the RFS gateway.
|
|
262
|
+
|
|
263
|
+
:param args: Positional arguments passed to the parent class
|
|
264
|
+
:param kwargs: Keyword arguments passed to the parent class
|
|
265
|
+
"""
|
|
194
266
|
super().__init__(*args, **kwargs)
|
|
195
267
|
|
|
196
268
|
self.ctl = None
|
|
@@ -207,15 +279,30 @@ class HvacHumiditySensor(BatteryState, IndoorHumidity, Fakeable): # HUM: I/12A0
|
|
|
207
279
|
_SLUG: str = DevType.HUM
|
|
208
280
|
|
|
209
281
|
@property
|
|
210
|
-
def temperature(self) -> float | None:
|
|
282
|
+
def temperature(self) -> float | None:
|
|
283
|
+
"""Return the current temperature in Celsius.
|
|
284
|
+
|
|
285
|
+
:return: The temperature in degrees Celsius, or None if not available
|
|
286
|
+
:rtype: float | None
|
|
287
|
+
"""
|
|
211
288
|
return self._msg_value(Code._12A0, key=SZ_TEMPERATURE)
|
|
212
289
|
|
|
213
290
|
@property
|
|
214
|
-
def dewpoint_temp(self) -> float | None:
|
|
291
|
+
def dewpoint_temp(self) -> float | None:
|
|
292
|
+
"""Return the dewpoint temperature in Celsius.
|
|
293
|
+
|
|
294
|
+
:return: The dewpoint temperature in degrees Celsius, or None if not available
|
|
295
|
+
:rtype: float | None
|
|
296
|
+
"""
|
|
215
297
|
return self._msg_value(Code._12A0, key="dewpoint_temp")
|
|
216
298
|
|
|
217
299
|
@property
|
|
218
300
|
def status(self) -> dict[str, Any]:
|
|
301
|
+
"""Return the status of the humidity sensor.
|
|
302
|
+
|
|
303
|
+
:return: A dictionary containing the sensor's status including temperature and humidity
|
|
304
|
+
:rtype: dict[str, Any]
|
|
305
|
+
"""
|
|
219
306
|
return {
|
|
220
307
|
**super().status,
|
|
221
308
|
SZ_TEMPERATURE: self.temperature,
|
|
@@ -236,6 +323,12 @@ class HvacCarbonDioxideSensor(CarbonDioxide, Fakeable): # CO2: I/1298
|
|
|
236
323
|
# .I --- 29:181813 32:155617 --:------ 1FC9 001 00
|
|
237
324
|
|
|
238
325
|
async def initiate_binding_process(self) -> Packet:
|
|
326
|
+
"""Initiate the binding process for the CO2 sensor.
|
|
327
|
+
|
|
328
|
+
:return: The packet sent to initiate binding
|
|
329
|
+
:rtype: Packet
|
|
330
|
+
:raises exc.BindingError: If binding fails
|
|
331
|
+
"""
|
|
239
332
|
return await super()._initiate_binding_process(
|
|
240
333
|
(Code._31E0, Code._1298, Code._2E10)
|
|
241
334
|
)
|
|
@@ -259,13 +352,24 @@ class HvacRemote(BatteryState, Fakeable, HvacRemoteBase): # REM: I/22F[138]
|
|
|
259
352
|
)
|
|
260
353
|
|
|
261
354
|
@property
|
|
262
|
-
def fan_rate(self) -> str | None:
|
|
263
|
-
|
|
355
|
+
def fan_rate(self) -> str | None:
|
|
356
|
+
"""Get the current fan rate setting.
|
|
357
|
+
|
|
358
|
+
:return: The fan rate as a string, or None if not available
|
|
359
|
+
:rtype: str | None
|
|
360
|
+
:note: This is a work in progress - rate can be either int or str
|
|
361
|
+
"""
|
|
264
362
|
return self._msg_value(Code._22F1, key="rate")
|
|
265
363
|
|
|
266
364
|
@fan_rate.setter
|
|
267
|
-
def fan_rate(self, value: int) -> None:
|
|
268
|
-
"""
|
|
365
|
+
def fan_rate(self, value: int) -> None:
|
|
366
|
+
"""Set a fake fan rate for the remote control.
|
|
367
|
+
|
|
368
|
+
:param value: The fan rate to set (can be int or str, but not None)
|
|
369
|
+
:type value: int
|
|
370
|
+
:raises TypeError: If the remote is not in faked mode
|
|
371
|
+
:note: This is a work in progress
|
|
372
|
+
"""
|
|
269
373
|
|
|
270
374
|
if not self.is_faked: # NOTE: some remotes are stateless (i.e. except seqn)
|
|
271
375
|
raise exc.DeviceNotFaked(f"{self}: Faking is not enabled")
|
|
@@ -278,10 +382,20 @@ class HvacRemote(BatteryState, Fakeable, HvacRemoteBase): # REM: I/22F[138]
|
|
|
278
382
|
|
|
279
383
|
@property
|
|
280
384
|
def fan_mode(self) -> str | None:
|
|
385
|
+
"""Return the current fan mode.
|
|
386
|
+
|
|
387
|
+
:return: The fan mode as a string, or None if not available
|
|
388
|
+
:rtype: str | None
|
|
389
|
+
"""
|
|
281
390
|
return self._msg_value(Code._22F1, key=SZ_FAN_MODE)
|
|
282
391
|
|
|
283
392
|
@property
|
|
284
393
|
def boost_timer(self) -> int | None:
|
|
394
|
+
"""Return the remaining boost timer in minutes.
|
|
395
|
+
|
|
396
|
+
:return: The remaining boost time in minutes, or None if boost is not active
|
|
397
|
+
:rtype: int | None
|
|
398
|
+
"""
|
|
285
399
|
return self._msg_value(Code._22F3, key=SZ_BOOST_TIMER)
|
|
286
400
|
|
|
287
401
|
@property
|
|
@@ -304,10 +418,15 @@ class HvacDisplayRemote(HvacRemote): # DIS
|
|
|
304
418
|
# )
|
|
305
419
|
|
|
306
420
|
|
|
307
|
-
class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A]
|
|
421
|
+
class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A], 2411
|
|
308
422
|
"""The FAN (ventilation) class.
|
|
309
423
|
|
|
310
424
|
The cardinal codes are 31D9, 31DA. Signature is RP/31DA.
|
|
425
|
+
|
|
426
|
+
Also handles 2411 parameter messages for configuration.
|
|
427
|
+
Since 2411 is not supported by all vendors, discovery is used to determine if it is supported.
|
|
428
|
+
Since more than 1 different parameters can be sent on 2411 messages,
|
|
429
|
+
we will process these in the dedicated _handle_2411_message method.
|
|
311
430
|
"""
|
|
312
431
|
|
|
313
432
|
# Itho Daalderop (NL)
|
|
@@ -319,24 +438,249 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A]
|
|
|
319
438
|
|
|
320
439
|
_SLUG: str = DevType.FAN
|
|
321
440
|
|
|
322
|
-
def
|
|
323
|
-
|
|
441
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
442
|
+
"""Initialize the HvacVentilator.
|
|
443
|
+
|
|
444
|
+
:param args: Positional arguments passed to the parent class
|
|
445
|
+
:param kwargs: Keyword arguments passed to the parent class
|
|
446
|
+
"""
|
|
447
|
+
super().__init__(*args, **kwargs)
|
|
448
|
+
self._supports_2411 = False # Flag for 2411 parameter support
|
|
449
|
+
self._params_2411: dict[str, float] = {} # Store 2411 parameters here
|
|
450
|
+
self._initialized_callback = None # Called when device is fully initialized
|
|
451
|
+
self._param_update_callback = None # Called when 2411 parameters are updated
|
|
452
|
+
self._hgi: Any | None = None # Will be set when HGI is available
|
|
453
|
+
self._bound_devices: dict[str, str] = {} # Track bound devices (e.g., REM/DIS)
|
|
454
|
+
|
|
455
|
+
def set_initialized_callback(self, callback: Callable[[], None] | None) -> None:
|
|
456
|
+
"""Set a callback to be executed when the next message (any) is received.
|
|
457
|
+
|
|
458
|
+
The callback will be used exactly once to indicate that the device is fully functional.
|
|
459
|
+
In ramses_cc, 2411 entities are created - on the fly - only for devices that support them.
|
|
460
|
+
|
|
461
|
+
:param callback: A callable that takes no arguments and returns None.
|
|
462
|
+
If None, any existing callback will be cleared.
|
|
463
|
+
:type callback: Callable[[], None] | None
|
|
464
|
+
:raises ValueError: If the callback is not callable and not None
|
|
465
|
+
"""
|
|
466
|
+
if callback is not None and not callable(callback):
|
|
467
|
+
raise ValueError("Callback must be callable or None")
|
|
468
|
+
|
|
469
|
+
self._initialized_callback = callback
|
|
470
|
+
if callback is not None:
|
|
471
|
+
_LOGGER.debug("Initialization callback set for %s", self.id)
|
|
472
|
+
|
|
473
|
+
def _handle_initialized_callback(self) -> None:
|
|
474
|
+
"""Handle the initialization callback.
|
|
475
|
+
|
|
476
|
+
This method is called when the device has been fully initialized and
|
|
477
|
+
is ready to process commands. It triggers any registered initialization
|
|
478
|
+
callbacks and performs necessary setup for 2411 parameter support.
|
|
479
|
+
"""
|
|
480
|
+
if self._initialized_callback is not None and self.supports_2411:
|
|
481
|
+
_LOGGER.debug("2411-Device initialized: %s", self.id)
|
|
482
|
+
if callable(self._initialized_callback):
|
|
483
|
+
try:
|
|
484
|
+
self._initialized_callback()
|
|
485
|
+
except Exception as ex:
|
|
486
|
+
_LOGGER.warning("Error in initialized_callback: %s", ex)
|
|
487
|
+
finally:
|
|
488
|
+
# Clear the callback so it's only called once
|
|
489
|
+
self._initialized_callback = None
|
|
490
|
+
|
|
491
|
+
def set_param_update_callback(
|
|
492
|
+
self, callback: Callable[[str, Any], None] | None
|
|
493
|
+
) -> None:
|
|
494
|
+
"""Set a callback to be called when 2411 parameters are updated.
|
|
495
|
+
|
|
496
|
+
This method registers a callback function that will be invoked whenever
|
|
497
|
+
a 2411 parameter is updated. The callback receives the parameter ID and
|
|
498
|
+
its new value as arguments.
|
|
499
|
+
|
|
500
|
+
Since 2411 parameters are configuration entities, we are not polling for them
|
|
501
|
+
and we update them immediately after receiving a 2411 message. We don't wait for them,
|
|
502
|
+
we only process when we see a 2411 response for our device. The request may have come
|
|
503
|
+
from another REM or DIS, but we will update to that as well.
|
|
504
|
+
|
|
505
|
+
:param callback: A callable that will be invoked with (param_id, value) when a
|
|
506
|
+
2411 parameter is updated, or None to clear the current callback
|
|
507
|
+
:type callback: Callable[[str, Any], None] | None
|
|
508
|
+
"""
|
|
509
|
+
self._param_update_callback = callback
|
|
510
|
+
|
|
511
|
+
def _handle_param_update(self, param_id: str, value: Any) -> None:
|
|
512
|
+
"""Handle a parameter update and notify listeners.
|
|
513
|
+
|
|
514
|
+
This method processes parameter updates and notifies any registered
|
|
515
|
+
callbacks of the change. It ensures thread safety and handles any
|
|
516
|
+
exceptions that may occur during callback execution.
|
|
517
|
+
|
|
518
|
+
:param param_id: The ID of the parameter that was updated
|
|
519
|
+
:type param_id: str
|
|
520
|
+
:param value: The new value of the parameter
|
|
521
|
+
:type value: float
|
|
522
|
+
"""
|
|
523
|
+
if callable(self._param_update_callback):
|
|
524
|
+
try:
|
|
525
|
+
self._param_update_callback(param_id, value)
|
|
526
|
+
except Exception as ex:
|
|
527
|
+
_LOGGER.warning("Error in param_update_callback: %s", ex)
|
|
528
|
+
|
|
529
|
+
@property
|
|
530
|
+
def supports_2411(self) -> bool:
|
|
531
|
+
"""Return whether this device supports 2411 parameters.
|
|
532
|
+
|
|
533
|
+
:return: True if the device supports 2411 parameters, False otherwise
|
|
534
|
+
:rtype: bool
|
|
535
|
+
"""
|
|
536
|
+
return self._supports_2411
|
|
537
|
+
|
|
538
|
+
@property
|
|
539
|
+
def hgi(self) -> Any | None:
|
|
540
|
+
"""Return the HGI (Home Gateway Interface) device if available.
|
|
541
|
+
|
|
542
|
+
The HGI device provides additional functionality for certain operations.
|
|
543
|
+
|
|
544
|
+
:return: The HGI device instance, or None if not available
|
|
545
|
+
:rtype: float | None
|
|
546
|
+
"""
|
|
547
|
+
if self._hgi is None and self._gwy and hasattr(self._gwy, "hgi"):
|
|
548
|
+
self._hgi = self._gwy.hgi
|
|
549
|
+
return self._hgi
|
|
324
550
|
|
|
325
|
-
def
|
|
326
|
-
"""
|
|
551
|
+
def get_2411_param(self, param_id: str) -> float | None:
|
|
552
|
+
"""Get a 2411 parameter value.
|
|
327
553
|
|
|
328
|
-
|
|
554
|
+
:param param_id: The parameter ID to retrieve.
|
|
555
|
+
:type param_id: str
|
|
556
|
+
:return: The parameter value if found, None otherwise
|
|
557
|
+
:rtype: float | None
|
|
329
558
|
"""
|
|
559
|
+
return self._params_2411.get(param_id)
|
|
560
|
+
|
|
561
|
+
def set_2411_param(self, param_id: str, value: float) -> bool:
|
|
562
|
+
"""Set a 2411 parameter value.
|
|
563
|
+
|
|
564
|
+
:param param_id: The parameter ID to retrieve.
|
|
565
|
+
:type param_id: str
|
|
566
|
+
:param value: The parameter value to set.
|
|
567
|
+
:type value: float
|
|
568
|
+
:return: True if the parameter was set, False otherwise
|
|
569
|
+
:rtype: bool
|
|
570
|
+
"""
|
|
571
|
+
if not self._supports_2411:
|
|
572
|
+
_LOGGER.warning("Device %s doesn't support 2411 parameters", self.id)
|
|
573
|
+
return False
|
|
574
|
+
|
|
575
|
+
self._params_2411[param_id] = value
|
|
576
|
+
return True
|
|
330
577
|
|
|
331
|
-
|
|
578
|
+
def get_fan_param(self, param_id: str) -> Any | None:
|
|
579
|
+
"""Retrieve a fan parameter value from the device's message store.
|
|
580
|
+
|
|
581
|
+
This wrapper method gets a specific parameter value for a FAN device stored in
|
|
582
|
+
_params_2411 dict. It first makes sure we use the proper param_id format
|
|
583
|
+
|
|
584
|
+
:param param_id: The parameter ID to retrieve.
|
|
585
|
+
:type param_id: str
|
|
586
|
+
:return: The parameter value if found, None otherwise
|
|
587
|
+
:rtype: float | None
|
|
588
|
+
"""
|
|
589
|
+
# Ensure param_id is uppercase and strip leading zeros for consistency
|
|
590
|
+
param_id = (
|
|
591
|
+
str(param_id).upper().lstrip("0") or "0"
|
|
592
|
+
) # Handle case where param_id is "0"
|
|
593
|
+
|
|
594
|
+
param_value = self.get_2411_param(param_id)
|
|
595
|
+
if param_value is not None:
|
|
596
|
+
return param_value
|
|
597
|
+
else:
|
|
598
|
+
_LOGGER.debug("Parameter %s not found for %s", param_id, self.id)
|
|
599
|
+
return None
|
|
600
|
+
|
|
601
|
+
def _handle_2411_message(self, msg: Message) -> None:
|
|
602
|
+
"""Handle incoming 2411 parameter messages.
|
|
603
|
+
|
|
604
|
+
This method processes 2411 parameter update messages, updates the device's
|
|
605
|
+
message store, and triggers any registered parameter update callbacks.
|
|
606
|
+
It handles parameter value normalization and validation.
|
|
607
|
+
|
|
608
|
+
:param msg: The incoming 2411 message
|
|
609
|
+
:type msg: Message
|
|
610
|
+
"""
|
|
611
|
+
if not hasattr(msg, "payload") or not isinstance(msg.payload, dict):
|
|
612
|
+
_LOGGER.debug("Invalid 2411 message format: %s", msg)
|
|
613
|
+
return
|
|
614
|
+
|
|
615
|
+
param_id = msg.payload.get("parameter")
|
|
616
|
+
param_value = msg.payload.get("value")
|
|
617
|
+
|
|
618
|
+
if not param_id or param_value is None:
|
|
619
|
+
_LOGGER.debug("Missing parameter ID or value in 2411 message: %s", msg)
|
|
620
|
+
return
|
|
621
|
+
|
|
622
|
+
# Mark that we support 2411 parameters
|
|
623
|
+
if not self._supports_2411:
|
|
624
|
+
self._supports_2411 = True
|
|
625
|
+
_LOGGER.debug("Device %s supports 2411 parameters", self.id)
|
|
626
|
+
|
|
627
|
+
# Normalize the value if needed
|
|
628
|
+
if param_id == "75" and isinstance(param_value, (int, float)):
|
|
629
|
+
param_value = round(float(param_value), 1)
|
|
630
|
+
elif param_id in ("52", "95"): # Percentage parameters
|
|
631
|
+
param_value = round(float(param_value), 3) # Keep precision for percentages
|
|
632
|
+
|
|
633
|
+
# Store in params
|
|
634
|
+
old_value = self.get_2411_param(param_id)
|
|
635
|
+
self.set_2411_param(param_id, param_value)
|
|
636
|
+
|
|
637
|
+
# Log the update
|
|
638
|
+
_LOGGER.debug(
|
|
639
|
+
"Updated 2411 parameter %s: %s (was: %s) for %s",
|
|
640
|
+
param_id,
|
|
641
|
+
param_value,
|
|
642
|
+
old_value,
|
|
643
|
+
self.id,
|
|
644
|
+
)
|
|
332
645
|
|
|
333
|
-
|
|
334
|
-
|
|
646
|
+
# call the 2411 parameter update callback
|
|
647
|
+
self._handle_param_update(param_id, param_value)
|
|
335
648
|
|
|
336
|
-
|
|
337
|
-
|
|
649
|
+
def _handle_msg(self, msg: Message) -> None:
|
|
650
|
+
"""Handle a message from this device.
|
|
651
|
+
|
|
652
|
+
This method processes incoming messages for the device, with special
|
|
653
|
+
handling for 2411 parameter messages. It updates the device state and
|
|
654
|
+
triggers any necessary callbacks.
|
|
655
|
+
|
|
656
|
+
After handling the messages, it calls the initialized callback if set to notify that
|
|
657
|
+
the device was fully initialized.
|
|
658
|
+
|
|
659
|
+
:param msg: The incoming message to process
|
|
660
|
+
:type msg: Message
|
|
661
|
+
"""
|
|
662
|
+
super()._handle_msg(msg)
|
|
663
|
+
|
|
664
|
+
# Handle 2411 parameter messages
|
|
665
|
+
if msg.code == Code._2411:
|
|
666
|
+
_LOGGER.debug(
|
|
667
|
+
"Received 2411 message from %s: verb=%s, payload=%s, src=%s, dst=%s",
|
|
668
|
+
self.id,
|
|
669
|
+
msg.verb,
|
|
670
|
+
msg.payload,
|
|
671
|
+
msg.src,
|
|
672
|
+
msg.dst,
|
|
673
|
+
)
|
|
674
|
+
self._handle_2411_message(msg)
|
|
675
|
+
|
|
676
|
+
self._handle_initialized_callback()
|
|
338
677
|
|
|
339
678
|
def _setup_discovery_cmds(self) -> None:
|
|
679
|
+
"""Set up discovery commands for the RFS gateway.
|
|
680
|
+
|
|
681
|
+
This method initializes the discovery commands needed to identify and
|
|
682
|
+
communicate with the RFS gateway device.
|
|
683
|
+
"""
|
|
340
684
|
super()._setup_discovery_cmds()
|
|
341
685
|
|
|
342
686
|
# RP --- 32:155617 18:005904 --:------ 22F1 003 000207
|
|
@@ -344,14 +688,25 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A]
|
|
|
344
688
|
Command.from_attrs(RQ, self.id, Code._22F1, "00"), 60 * 60 * 24, delay=15
|
|
345
689
|
) # to learn scheme: orcon/itho/other (04/07/0?)
|
|
346
690
|
|
|
691
|
+
# Add a single discovery command for all parameters (3F likely to be supported if any)
|
|
692
|
+
# The handler will process the response and update the appropriate parameter and
|
|
693
|
+
# also set the supports_2411 flag
|
|
694
|
+
_LOGGER.debug("Adding single discovery command for all 2411 parameters")
|
|
695
|
+
self._add_discovery_cmd(
|
|
696
|
+
Command.from_attrs(RQ, self.id, Code._2411, "00003F"),
|
|
697
|
+
interval=60 * 60 * 24, # Check daily
|
|
698
|
+
delay=40, # Initial delay before first discovery
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
# Standard discovery commands for other codes
|
|
347
702
|
for code in (
|
|
348
|
-
Code._2210,
|
|
349
|
-
Code._22E0,
|
|
350
|
-
Code._22E5,
|
|
351
|
-
Code._22E9,
|
|
352
|
-
Code._22F2,
|
|
353
|
-
Code._22F4,
|
|
354
|
-
Code._22F8,
|
|
703
|
+
Code._2210, # Air quality
|
|
704
|
+
Code._22E0, # Bypass position
|
|
705
|
+
Code._22E5, # Remaining minutes
|
|
706
|
+
Code._22E9, # Speed cap
|
|
707
|
+
Code._22F2, # Post heat
|
|
708
|
+
Code._22F4, # Pre heat
|
|
709
|
+
Code._22F8, # Air quality base
|
|
355
710
|
):
|
|
356
711
|
self._add_discovery_cmd(
|
|
357
712
|
Command.from_attrs(RQ, self.id, code, "00"), 60 * 30, delay=15
|
|
@@ -362,12 +717,105 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A]
|
|
|
362
717
|
Command.from_attrs(RQ, self.id, code, "00"), 60 * 30, delay=30
|
|
363
718
|
)
|
|
364
719
|
|
|
720
|
+
def add_bound_device(self, device_id: str, device_type: str) -> None:
|
|
721
|
+
"""Add a bound device to this FAN.
|
|
722
|
+
|
|
723
|
+
This method registers a REM or DIS device as bound to this FAN device.
|
|
724
|
+
Bound devices are required for certain operations like setting parameters.
|
|
725
|
+
|
|
726
|
+
A bound device is needed to be able to send 2411 parameter Set messages,
|
|
727
|
+
or the device will not accept and respond to them.
|
|
728
|
+
In HomeAssistant, ramses_cc, you can set a bound device in the device configuration.
|
|
729
|
+
|
|
730
|
+
System schema and known devices example:
|
|
731
|
+
"32:153289":
|
|
732
|
+
bound: "37:168270"
|
|
733
|
+
class: FAN
|
|
734
|
+
"37:168270":
|
|
735
|
+
class: REM
|
|
736
|
+
faked: true
|
|
737
|
+
|
|
738
|
+
:param device_id: The unique identifier of the device to bind
|
|
739
|
+
:type device_id: str
|
|
740
|
+
:param device_type: The type of device (must be 'REM' or 'DIS')
|
|
741
|
+
:type device_type: str
|
|
742
|
+
:raises ValueError: If the device type is not 'REM' or 'DIS'
|
|
743
|
+
"""
|
|
744
|
+
if device_type not in (DevType.REM, DevType.DIS):
|
|
745
|
+
_LOGGER.warning(
|
|
746
|
+
"Cannot bind device %s of type %s to FAN %s: must be REM or DIS",
|
|
747
|
+
device_id,
|
|
748
|
+
device_type,
|
|
749
|
+
self.id,
|
|
750
|
+
)
|
|
751
|
+
return
|
|
752
|
+
|
|
753
|
+
self._bound_devices[device_id] = device_type
|
|
754
|
+
_LOGGER.info("Bound %s device %s to FAN %s", device_type, device_id, self.id)
|
|
755
|
+
|
|
756
|
+
def remove_bound_device(self, device_id: str) -> None:
|
|
757
|
+
"""Remove a bound device from this FAN.
|
|
758
|
+
|
|
759
|
+
This method unregisters a previously bound device from this FAN.
|
|
760
|
+
|
|
761
|
+
:param device_id: The unique identifier of the device to unbind
|
|
762
|
+
:type device_id: str
|
|
763
|
+
"""
|
|
764
|
+
if device_id in self._bound_devices:
|
|
765
|
+
device_type = self._bound_devices.pop(device_id)
|
|
766
|
+
_LOGGER.info(
|
|
767
|
+
"Removed bound %s device %s from FAN %s",
|
|
768
|
+
device_type,
|
|
769
|
+
device_id,
|
|
770
|
+
self.id,
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
def get_bound_rem(self) -> str | None:
|
|
774
|
+
"""Get the first bound REM/DIS device ID for this FAN.
|
|
775
|
+
|
|
776
|
+
This method retrieves the device ID of the first bound REM or DIS device.
|
|
777
|
+
Bound devices are required for certain operations like setting parameters.
|
|
778
|
+
|
|
779
|
+
:return: The device ID of the first bound REM or DIS device, or None if none found
|
|
780
|
+
:rtype: str | None
|
|
781
|
+
"""
|
|
782
|
+
if not self._bound_devices:
|
|
783
|
+
_LOGGER.debug("No bound devices found for FAN %s", self.id)
|
|
784
|
+
return None
|
|
785
|
+
|
|
786
|
+
# Find first REM or DIS device
|
|
787
|
+
for device_id, device_type in self._bound_devices.items():
|
|
788
|
+
if device_type in (DevType.REM, DevType.DIS):
|
|
789
|
+
_LOGGER.debug(
|
|
790
|
+
"Found bound %s device %s for FAN %s",
|
|
791
|
+
device_type,
|
|
792
|
+
device_id,
|
|
793
|
+
self.id,
|
|
794
|
+
)
|
|
795
|
+
return device_id
|
|
796
|
+
|
|
797
|
+
_LOGGER.debug("No bound REM or DIS devices found for FAN %s", self.id)
|
|
798
|
+
return None
|
|
799
|
+
|
|
365
800
|
@property
|
|
366
801
|
def air_quality(self) -> float | None:
|
|
802
|
+
"""Return the current air quality measurement.
|
|
803
|
+
|
|
804
|
+
:return: The air quality measurement as a float, or None if not available
|
|
805
|
+
:rtype: float | None
|
|
806
|
+
"""
|
|
367
807
|
return self._msg_value(Code._31DA, key=SZ_AIR_QUALITY)
|
|
368
808
|
|
|
369
809
|
@property
|
|
370
810
|
def air_quality_base(self) -> float | None:
|
|
811
|
+
"""Return the base air quality measurement.
|
|
812
|
+
|
|
813
|
+
This represents the baseline or raw air quality measurement before any
|
|
814
|
+
processing or normalization.
|
|
815
|
+
|
|
816
|
+
:return: The base air quality measurement, or None if not available
|
|
817
|
+
:rtype: float | None
|
|
818
|
+
"""
|
|
371
819
|
return self._msg_value(Code._31DA, key=SZ_AIR_QUALITY_BASIS)
|
|
372
820
|
|
|
373
821
|
@property
|
|
@@ -396,6 +844,11 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A]
|
|
|
396
844
|
|
|
397
845
|
@property
|
|
398
846
|
def co2_level(self) -> int | None:
|
|
847
|
+
"""Return the CO2 level in parts per million (ppm).
|
|
848
|
+
|
|
849
|
+
:return: The CO2 level in ppm, or None if not available
|
|
850
|
+
:rtype: int | None
|
|
851
|
+
"""
|
|
399
852
|
return self._msg_value(Code._31DA, key=SZ_CO2_LEVEL)
|
|
400
853
|
|
|
401
854
|
@property
|
|
@@ -419,10 +872,20 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A]
|
|
|
419
872
|
|
|
420
873
|
@property
|
|
421
874
|
def exhaust_flow(self) -> float | None:
|
|
875
|
+
"""Return the current exhaust air flow rate.
|
|
876
|
+
|
|
877
|
+
:return: The exhaust air flow rate in m³/h, or None if not available
|
|
878
|
+
:rtype: float | None
|
|
879
|
+
"""
|
|
422
880
|
return self._msg_value(Code._31DA, key=SZ_EXHAUST_FLOW)
|
|
423
881
|
|
|
424
882
|
@property
|
|
425
883
|
def exhaust_temp(self) -> float | None:
|
|
884
|
+
"""Return the current exhaust air temperature.
|
|
885
|
+
|
|
886
|
+
:return: The exhaust air temperature in degrees Celsius, or None if not available
|
|
887
|
+
:rtype: float | None
|
|
888
|
+
"""
|
|
426
889
|
return self._msg_value(Code._31DA, key=SZ_EXHAUST_TEMP)
|
|
427
890
|
|
|
428
891
|
@property
|
|
@@ -481,10 +944,22 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A]
|
|
|
481
944
|
|
|
482
945
|
@property
|
|
483
946
|
def indoor_temp(self) -> float | None:
|
|
947
|
+
"""Return the current indoor temperature.
|
|
948
|
+
|
|
949
|
+
:return: The indoor temperature in degrees Celsius, or None if not available
|
|
950
|
+
:rtype: float | None
|
|
951
|
+
"""
|
|
484
952
|
return self._msg_value(Code._31DA, key=SZ_INDOOR_TEMP)
|
|
485
953
|
|
|
486
954
|
@property
|
|
487
955
|
def outdoor_humidity(self) -> float | None:
|
|
956
|
+
"""Return the outdoor relative humidity.
|
|
957
|
+
|
|
958
|
+
Handles special case for Ventura devices that send humidity data in 12A0 messages.
|
|
959
|
+
|
|
960
|
+
:return: The outdoor relative humidity as a percentage (0-100), or None if not available
|
|
961
|
+
:rtype: float | None
|
|
962
|
+
"""
|
|
488
963
|
if Code._12A0 in self._msgs and isinstance(
|
|
489
964
|
self._msgs[Code._12A0].payload, list
|
|
490
965
|
): # FAN Ventura sends RH/temps as a list; element [1] contains outdoor_hum
|
|
@@ -495,22 +970,47 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A]
|
|
|
495
970
|
|
|
496
971
|
@property
|
|
497
972
|
def outdoor_temp(self) -> float | None:
|
|
973
|
+
"""Return the outdoor temperature in Celsius.
|
|
974
|
+
|
|
975
|
+
:return: The outdoor temperature in degrees Celsius, or None if not available
|
|
976
|
+
:rtype: float | None
|
|
977
|
+
"""
|
|
498
978
|
return self._msg_value(Code._31DA, key=SZ_OUTDOOR_TEMP)
|
|
499
979
|
|
|
500
980
|
@property
|
|
501
981
|
def post_heat(self) -> int | None:
|
|
982
|
+
"""Return the post-heat status.
|
|
983
|
+
|
|
984
|
+
:return: The post-heat status as an integer, or None if not available
|
|
985
|
+
:rtype: int | None
|
|
986
|
+
"""
|
|
502
987
|
return self._msg_value(Code._31DA, key=SZ_POST_HEAT)
|
|
503
988
|
|
|
504
989
|
@property
|
|
505
990
|
def pre_heat(self) -> int | None:
|
|
991
|
+
"""Return the pre-heat status.
|
|
992
|
+
|
|
993
|
+
:return: The pre-heat status as an integer, or None if not available
|
|
994
|
+
:rtype: int | None
|
|
995
|
+
"""
|
|
506
996
|
return self._msg_value(Code._31DA, key=SZ_PRE_HEAT)
|
|
507
997
|
|
|
508
998
|
@property
|
|
509
999
|
def remaining_mins(self) -> int | None:
|
|
1000
|
+
"""Return the remaining minutes for the current operation.
|
|
1001
|
+
|
|
1002
|
+
:return: The remaining minutes as an integer, or None if not available
|
|
1003
|
+
:rtype: int | None
|
|
1004
|
+
"""
|
|
510
1005
|
return self._msg_value(Code._31DA, key=SZ_REMAINING_MINS)
|
|
511
1006
|
|
|
512
1007
|
@property
|
|
513
1008
|
def request_fan_speed(self) -> float | None:
|
|
1009
|
+
"""Return the requested fan speed.
|
|
1010
|
+
|
|
1011
|
+
:return: The requested fan speed as a percentage, or None if not available
|
|
1012
|
+
:rtype: float | None
|
|
1013
|
+
"""
|
|
514
1014
|
return self._msg_value(Code._2210, key=SZ_REQ_SPEED)
|
|
515
1015
|
|
|
516
1016
|
@property
|
|
@@ -523,18 +1023,40 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A]
|
|
|
523
1023
|
|
|
524
1024
|
@property
|
|
525
1025
|
def speed_cap(self) -> int | None:
|
|
1026
|
+
"""Return the speed capabilities of the fan.
|
|
1027
|
+
|
|
1028
|
+
:return: The speed capabilities as an integer, or None if not available
|
|
1029
|
+
:rtype: int | None
|
|
1030
|
+
"""
|
|
526
1031
|
return self._msg_value(Code._31DA, key=SZ_SPEED_CAPABILITIES)
|
|
527
1032
|
|
|
528
1033
|
@property
|
|
529
1034
|
def supply_fan_speed(self) -> float | None:
|
|
1035
|
+
"""Return the supply fan speed.
|
|
1036
|
+
|
|
1037
|
+
:return: The supply fan speed as a percentage, or None if not available
|
|
1038
|
+
:rtype: float | None
|
|
1039
|
+
"""
|
|
530
1040
|
return self._msg_value(Code._31DA, key=SZ_SUPPLY_FAN_SPEED)
|
|
531
1041
|
|
|
532
1042
|
@property
|
|
533
1043
|
def supply_flow(self) -> float | None:
|
|
1044
|
+
"""Return the supply air flow rate.
|
|
1045
|
+
|
|
1046
|
+
:return: The supply air flow rate in m³/h, or None if not available
|
|
1047
|
+
:rtype: float | None
|
|
1048
|
+
"""
|
|
534
1049
|
return self._msg_value(Code._31DA, key=SZ_SUPPLY_FLOW)
|
|
535
1050
|
|
|
536
1051
|
@property
|
|
537
1052
|
def supply_temp(self) -> float | None:
|
|
1053
|
+
"""Return the supply air temperature.
|
|
1054
|
+
|
|
1055
|
+
Handles special case for Ventura devices that send temperature data in 12A0 messages.
|
|
1056
|
+
|
|
1057
|
+
:return: The supply air temperature in Celsius, or None if not available
|
|
1058
|
+
:rtype: float | None
|
|
1059
|
+
"""
|
|
538
1060
|
if Code._12A0 in self._msgs and isinstance(
|
|
539
1061
|
self._msgs[Code._12A0].payload, list
|
|
540
1062
|
): # FAN Ventura sends RH/temps as a list;
|
|
@@ -559,6 +1081,13 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A]
|
|
|
559
1081
|
|
|
560
1082
|
@property
|
|
561
1083
|
def temperature(self) -> float | None: # Celsius
|
|
1084
|
+
"""Return the current temperature in Celsius.
|
|
1085
|
+
|
|
1086
|
+
Handles special cases.
|
|
1087
|
+
|
|
1088
|
+
:return: The temperature in degrees Celsius, or None if not available
|
|
1089
|
+
:rtype: float | None
|
|
1090
|
+
"""
|
|
562
1091
|
if Code._12A0 in self._msgs and isinstance(
|
|
563
1092
|
self._msgs[Code._12A0].payload, list
|
|
564
1093
|
): # FAN Ventura sends RH/temps as a list; use element [1]
|
|
@@ -655,7 +1184,7 @@ _REMOTES = {
|
|
|
655
1184
|
"""
|
|
656
1185
|
# CVE/HRU remote (536-0124) [RFT W: 3 modes, timer]
|
|
657
1186
|
"away": (Code._22F1, 00, 01|04"), # how to invoke?
|
|
658
|
-
"low": (Code._22F1, 00, 02|04"),
|
|
1187
|
+
"low": (Code._22F1, 00, 02|04"), # aka eco
|
|
659
1188
|
"medium": (Code._22F1, 00, 03|04"), # aka auto (with sensors) - is that only for 63?
|
|
660
1189
|
"high": (Code._22F1, 00, 04|04"), # aka full
|
|
661
1190
|
|