ramses-rf 0.51.7__py3-none-any.whl → 0.51.8__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/device/hvac.py +579 -32
- 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.8.dist-info}/METADATA +6 -6
- {ramses_rf-0.51.7.dist-info → ramses_rf-0.51.8.dist-info}/RECORD +21 -21
- ramses_tx/__init__.py +25 -4
- ramses_tx/command.py +1449 -138
- ramses_tx/helpers.py +2 -2
- ramses_tx/parsers.py +12 -9
- 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/version.py +1 -1
- {ramses_rf-0.51.7.dist-info → ramses_rf-0.51.8.dist-info}/WHEEL +0 -0
- {ramses_rf-0.51.7.dist-info → ramses_rf-0.51.8.dist-info}/entry_points.txt +0 -0
- {ramses_rf-0.51.7.dist-info → ramses_rf-0.51.8.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,201 @@ 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._initialized_callback = None # Called when device is fully initialized
|
|
450
|
+
self._param_update_callback = None # Called when 2411 parameters are updated
|
|
451
|
+
self._hgi: Any | None = None # Will be set when HGI is available
|
|
452
|
+
self._bound_devices: dict[str, str] = {} # Track bound devices (e.g., REM/DIS)
|
|
453
|
+
|
|
454
|
+
def set_initialized_callback(self, callback: Callable[[], None] | None) -> None:
|
|
455
|
+
"""Set a callback to be executed when the next message (any) is received.
|
|
456
|
+
|
|
457
|
+
The callback will be used exactly once to indicate that the device is fully functional.
|
|
458
|
+
In ramses_cc, 2411 entities are created - on the fly - only for devices that support them.
|
|
459
|
+
|
|
460
|
+
:param callback: A callable that takes no arguments and returns None.
|
|
461
|
+
If None, any existing callback will be cleared.
|
|
462
|
+
:type callback: Callable[[], None] | None
|
|
463
|
+
:raises ValueError: If the callback is not callable and not None
|
|
464
|
+
"""
|
|
465
|
+
if callback is not None and not callable(callback):
|
|
466
|
+
raise ValueError("Callback must be callable or None")
|
|
324
467
|
|
|
325
|
-
|
|
326
|
-
|
|
468
|
+
self._initialized_callback = callback
|
|
469
|
+
if callback is not None:
|
|
470
|
+
_LOGGER.debug("Initialization callback set for %s", self.id)
|
|
327
471
|
|
|
328
|
-
|
|
472
|
+
def _handle_initialized_callback(self) -> None:
|
|
473
|
+
"""Handle the initialization callback.
|
|
474
|
+
|
|
475
|
+
This method is called when the device has been fully initialized and
|
|
476
|
+
is ready to process commands. It triggers any registered initialization
|
|
477
|
+
callbacks and performs necessary setup for 2411 parameter support.
|
|
478
|
+
"""
|
|
479
|
+
if self._initialized_callback is not None and self.supports_2411:
|
|
480
|
+
_LOGGER.debug("2411-Device initialized: %s", self.id)
|
|
481
|
+
if callable(self._initialized_callback):
|
|
482
|
+
try:
|
|
483
|
+
self._initialized_callback()
|
|
484
|
+
except Exception as ex:
|
|
485
|
+
_LOGGER.warning("Error in initialized_callback: %s", ex)
|
|
486
|
+
finally:
|
|
487
|
+
# Clear the callback so it's only called once
|
|
488
|
+
self._initialized_callback = None
|
|
489
|
+
|
|
490
|
+
def set_param_update_callback(
|
|
491
|
+
self, callback: Callable[[str, Any], None] | None
|
|
492
|
+
) -> None:
|
|
493
|
+
"""Set a callback to be called when 2411 parameters are updated.
|
|
494
|
+
|
|
495
|
+
This method registers a callback function that will be invoked whenever
|
|
496
|
+
a 2411 parameter is updated. The callback receives the parameter ID and
|
|
497
|
+
its new value as arguments.
|
|
498
|
+
|
|
499
|
+
Since 2411 parameters are configuration entities, we are not polling for them
|
|
500
|
+
and we update them immediately after receiving a 2411 message. We don't wait for them,
|
|
501
|
+
we only process when we see a 2411 response for our device. The request may have come
|
|
502
|
+
from another REM or DIS, but we will update to that as well.
|
|
503
|
+
|
|
504
|
+
:param callback: A callable that will be invoked with (param_id, value) when a
|
|
505
|
+
2411 parameter is updated, or None to clear the current callback
|
|
506
|
+
:type callback: Callable[[str, Any], None] | None
|
|
507
|
+
"""
|
|
508
|
+
self._param_update_callback = callback
|
|
509
|
+
|
|
510
|
+
def _handle_param_update(self, param_id: str, value: Any) -> None:
|
|
511
|
+
"""Handle a parameter update and notify listeners.
|
|
512
|
+
|
|
513
|
+
This method processes parameter updates and notifies any registered
|
|
514
|
+
callbacks of the change. It ensures thread safety and handles any
|
|
515
|
+
exceptions that may occur during callback execution.
|
|
516
|
+
|
|
517
|
+
:param param_id: The ID of the parameter that was updated
|
|
518
|
+
:type param_id: str
|
|
519
|
+
:param value: The new value of the parameter
|
|
520
|
+
:type value: Any
|
|
521
|
+
"""
|
|
522
|
+
if callable(self._param_update_callback):
|
|
523
|
+
try:
|
|
524
|
+
self._param_update_callback(param_id, value)
|
|
525
|
+
except Exception as ex:
|
|
526
|
+
_LOGGER.warning("Error in param_update_callback: %s", ex)
|
|
527
|
+
|
|
528
|
+
@property
|
|
529
|
+
def supports_2411(self) -> bool:
|
|
530
|
+
"""Return whether this device supports 2411 parameters.
|
|
531
|
+
|
|
532
|
+
:return: True if the device supports 2411 parameters, False otherwise
|
|
533
|
+
:rtype: bool
|
|
329
534
|
"""
|
|
535
|
+
return self._supports_2411
|
|
536
|
+
|
|
537
|
+
@property
|
|
538
|
+
def hgi(self) -> Any | None:
|
|
539
|
+
"""Return the HGI (Home Gateway Interface) device if available.
|
|
330
540
|
|
|
331
|
-
|
|
541
|
+
The HGI device provides additional functionality for certain operations.
|
|
542
|
+
|
|
543
|
+
:return: The HGI device instance, or None if not available
|
|
544
|
+
:rtype: Any | None
|
|
545
|
+
"""
|
|
546
|
+
if self._hgi is None and self._gwy and hasattr(self._gwy, "hgi"):
|
|
547
|
+
self._hgi = self._gwy.hgi
|
|
548
|
+
return self._hgi
|
|
332
549
|
|
|
333
|
-
|
|
334
|
-
|
|
550
|
+
def _handle_2411_message(self, msg: Message) -> None:
|
|
551
|
+
"""Handle incoming 2411 parameter messages.
|
|
552
|
+
|
|
553
|
+
This method processes 2411 parameter update messages, updates the device's
|
|
554
|
+
message store, and triggers any registered parameter update callbacks.
|
|
555
|
+
It handles parameter value normalization and validation.
|
|
556
|
+
|
|
557
|
+
:param msg: The incoming 2411 message
|
|
558
|
+
:type msg: Message
|
|
559
|
+
"""
|
|
560
|
+
if not hasattr(msg, "payload") or not isinstance(msg.payload, dict):
|
|
561
|
+
_LOGGER.debug("Invalid 2411 message format: %s", msg)
|
|
562
|
+
return
|
|
563
|
+
|
|
564
|
+
param_id = msg.payload.get("parameter")
|
|
565
|
+
param_value = msg.payload.get("value")
|
|
566
|
+
|
|
567
|
+
if not param_id or param_value is None:
|
|
568
|
+
_LOGGER.debug("Missing parameter ID or value in 2411 message: %s", msg)
|
|
569
|
+
return
|
|
570
|
+
|
|
571
|
+
# Create a composite key for this parameter using the normalized ID
|
|
572
|
+
key = f"{Code._2411}_{param_id}"
|
|
573
|
+
|
|
574
|
+
# Store the message in the device's message store
|
|
575
|
+
old_value = self._msgs.get(Code._2411)
|
|
576
|
+
# Use direct assignment for Code._2411 key
|
|
577
|
+
self._msgs[Code._2411] = msg
|
|
578
|
+
# For the composite key, we need to bypass type checking
|
|
579
|
+
self._msgs[key] = msg # type: ignore[index]
|
|
580
|
+
|
|
581
|
+
_LOGGER.debug(
|
|
582
|
+
"Updated 2411 parameter %s = %s (was: %s) for %s",
|
|
583
|
+
param_id,
|
|
584
|
+
param_value,
|
|
585
|
+
old_value.payload if old_value else None,
|
|
586
|
+
self.id,
|
|
587
|
+
)
|
|
335
588
|
|
|
336
|
-
|
|
337
|
-
|
|
589
|
+
# Mark that we support 2411 parameters
|
|
590
|
+
if not self._supports_2411:
|
|
591
|
+
self._supports_2411 = True
|
|
592
|
+
_LOGGER.debug("Device %s supports 2411 parameters", self.id)
|
|
593
|
+
|
|
594
|
+
# Round parameter 75 values to 1 decimal place
|
|
595
|
+
if param_id == "75" and isinstance(param_value, int | float):
|
|
596
|
+
param_value = round(float(param_value), 1)
|
|
597
|
+
|
|
598
|
+
# call the 2411 parameter update callback
|
|
599
|
+
self._handle_param_update(param_id, param_value)
|
|
600
|
+
|
|
601
|
+
def _handle_msg(self, msg: Message) -> None:
|
|
602
|
+
"""Handle a message from this device.
|
|
603
|
+
|
|
604
|
+
This method processes incoming messages for the device, with special
|
|
605
|
+
handling for 2411 parameter messages. It updates the device state and
|
|
606
|
+
triggers any necessary callbacks.
|
|
607
|
+
|
|
608
|
+
After handling the messages, it calls the initialized callback if set to notify that
|
|
609
|
+
the device was fully initialized.
|
|
610
|
+
|
|
611
|
+
:param msg: The incoming message to process
|
|
612
|
+
:type msg: Message
|
|
613
|
+
"""
|
|
614
|
+
super()._handle_msg(msg)
|
|
615
|
+
|
|
616
|
+
# Handle 2411 parameter messages
|
|
617
|
+
if msg.code == Code._2411:
|
|
618
|
+
_LOGGER.debug(
|
|
619
|
+
"Received 2411 message from %s: verb=%s, payload=%s, src=%s, dst=%s",
|
|
620
|
+
self.id,
|
|
621
|
+
msg.verb,
|
|
622
|
+
msg.payload,
|
|
623
|
+
msg.src,
|
|
624
|
+
msg.dst,
|
|
625
|
+
)
|
|
626
|
+
self._handle_2411_message(msg)
|
|
627
|
+
|
|
628
|
+
self._handle_initialized_callback()
|
|
338
629
|
|
|
339
630
|
def _setup_discovery_cmds(self) -> None:
|
|
631
|
+
"""Set up discovery commands for the RFS gateway.
|
|
632
|
+
|
|
633
|
+
This method initializes the discovery commands needed to identify and
|
|
634
|
+
communicate with the RFS gateway device.
|
|
635
|
+
"""
|
|
340
636
|
super()._setup_discovery_cmds()
|
|
341
637
|
|
|
342
638
|
# RP --- 32:155617 18:005904 --:------ 22F1 003 000207
|
|
@@ -344,14 +640,25 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A]
|
|
|
344
640
|
Command.from_attrs(RQ, self.id, Code._22F1, "00"), 60 * 60 * 24, delay=15
|
|
345
641
|
) # to learn scheme: orcon/itho/other (04/07/0?)
|
|
346
642
|
|
|
643
|
+
# Add a single discovery command for all parameters (3F likely to be supported if any)
|
|
644
|
+
# The handler will process the response and update the appropriate parameter and
|
|
645
|
+
# also set the supports_2411 flag
|
|
646
|
+
_LOGGER.debug("Adding single discovery command for all 2411 parameters")
|
|
647
|
+
self._add_discovery_cmd(
|
|
648
|
+
Command.from_attrs(RQ, self.id, Code._2411, "00003F"),
|
|
649
|
+
interval=60 * 60 * 24, # Check daily
|
|
650
|
+
delay=40, # Initial delay before first discovery
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
# Standard discovery commands for other codes
|
|
347
654
|
for code in (
|
|
348
|
-
Code._2210,
|
|
349
|
-
Code._22E0,
|
|
350
|
-
Code._22E5,
|
|
351
|
-
Code._22E9,
|
|
352
|
-
Code._22F2,
|
|
353
|
-
Code._22F4,
|
|
354
|
-
Code._22F8,
|
|
655
|
+
Code._2210, # Air quality
|
|
656
|
+
Code._22E0, # Bypass position
|
|
657
|
+
Code._22E5, # Remaining minutes
|
|
658
|
+
Code._22E9, # Speed cap
|
|
659
|
+
Code._22F2, # Post heat
|
|
660
|
+
Code._22F4, # Pre heat
|
|
661
|
+
Code._22F8, # Air quality base
|
|
355
662
|
):
|
|
356
663
|
self._add_discovery_cmd(
|
|
357
664
|
Command.from_attrs(RQ, self.id, code, "00"), 60 * 30, delay=15
|
|
@@ -362,12 +669,171 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A]
|
|
|
362
669
|
Command.from_attrs(RQ, self.id, code, "00"), 60 * 30, delay=30
|
|
363
670
|
)
|
|
364
671
|
|
|
672
|
+
def add_bound_device(self, device_id: str, device_type: str) -> None:
|
|
673
|
+
"""Add a bound device to this FAN.
|
|
674
|
+
|
|
675
|
+
This method registers a REM or DIS device as bound to this FAN device.
|
|
676
|
+
Bound devices are required for certain operations like setting parameters.
|
|
677
|
+
|
|
678
|
+
A bound device is needed to be able to send 2411 parameter Set messages,
|
|
679
|
+
or the device will not accept and respond to them.
|
|
680
|
+
In HomeAssistant, ramses_cc, you can set a bound device in the device configuration.
|
|
681
|
+
|
|
682
|
+
System schema and known devices example:
|
|
683
|
+
"32:153289":
|
|
684
|
+
bound: "37:168270"
|
|
685
|
+
class: FAN
|
|
686
|
+
"37:168270":
|
|
687
|
+
class: REM
|
|
688
|
+
faked: true
|
|
689
|
+
|
|
690
|
+
:param device_id: The unique identifier of the device to bind
|
|
691
|
+
:type device_id: str
|
|
692
|
+
:param device_type: The type of device (must be 'REM' or 'DIS')
|
|
693
|
+
:type device_type: str
|
|
694
|
+
:raises ValueError: If the device type is not 'REM' or 'DIS'
|
|
695
|
+
"""
|
|
696
|
+
if device_type not in (DevType.REM, DevType.DIS):
|
|
697
|
+
_LOGGER.warning(
|
|
698
|
+
"Cannot bind device %s of type %s to FAN %s: must be REM or DIS",
|
|
699
|
+
device_id,
|
|
700
|
+
device_type,
|
|
701
|
+
self.id,
|
|
702
|
+
)
|
|
703
|
+
return
|
|
704
|
+
|
|
705
|
+
self._bound_devices[device_id] = device_type
|
|
706
|
+
_LOGGER.info("Bound %s device %s to FAN %s", device_type, device_id, self.id)
|
|
707
|
+
|
|
708
|
+
def remove_bound_device(self, device_id: str) -> None:
|
|
709
|
+
"""Remove a bound device from this FAN.
|
|
710
|
+
|
|
711
|
+
This method unregisters a previously bound device from this FAN.
|
|
712
|
+
|
|
713
|
+
:param device_id: The unique identifier of the device to unbind
|
|
714
|
+
:type device_id: str
|
|
715
|
+
"""
|
|
716
|
+
if device_id in self._bound_devices:
|
|
717
|
+
device_type = self._bound_devices.pop(device_id)
|
|
718
|
+
_LOGGER.info(
|
|
719
|
+
"Removed bound %s device %s from FAN %s",
|
|
720
|
+
device_type,
|
|
721
|
+
device_id,
|
|
722
|
+
self.id,
|
|
723
|
+
)
|
|
724
|
+
|
|
725
|
+
def get_bound_rem(self) -> str | None:
|
|
726
|
+
"""Get the first bound REM/DIS device ID for this FAN.
|
|
727
|
+
|
|
728
|
+
This method retrieves the device ID of the first bound REM or DIS device.
|
|
729
|
+
Bound devices are required for certain operations like setting parameters.
|
|
730
|
+
|
|
731
|
+
:return: The device ID of the first bound REM or DIS device, or None if none found
|
|
732
|
+
:rtype: str | None
|
|
733
|
+
"""
|
|
734
|
+
if not self._bound_devices:
|
|
735
|
+
_LOGGER.debug("No bound devices found for FAN %s", self.id)
|
|
736
|
+
return None
|
|
737
|
+
|
|
738
|
+
# Find first REM or DIS device
|
|
739
|
+
for device_id, device_type in self._bound_devices.items():
|
|
740
|
+
if device_type in (DevType.REM, DevType.DIS):
|
|
741
|
+
_LOGGER.debug(
|
|
742
|
+
"Found bound %s device %s for FAN %s",
|
|
743
|
+
device_type,
|
|
744
|
+
device_id,
|
|
745
|
+
self.id,
|
|
746
|
+
)
|
|
747
|
+
return device_id
|
|
748
|
+
|
|
749
|
+
_LOGGER.debug("No bound REM or DIS devices found for FAN %s", self.id)
|
|
750
|
+
return None
|
|
751
|
+
|
|
752
|
+
def get_fan_param(self, param_id: str) -> Any | None:
|
|
753
|
+
"""Retrieve a fan parameter value from the device's message store.
|
|
754
|
+
|
|
755
|
+
This method attempts to fetch a specific parameter value for a FAN device from the
|
|
756
|
+
stored messages. It first looks for the parameter using a composite key (e.g., '2411_3F')
|
|
757
|
+
and falls back to checking the general 2411 message if needed.
|
|
758
|
+
|
|
759
|
+
:param param_id: The parameter ID to retrieve.
|
|
760
|
+
:type param_id: str
|
|
761
|
+
:return: The parameter value if found, None otherwise
|
|
762
|
+
:rtype: Any | None
|
|
763
|
+
"""
|
|
764
|
+
# Ensure param_id is uppercase and strip leading zeros for consistency
|
|
765
|
+
param_id = (
|
|
766
|
+
str(param_id).upper().lstrip("0") or "0"
|
|
767
|
+
) # Handle case where param_id is "0"
|
|
768
|
+
# we need some extra workarounds to please mypy
|
|
769
|
+
# Create a composite key for this parameter using the normalized ID
|
|
770
|
+
key = f"{Code._2411}_{param_id}"
|
|
771
|
+
|
|
772
|
+
# Get the message using the composite key first, fall back to just the code
|
|
773
|
+
msg = None
|
|
774
|
+
|
|
775
|
+
# First try to get the specific parameter message
|
|
776
|
+
try:
|
|
777
|
+
# Try to access the message directly using the key
|
|
778
|
+
msg = self._msgs[key] # type: ignore[index]
|
|
779
|
+
except (KeyError, TypeError):
|
|
780
|
+
# If that fails, try to find the message by iterating through the dictionary
|
|
781
|
+
msg = next((v for k, v in self._msgs.items() if str(k) == key), None)
|
|
782
|
+
|
|
783
|
+
# If not found, try to get the general 2411 message
|
|
784
|
+
if msg is None:
|
|
785
|
+
msg = self._msgs.get(Code._2411)
|
|
786
|
+
|
|
787
|
+
if not msg or not hasattr(msg, "payload"):
|
|
788
|
+
if not self.supports_2411:
|
|
789
|
+
_LOGGER.debug(
|
|
790
|
+
"Cannot get parameter %s from %s: 2411 parameters not supported",
|
|
791
|
+
param_id,
|
|
792
|
+
self.id,
|
|
793
|
+
)
|
|
794
|
+
else:
|
|
795
|
+
_LOGGER.debug(
|
|
796
|
+
"No payload found for parameter %s on %s", param_id, self.id
|
|
797
|
+
)
|
|
798
|
+
return None
|
|
799
|
+
|
|
800
|
+
# If we have a message but not the specific parameter, try to get it from the payload
|
|
801
|
+
if param_id and hasattr(msg.payload, "get"):
|
|
802
|
+
value = msg.payload.get("value")
|
|
803
|
+
if value is not None:
|
|
804
|
+
return value
|
|
805
|
+
|
|
806
|
+
# If we get here, the parameter wasn't found in the message
|
|
807
|
+
if not self.supports_2411:
|
|
808
|
+
_LOGGER.debug(
|
|
809
|
+
"Parameter %s not found for %s: 2411 parameters not supported",
|
|
810
|
+
param_id,
|
|
811
|
+
self.id,
|
|
812
|
+
)
|
|
813
|
+
else:
|
|
814
|
+
_LOGGER.debug("Parameter %s not found in payload for %s", param_id, self.id)
|
|
815
|
+
|
|
816
|
+
return None
|
|
817
|
+
|
|
365
818
|
@property
|
|
366
819
|
def air_quality(self) -> float | None:
|
|
820
|
+
"""Return the current air quality measurement.
|
|
821
|
+
|
|
822
|
+
:return: The air quality measurement as a float, or None if not available
|
|
823
|
+
:rtype: float | None
|
|
824
|
+
"""
|
|
367
825
|
return self._msg_value(Code._31DA, key=SZ_AIR_QUALITY)
|
|
368
826
|
|
|
369
827
|
@property
|
|
370
828
|
def air_quality_base(self) -> float | None:
|
|
829
|
+
"""Return the base air quality measurement.
|
|
830
|
+
|
|
831
|
+
This represents the baseline or raw air quality measurement before any
|
|
832
|
+
processing or normalization.
|
|
833
|
+
|
|
834
|
+
:return: The base air quality measurement, or None if not available
|
|
835
|
+
:rtype: float | None
|
|
836
|
+
"""
|
|
371
837
|
return self._msg_value(Code._31DA, key=SZ_AIR_QUALITY_BASIS)
|
|
372
838
|
|
|
373
839
|
@property
|
|
@@ -396,6 +862,11 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A]
|
|
|
396
862
|
|
|
397
863
|
@property
|
|
398
864
|
def co2_level(self) -> int | None:
|
|
865
|
+
"""Return the CO2 level in parts per million (ppm).
|
|
866
|
+
|
|
867
|
+
:return: The CO2 level in ppm, or None if not available
|
|
868
|
+
:rtype: int | None
|
|
869
|
+
"""
|
|
399
870
|
return self._msg_value(Code._31DA, key=SZ_CO2_LEVEL)
|
|
400
871
|
|
|
401
872
|
@property
|
|
@@ -419,10 +890,20 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A]
|
|
|
419
890
|
|
|
420
891
|
@property
|
|
421
892
|
def exhaust_flow(self) -> float | None:
|
|
893
|
+
"""Return the current exhaust air flow rate.
|
|
894
|
+
|
|
895
|
+
:return: The exhaust air flow rate in m³/h, or None if not available
|
|
896
|
+
:rtype: float | None
|
|
897
|
+
"""
|
|
422
898
|
return self._msg_value(Code._31DA, key=SZ_EXHAUST_FLOW)
|
|
423
899
|
|
|
424
900
|
@property
|
|
425
901
|
def exhaust_temp(self) -> float | None:
|
|
902
|
+
"""Return the current exhaust air temperature.
|
|
903
|
+
|
|
904
|
+
:return: The exhaust air temperature in degrees Celsius, or None if not available
|
|
905
|
+
:rtype: float | None
|
|
906
|
+
"""
|
|
426
907
|
return self._msg_value(Code._31DA, key=SZ_EXHAUST_TEMP)
|
|
427
908
|
|
|
428
909
|
@property
|
|
@@ -481,10 +962,22 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A]
|
|
|
481
962
|
|
|
482
963
|
@property
|
|
483
964
|
def indoor_temp(self) -> float | None:
|
|
965
|
+
"""Return the current indoor temperature.
|
|
966
|
+
|
|
967
|
+
:return: The indoor temperature in degrees Celsius, or None if not available
|
|
968
|
+
:rtype: float | None
|
|
969
|
+
"""
|
|
484
970
|
return self._msg_value(Code._31DA, key=SZ_INDOOR_TEMP)
|
|
485
971
|
|
|
486
972
|
@property
|
|
487
973
|
def outdoor_humidity(self) -> float | None:
|
|
974
|
+
"""Return the outdoor relative humidity.
|
|
975
|
+
|
|
976
|
+
Handles special case for Ventura devices that send humidity data in 12A0 messages.
|
|
977
|
+
|
|
978
|
+
:return: The outdoor relative humidity as a percentage (0-100), or None if not available
|
|
979
|
+
:rtype: float | None
|
|
980
|
+
"""
|
|
488
981
|
if Code._12A0 in self._msgs and isinstance(
|
|
489
982
|
self._msgs[Code._12A0].payload, list
|
|
490
983
|
): # FAN Ventura sends RH/temps as a list; element [1] contains outdoor_hum
|
|
@@ -495,22 +988,47 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A]
|
|
|
495
988
|
|
|
496
989
|
@property
|
|
497
990
|
def outdoor_temp(self) -> float | None:
|
|
991
|
+
"""Return the outdoor temperature in Celsius.
|
|
992
|
+
|
|
993
|
+
:return: The outdoor temperature in degrees Celsius, or None if not available
|
|
994
|
+
:rtype: float | None
|
|
995
|
+
"""
|
|
498
996
|
return self._msg_value(Code._31DA, key=SZ_OUTDOOR_TEMP)
|
|
499
997
|
|
|
500
998
|
@property
|
|
501
999
|
def post_heat(self) -> int | None:
|
|
1000
|
+
"""Return the post-heat status.
|
|
1001
|
+
|
|
1002
|
+
:return: The post-heat status as an integer, or None if not available
|
|
1003
|
+
:rtype: int | None
|
|
1004
|
+
"""
|
|
502
1005
|
return self._msg_value(Code._31DA, key=SZ_POST_HEAT)
|
|
503
1006
|
|
|
504
1007
|
@property
|
|
505
1008
|
def pre_heat(self) -> int | None:
|
|
1009
|
+
"""Return the pre-heat status.
|
|
1010
|
+
|
|
1011
|
+
:return: The pre-heat status as an integer, or None if not available
|
|
1012
|
+
:rtype: int | None
|
|
1013
|
+
"""
|
|
506
1014
|
return self._msg_value(Code._31DA, key=SZ_PRE_HEAT)
|
|
507
1015
|
|
|
508
1016
|
@property
|
|
509
1017
|
def remaining_mins(self) -> int | None:
|
|
1018
|
+
"""Return the remaining minutes for the current operation.
|
|
1019
|
+
|
|
1020
|
+
:return: The remaining minutes as an integer, or None if not available
|
|
1021
|
+
:rtype: int | None
|
|
1022
|
+
"""
|
|
510
1023
|
return self._msg_value(Code._31DA, key=SZ_REMAINING_MINS)
|
|
511
1024
|
|
|
512
1025
|
@property
|
|
513
1026
|
def request_fan_speed(self) -> float | None:
|
|
1027
|
+
"""Return the requested fan speed.
|
|
1028
|
+
|
|
1029
|
+
:return: The requested fan speed as a percentage, or None if not available
|
|
1030
|
+
:rtype: float | None
|
|
1031
|
+
"""
|
|
514
1032
|
return self._msg_value(Code._2210, key=SZ_REQ_SPEED)
|
|
515
1033
|
|
|
516
1034
|
@property
|
|
@@ -523,18 +1041,40 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A]
|
|
|
523
1041
|
|
|
524
1042
|
@property
|
|
525
1043
|
def speed_cap(self) -> int | None:
|
|
1044
|
+
"""Return the speed capabilities of the fan.
|
|
1045
|
+
|
|
1046
|
+
:return: The speed capabilities as an integer, or None if not available
|
|
1047
|
+
:rtype: int | None
|
|
1048
|
+
"""
|
|
526
1049
|
return self._msg_value(Code._31DA, key=SZ_SPEED_CAPABILITIES)
|
|
527
1050
|
|
|
528
1051
|
@property
|
|
529
1052
|
def supply_fan_speed(self) -> float | None:
|
|
1053
|
+
"""Return the supply fan speed.
|
|
1054
|
+
|
|
1055
|
+
:return: The supply fan speed as a percentage, or None if not available
|
|
1056
|
+
:rtype: float | None
|
|
1057
|
+
"""
|
|
530
1058
|
return self._msg_value(Code._31DA, key=SZ_SUPPLY_FAN_SPEED)
|
|
531
1059
|
|
|
532
1060
|
@property
|
|
533
1061
|
def supply_flow(self) -> float | None:
|
|
1062
|
+
"""Return the supply air flow rate.
|
|
1063
|
+
|
|
1064
|
+
:return: The supply air flow rate in m³/h, or None if not available
|
|
1065
|
+
:rtype: float | None
|
|
1066
|
+
"""
|
|
534
1067
|
return self._msg_value(Code._31DA, key=SZ_SUPPLY_FLOW)
|
|
535
1068
|
|
|
536
1069
|
@property
|
|
537
1070
|
def supply_temp(self) -> float | None:
|
|
1071
|
+
"""Return the supply air temperature.
|
|
1072
|
+
|
|
1073
|
+
Handles special case for Ventura devices that send temperature data in 12A0 messages.
|
|
1074
|
+
|
|
1075
|
+
:return: The supply air temperature in Celsius, or None if not available
|
|
1076
|
+
:rtype: float | None
|
|
1077
|
+
"""
|
|
538
1078
|
if Code._12A0 in self._msgs and isinstance(
|
|
539
1079
|
self._msgs[Code._12A0].payload, list
|
|
540
1080
|
): # FAN Ventura sends RH/temps as a list;
|
|
@@ -559,6 +1099,13 @@ class HvacVentilator(FilterChange): # FAN: RP/31DA, I/31D[9A]
|
|
|
559
1099
|
|
|
560
1100
|
@property
|
|
561
1101
|
def temperature(self) -> float | None: # Celsius
|
|
1102
|
+
"""Return the current temperature in Celsius.
|
|
1103
|
+
|
|
1104
|
+
Handles special cases.
|
|
1105
|
+
|
|
1106
|
+
:return: The temperature in degrees Celsius, or None if not available
|
|
1107
|
+
:rtype: float | None
|
|
1108
|
+
"""
|
|
562
1109
|
if Code._12A0 in self._msgs and isinstance(
|
|
563
1110
|
self._msgs[Code._12A0].payload, list
|
|
564
1111
|
): # FAN Ventura sends RH/temps as a list; use element [1]
|
|
@@ -655,7 +1202,7 @@ _REMOTES = {
|
|
|
655
1202
|
"""
|
|
656
1203
|
# CVE/HRU remote (536-0124) [RFT W: 3 modes, timer]
|
|
657
1204
|
"away": (Code._22F1, 00, 01|04"), # how to invoke?
|
|
658
|
-
"low": (Code._22F1, 00, 02|04"),
|
|
1205
|
+
"low": (Code._22F1, 00, 02|04"), # aka eco
|
|
659
1206
|
"medium": (Code._22F1, 00, 03|04"), # aka auto (with sensors) - is that only for 63?
|
|
660
1207
|
"high": (Code._22F1, 00, 04|04"), # aka full
|
|
661
1208
|
|