ramses-rf 0.51.6__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/helpers.py +1 -1
- ramses_rf/schemas.py +5 -2
- ramses_rf/version.py +1 -1
- {ramses_rf-0.51.6.dist-info → ramses_rf-0.51.8.dist-info}/METADATA +6 -6
- {ramses_rf-0.51.6.dist-info → ramses_rf-0.51.8.dist-info}/RECORD +24 -24
- ramses_tx/__init__.py +25 -4
- ramses_tx/command.py +1449 -138
- ramses_tx/helpers.py +6 -9
- ramses_tx/logger.py +0 -27
- ramses_tx/parsers.py +33 -28
- 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 +44 -3
- ramses_tx/version.py +1 -1
- {ramses_rf-0.51.6.dist-info → ramses_rf-0.51.8.dist-info}/WHEEL +0 -0
- {ramses_rf-0.51.6.dist-info → ramses_rf-0.51.8.dist-info}/entry_points.txt +0 -0
- {ramses_rf-0.51.6.dist-info → ramses_rf-0.51.8.dist-info}/licenses/LICENSE +0 -0
ramses_tx/helpers.py
CHANGED
|
@@ -470,7 +470,7 @@ def parse_valve_demand(
|
|
|
470
470
|
if int(value, 16) & 0xF0 == 0xF0:
|
|
471
471
|
return _faulted_device(SZ_HEAT_DEMAND, value)
|
|
472
472
|
|
|
473
|
-
result = int(value, 16) / 200 # c.f.
|
|
473
|
+
result = int(value, 16) / 200 # c.f. hex_to_percent
|
|
474
474
|
if result == 1.01: # HACK - does it mean maximum?
|
|
475
475
|
result = 1.0
|
|
476
476
|
elif result > 1.0:
|
|
@@ -606,12 +606,9 @@ def _parse_hvac_humidity(
|
|
|
606
606
|
if int(value, 16) & 0xF0 == 0xF0:
|
|
607
607
|
return _faulted_sensor(param_name, value)
|
|
608
608
|
|
|
609
|
-
percentage =
|
|
610
|
-
assert percentage <= 1.0, value # TODO: raise exception if > 1.0?
|
|
609
|
+
percentage = hex_to_percent(value, False) # TODO: confirm not /200
|
|
611
610
|
|
|
612
|
-
result: dict[str, float | str | None] = {
|
|
613
|
-
param_name: percentage
|
|
614
|
-
} # was: percent_from_hex(value, high_res=False)
|
|
611
|
+
result: dict[str, float | str | None] = {param_name: percentage}
|
|
615
612
|
if temp:
|
|
616
613
|
result |= {SZ_TEMPERATURE: hex_to_temp(temp)}
|
|
617
614
|
if dewpoint:
|
|
@@ -790,9 +787,9 @@ def fan_info_to_byte(info: str) -> int:
|
|
|
790
787
|
|
|
791
788
|
def fan_info_flags(flags_list: list[int]) -> int:
|
|
792
789
|
flag_res: int = 0
|
|
793
|
-
for index,
|
|
790
|
+
for index, shift in enumerate(range(7, 4, -1)): # index = 7, 6 and 5
|
|
794
791
|
if flags_list[index] == 1:
|
|
795
|
-
flag_res |= 1 <<
|
|
792
|
+
flag_res |= 1 << shift # set bits
|
|
796
793
|
return flag_res
|
|
797
794
|
|
|
798
795
|
|
|
@@ -881,7 +878,7 @@ def _parse_fan_heater(param_name: str, value: HexStr2) -> Mapping[str, float | N
|
|
|
881
878
|
if int(value, 16) & 0xF0 == 0xF0:
|
|
882
879
|
return _faulted_sensor(param_name, value) # type: ignore[return-value]
|
|
883
880
|
|
|
884
|
-
percentage = int(value, 16) / 200 # Siber DF EVO 2 is /200, not /100 (?
|
|
881
|
+
percentage = int(value, 16) / 200 # Siber DF EVO 2 is /200, not /100 (Others?)
|
|
885
882
|
assert percentage <= 1.0, value # TODO: raise exception if > 1.0?
|
|
886
883
|
|
|
887
884
|
return {param_name: percentage} # was: percent_from_hex(value, high_res=False)
|
ramses_tx/logger.py
CHANGED
|
@@ -173,33 +173,6 @@ class TimedRotatingFileHandler(_TimedRotatingFileHandler):
|
|
|
173
173
|
# self.doRollover()
|
|
174
174
|
# return super().emit(record)
|
|
175
175
|
|
|
176
|
-
# To fix issue ramses_cc 293, test if this override is still required
|
|
177
|
-
# async def getFilesToDelete(self) -> list[str]: # zxdavb: my version
|
|
178
|
-
# """Determine the files to delete when rolling over.
|
|
179
|
-
#
|
|
180
|
-
# Overridden as old log files were not being deleted.
|
|
181
|
-
# """
|
|
182
|
-
# # See bpo-44753 (this code is as was before that commit), bpo45628, bpo-46063
|
|
183
|
-
# dirName, baseName = os.path.split(self.baseFilename)
|
|
184
|
-
# loop = asyncio.get_running_loop()
|
|
185
|
-
# # Must run async in executor to prevent HA blocking call on rollover (ramses_cc issue 293)
|
|
186
|
-
# file_names = await loop.run_in_executor(None, os.listdir, dirName) < doesn't work
|
|
187
|
-
#
|
|
188
|
-
# result = []
|
|
189
|
-
# prefix = baseName + "."
|
|
190
|
-
# plen = len(prefix)
|
|
191
|
-
# for fileName in file_names:
|
|
192
|
-
# if fileName[:plen] == prefix:
|
|
193
|
-
# suffix = fileName[plen:]
|
|
194
|
-
# if self.extMatch.match(suffix):
|
|
195
|
-
# result.append(os.path.join(dirName, fileName))
|
|
196
|
-
# if len(result) < self.backupCount:
|
|
197
|
-
# result = []
|
|
198
|
-
# else:
|
|
199
|
-
# result.sort()
|
|
200
|
-
# result = result[: len(result) - self.backupCount]
|
|
201
|
-
# return result
|
|
202
|
-
|
|
203
176
|
|
|
204
177
|
def getLogger( # permits a bespoke Logger class
|
|
205
178
|
name: str | None = None, pkt_log: bool = False
|
ramses_tx/parsers.py
CHANGED
|
@@ -1391,15 +1391,18 @@ def parser_2210(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
1391
1391
|
"02", # requested by CO2 level/sensor
|
|
1392
1392
|
"03", # requested by humidity level/sensor
|
|
1393
1393
|
), f"expected req_reason (00|02|03), not {payload[20:22]}"
|
|
1394
|
-
assert payload[78:80] in (
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1394
|
+
assert payload[78:80] in (
|
|
1395
|
+
"00",
|
|
1396
|
+
"02",
|
|
1397
|
+
), f"expected byte 39 (00|02), not {payload[78:80]}"
|
|
1398
|
+
assert payload[80:82] in (
|
|
1399
|
+
"01",
|
|
1400
|
+
"08",
|
|
1401
|
+
), f"expected byte 40 (01|08), not {payload[80:82]}"
|
|
1402
|
+
assert payload[82:] in (
|
|
1403
|
+
"00",
|
|
1404
|
+
"40",
|
|
1405
|
+
), f"expected byte 41- (00|40), not {payload[82:]}"
|
|
1403
1406
|
|
|
1404
1407
|
except AssertionError as err:
|
|
1405
1408
|
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({err})")
|
|
@@ -1648,21 +1651,17 @@ def parser_22f3(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
1648
1651
|
_LOGGER.warning(f"{msg!r} < {_INFORM_DEV_MSG} ({err})")
|
|
1649
1652
|
|
|
1650
1653
|
new_speed = { # from now, until timer expiry
|
|
1651
|
-
0x00: "fan_boost", #
|
|
1652
|
-
0x01: "per_request", #
|
|
1653
|
-
0x02: "
|
|
1654
|
+
0x00: "fan_boost", # set fan off, or 'boost' mode?
|
|
1655
|
+
0x01: "per_request?", # set fan as per payload[6:10]?
|
|
1656
|
+
0x02: "per_request", # set fan as per payload[6:10]
|
|
1654
1657
|
}.get(int(payload[2:4], 0x10) & 0x07) # 0b0000-0111
|
|
1655
1658
|
|
|
1656
|
-
fallback_speed
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
# set fan as per
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
0x08: "fan_off", # # set fan off?
|
|
1663
|
-
0x10: "per_request", # # set fan as per payload[6:10], or payload[10:]?
|
|
1664
|
-
0x18: "per_vent_speed", # set fan as per current fan mode/speed?
|
|
1665
|
-
}.get(int(payload[2:4], 0x10) & 0x38) # 0b0011-1000
|
|
1659
|
+
fallback_speed = { # after timer expiry
|
|
1660
|
+
0x00: "per_vent_speed", # set fan as per current fan mode
|
|
1661
|
+
0x08: "fan_off", # set fan off?
|
|
1662
|
+
0x10: "per_request", # set fan as per payload[10:14]
|
|
1663
|
+
0x18: "per_vent_speed?", # set fan as per current fan mode/speed?
|
|
1664
|
+
}.get(int(payload[2:4], 0x10) & 0x38) # 0b0011-1000
|
|
1666
1665
|
|
|
1667
1666
|
units = {
|
|
1668
1667
|
0x00: "minutes",
|
|
@@ -1677,15 +1676,21 @@ def parser_22f3(payload: str, msg: Message) -> dict[str, Any]:
|
|
|
1677
1676
|
result = {
|
|
1678
1677
|
"minutes" if units != "index" else "index": duration,
|
|
1679
1678
|
"flags": hex_to_flag8(payload[2:4]),
|
|
1680
|
-
"
|
|
1681
|
-
"
|
|
1679
|
+
"new_speed_mode": new_speed,
|
|
1680
|
+
"fallback_speed_mode": fallback_speed,
|
|
1682
1681
|
}
|
|
1683
1682
|
|
|
1684
|
-
if msg.
|
|
1685
|
-
result["
|
|
1683
|
+
if msg._addrs[0] == NON_DEV_ADDR and msg.len <= 3:
|
|
1684
|
+
result["_scheme"] = "itho"
|
|
1685
|
+
|
|
1686
|
+
if msg.len >= 5 and payload[6:10] != "0000": # new speed
|
|
1687
|
+
mode_info = parser_22f1(f"00{payload[6:10]}", msg)
|
|
1688
|
+
result["_scheme"] = mode_info.get("_scheme")
|
|
1689
|
+
result["fan_mode"] = mode_info.get("fan_mode")
|
|
1686
1690
|
|
|
1687
|
-
if msg.len >= 7: # fallback speed
|
|
1688
|
-
|
|
1691
|
+
if msg.len >= 7 and payload[10:14] != "0000": # fallback speed
|
|
1692
|
+
mode_info = parser_22f1(f"00{payload[10:14]}", msg)
|
|
1693
|
+
result["fallback_fan_mode"] = mode_info.get("fan_mode")
|
|
1689
1694
|
|
|
1690
1695
|
return result
|
|
1691
1696
|
|
ramses_tx/protocol.py
CHANGED
|
@@ -332,7 +332,7 @@ class _DeviceIdFilterMixin(_BaseProtocol):
|
|
|
332
332
|
/,
|
|
333
333
|
*,
|
|
334
334
|
disable_warnings: bool = False,
|
|
335
|
-
|
|
335
|
+
strict_checking: bool = False,
|
|
336
336
|
) -> DeviceIdT | None:
|
|
337
337
|
"""Return the device_id of the gateway specified in the include_list, if any.
|
|
338
338
|
|
|
@@ -384,7 +384,7 @@ class _DeviceIdFilterMixin(_BaseProtocol):
|
|
|
384
384
|
f"The {SZ_KNOWN_LIST} includes exactly one gateway (HGI): {known_hgi}"
|
|
385
385
|
)
|
|
386
386
|
|
|
387
|
-
if
|
|
387
|
+
if strict_checking:
|
|
388
388
|
return known_hgi if [known_hgi] == explicit_hgis else None
|
|
389
389
|
return known_hgi
|
|
390
390
|
|
ramses_tx/protocol_fsm.py
CHANGED
|
@@ -291,7 +291,7 @@ class ProtocolContext:
|
|
|
291
291
|
# may want to set some instance variables, according to type of transport
|
|
292
292
|
self._state.connection_made()
|
|
293
293
|
|
|
294
|
-
# TODO: Should we clear the buffer if connection is lost (and
|
|
294
|
+
# TODO: Should we clear the buffer if connection is lost (and apologise to senders?
|
|
295
295
|
def connection_lost(self, err: ExceptionT | None) -> None:
|
|
296
296
|
self._state.connection_lost()
|
|
297
297
|
|
ramses_tx/ramses.py
CHANGED
|
@@ -223,7 +223,7 @@ CODES_SCHEMA: dict[Code, dict[str, Any]] = { # rf_unknown
|
|
|
223
223
|
SZ_NAME: "device_info",
|
|
224
224
|
I_: r"^(00|FF)([0-9A-F]{30,})?$", # r"^[0-9A-F]{32,}$" might be OK
|
|
225
225
|
RQ: r"^00$", # NOTE: 63 seen (no RP), some devices will accept [0-9A-F]{2}
|
|
226
|
-
# RP: r"^[0-9A-F]{2}([0-9A-F]){30,}$", # NOTE:
|
|
226
|
+
# RP: r"^[0-9A-F]{2}([0-9A-F]){30,}$", # NOTE: index same as RQ
|
|
227
227
|
SZ_LIFESPAN: False,
|
|
228
228
|
},
|
|
229
229
|
Code._10E1: { # device_id
|
|
@@ -480,6 +480,7 @@ CODES_SCHEMA: dict[Code, dict[str, Any]] = { # rf_unknown
|
|
|
480
480
|
SZ_NAME: "fan_params",
|
|
481
481
|
I_: r"^(00|01|15|16|17|21)00[0-9A-F]{6}([0-9A-F]{8}){4}[0-9A-F]{4}$",
|
|
482
482
|
RQ: r"^(00|01|15|16|17|21)00[0-9A-F]{2}((00){19})?$",
|
|
483
|
+
RP: r"^(00|01|15|16|17|21)00[0-9A-F]{6}[0-9A-F]{8}(([0-9A-F]{8}){3}[0-9A-F]{4})?$",
|
|
483
484
|
W_: r"^(00|01|15|16|17|21)00[0-9A-F]{6}[0-9A-F]{8}(([0-9A-F]{8}){3}[0-9A-F]{4})?$",
|
|
484
485
|
},
|
|
485
486
|
Code._2420: { # unknown_2420, from OTB
|
|
@@ -1220,6 +1221,7 @@ SZ_MIN_VALUE: Final = "min_value"
|
|
|
1220
1221
|
SZ_MAX_VALUE: Final = "max_value"
|
|
1221
1222
|
SZ_PRECISION: Final = "precision"
|
|
1222
1223
|
SZ_DATA_TYPE: Final = "data_type"
|
|
1224
|
+
SZ_DATA_UNIT: Final = "data_unit"
|
|
1223
1225
|
|
|
1224
1226
|
_22F1_MODE_ITHO: dict[str, str] = {
|
|
1225
1227
|
"00": "off", # not seen
|
|
@@ -1265,12 +1267,13 @@ _22F1_SCHEMES: dict[str, dict[str, str]] = {
|
|
|
1265
1267
|
|
|
1266
1268
|
# unclear if true for only Orcon/*all* models
|
|
1267
1269
|
_2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
|
|
1268
|
-
"31": { # slot 09
|
|
1270
|
+
"31": { # slot 09 (FANs produced after 2021)
|
|
1269
1271
|
SZ_DESCRIPTION: "Time to change filter (days)",
|
|
1270
1272
|
SZ_MIN_VALUE: 0,
|
|
1271
1273
|
SZ_MAX_VALUE: 1800,
|
|
1272
1274
|
SZ_PRECISION: 30,
|
|
1273
1275
|
SZ_DATA_TYPE: "10",
|
|
1276
|
+
SZ_DATA_UNIT: "days",
|
|
1274
1277
|
},
|
|
1275
1278
|
"3D": { # slot 00
|
|
1276
1279
|
SZ_DESCRIPTION: "Away mode Supply fan rate (%)",
|
|
@@ -1278,6 +1281,7 @@ _2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
|
|
|
1278
1281
|
SZ_MAX_VALUE: 0.4,
|
|
1279
1282
|
SZ_PRECISION: 0.005,
|
|
1280
1283
|
SZ_DATA_TYPE: "0F",
|
|
1284
|
+
SZ_DATA_UNIT: "%",
|
|
1281
1285
|
},
|
|
1282
1286
|
"3E": { # slot 01
|
|
1283
1287
|
SZ_DESCRIPTION: "Away mode Exhaust fan rate (%)",
|
|
@@ -1285,6 +1289,7 @@ _2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
|
|
|
1285
1289
|
SZ_MAX_VALUE: 0.4,
|
|
1286
1290
|
SZ_PRECISION: 0.005,
|
|
1287
1291
|
SZ_DATA_TYPE: "0F",
|
|
1292
|
+
SZ_DATA_UNIT: "%",
|
|
1288
1293
|
},
|
|
1289
1294
|
"3F": { # slot 02
|
|
1290
1295
|
SZ_DESCRIPTION: "Low mode Supply fan rate (%)",
|
|
@@ -1292,6 +1297,7 @@ _2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
|
|
|
1292
1297
|
SZ_MAX_VALUE: 0.8,
|
|
1293
1298
|
SZ_PRECISION: 0.005,
|
|
1294
1299
|
SZ_DATA_TYPE: "0F",
|
|
1300
|
+
SZ_DATA_UNIT: "%",
|
|
1295
1301
|
},
|
|
1296
1302
|
"40": { # slot 03
|
|
1297
1303
|
SZ_DESCRIPTION: "Low mode Exhaust fan rate (%)",
|
|
@@ -1299,13 +1305,15 @@ _2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
|
|
|
1299
1305
|
SZ_MAX_VALUE: 0.8,
|
|
1300
1306
|
SZ_PRECISION: 0.005,
|
|
1301
1307
|
SZ_DATA_TYPE: "0F",
|
|
1308
|
+
SZ_DATA_UNIT: "%",
|
|
1302
1309
|
},
|
|
1303
1310
|
"41": { # slot 04
|
|
1304
1311
|
SZ_DESCRIPTION: "Medium mode Supply fan rate (%)",
|
|
1305
|
-
SZ_MIN_VALUE: 0.0,
|
|
1312
|
+
SZ_MIN_VALUE: 0.1, # Orcon FAN responds with 0.0, but I guess this should be the same as for "42"
|
|
1306
1313
|
SZ_MAX_VALUE: 1.0,
|
|
1307
1314
|
SZ_PRECISION: 0.005,
|
|
1308
1315
|
SZ_DATA_TYPE: "0F",
|
|
1316
|
+
SZ_DATA_UNIT: "%",
|
|
1309
1317
|
},
|
|
1310
1318
|
"42": { # slot 05
|
|
1311
1319
|
SZ_DESCRIPTION: "Medium mode Exhaust fan rate (%)",
|
|
@@ -1313,13 +1321,15 @@ _2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
|
|
|
1313
1321
|
SZ_MAX_VALUE: 1.0,
|
|
1314
1322
|
SZ_PRECISION: 0.005,
|
|
1315
1323
|
SZ_DATA_TYPE: "0F",
|
|
1324
|
+
SZ_DATA_UNIT: "%",
|
|
1316
1325
|
},
|
|
1317
1326
|
"43": { # slot 06
|
|
1318
1327
|
SZ_DESCRIPTION: "High mode Supply fan rate (%)",
|
|
1319
|
-
SZ_MIN_VALUE: 0.
|
|
1328
|
+
SZ_MIN_VALUE: 0.1,
|
|
1320
1329
|
SZ_MAX_VALUE: 1.0,
|
|
1321
1330
|
SZ_PRECISION: 0.005,
|
|
1322
1331
|
SZ_DATA_TYPE: "0F",
|
|
1332
|
+
SZ_DATA_UNIT: "%",
|
|
1323
1333
|
},
|
|
1324
1334
|
"44": { # slot 07
|
|
1325
1335
|
SZ_DESCRIPTION: "High mode Exhaust fan rate (%)",
|
|
@@ -1327,6 +1337,15 @@ _2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
|
|
|
1327
1337
|
SZ_MAX_VALUE: 1.0,
|
|
1328
1338
|
SZ_PRECISION: 0.005,
|
|
1329
1339
|
SZ_DATA_TYPE: "0F",
|
|
1340
|
+
SZ_DATA_UNIT: "%",
|
|
1341
|
+
},
|
|
1342
|
+
"4B": { # slot 09 (FANs produced before 2021) Also check code 22F7
|
|
1343
|
+
SZ_DESCRIPTION: "(Test) Bypass Valve (0=auto, 1=open, 2=closed)",
|
|
1344
|
+
SZ_MIN_VALUE: 0,
|
|
1345
|
+
SZ_MAX_VALUE: 2,
|
|
1346
|
+
SZ_PRECISION: 1,
|
|
1347
|
+
SZ_DATA_TYPE: "00",
|
|
1348
|
+
SZ_DATA_UNIT: "",
|
|
1330
1349
|
},
|
|
1331
1350
|
"4E": { # slot 0A
|
|
1332
1351
|
SZ_DESCRIPTION: "Moisture scenario position (0=medium, 1=high)",
|
|
@@ -1334,13 +1353,15 @@ _2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
|
|
|
1334
1353
|
SZ_MAX_VALUE: 1,
|
|
1335
1354
|
SZ_PRECISION: 1,
|
|
1336
1355
|
SZ_DATA_TYPE: "00",
|
|
1356
|
+
SZ_DATA_UNIT: "",
|
|
1337
1357
|
},
|
|
1338
1358
|
"52": { # slot 0B
|
|
1339
1359
|
SZ_DESCRIPTION: "Sensor sensitivity (%)",
|
|
1340
1360
|
SZ_MIN_VALUE: 0,
|
|
1341
1361
|
SZ_MAX_VALUE: 25.0,
|
|
1342
1362
|
SZ_PRECISION: 0.1,
|
|
1343
|
-
SZ_DATA_TYPE: "
|
|
1363
|
+
SZ_DATA_TYPE: "01",
|
|
1364
|
+
SZ_DATA_UNIT: "%",
|
|
1344
1365
|
},
|
|
1345
1366
|
"54": { # slot 0C
|
|
1346
1367
|
SZ_DESCRIPTION: "Moisture sensor overrun time (mins)",
|
|
@@ -1348,13 +1369,15 @@ _2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
|
|
|
1348
1369
|
SZ_MAX_VALUE: 60,
|
|
1349
1370
|
SZ_PRECISION: 1,
|
|
1350
1371
|
SZ_DATA_TYPE: "00",
|
|
1372
|
+
SZ_DATA_UNIT: "min",
|
|
1351
1373
|
},
|
|
1352
1374
|
"75": { # slot 0D
|
|
1353
1375
|
SZ_DESCRIPTION: "Comfort temperature (°C)",
|
|
1354
1376
|
SZ_MIN_VALUE: 0.0,
|
|
1355
1377
|
SZ_MAX_VALUE: 30.0,
|
|
1356
1378
|
SZ_PRECISION: 0.01,
|
|
1357
|
-
SZ_DATA_TYPE: 92,
|
|
1379
|
+
SZ_DATA_TYPE: "92",
|
|
1380
|
+
SZ_DATA_UNIT: "°C",
|
|
1358
1381
|
},
|
|
1359
1382
|
"95": { # slot 08
|
|
1360
1383
|
SZ_DESCRIPTION: "Boost mode Supply/exhaust fan rate (%)",
|
|
@@ -1362,6 +1385,7 @@ _2411_PARAMS_SCHEMA: dict[str, dict[str, Any]] = {
|
|
|
1362
1385
|
SZ_MAX_VALUE: 1.0,
|
|
1363
1386
|
SZ_PRECISION: 0.005,
|
|
1364
1387
|
SZ_DATA_TYPE: "0F",
|
|
1388
|
+
SZ_DATA_UNIT: "%",
|
|
1365
1389
|
},
|
|
1366
1390
|
}
|
|
1367
1391
|
|
|
@@ -1456,3 +1480,19 @@ _31DA_FAN_INFO: dict[int, str] = {
|
|
|
1456
1480
|
# RAMSES_ZONES_ALL = RAMSES_ZONES.pop("ALL")
|
|
1457
1481
|
# RAMSES_ZONES_DHW = RAMSES_ZONES[ZON_ROLE.DHW]
|
|
1458
1482
|
# [RAMSES_ZONES[k].update(RAMSES_ZONES_ALL) for k in RAMSES_ZONES if k != ZON_ROLE.DHW]
|
|
1483
|
+
|
|
1484
|
+
__all__ = [
|
|
1485
|
+
"CODES_BY_DEV_SLUG",
|
|
1486
|
+
"CODES_SCHEMA",
|
|
1487
|
+
"CODE_NAME_LOOKUP",
|
|
1488
|
+
"CODES_BY_DEV_SLUG",
|
|
1489
|
+
"CODES_SCHEMA",
|
|
1490
|
+
"HVAC_KLASS_BY_VC_PAIR",
|
|
1491
|
+
"_2411_PARAMS_SCHEMA",
|
|
1492
|
+
"SZ_DESCRIPTION",
|
|
1493
|
+
"SZ_MIN_VALUE",
|
|
1494
|
+
"SZ_MAX_VALUE",
|
|
1495
|
+
"SZ_PRECISION",
|
|
1496
|
+
"SZ_DATA_TYPE",
|
|
1497
|
+
"SZ_DATA_UNIT",
|
|
1498
|
+
]
|
ramses_tx/schemas.py
CHANGED
|
@@ -215,6 +215,7 @@ def ConvertNullToDict() -> Callable[[_T | None], _T | dict[Never, Never]]:
|
|
|
215
215
|
|
|
216
216
|
|
|
217
217
|
SZ_ALIAS: Final = "alias"
|
|
218
|
+
SZ_BOUND_TO: Final = "bound"
|
|
218
219
|
SZ_CLASS: Final = "class"
|
|
219
220
|
SZ_FAKED: Final = "faked"
|
|
220
221
|
SZ_SCHEME: Final = "scheme"
|
|
@@ -296,6 +297,8 @@ def sch_global_traits_dict_factory(
|
|
|
296
297
|
vol.Optional(SZ_CLASS, default="HVC"): vol.Any(
|
|
297
298
|
None, *hvac_slugs, *(str(DEV_TYPE_MAP[s]) for s in hvac_slugs)
|
|
298
299
|
), # TODO: consider removing None
|
|
300
|
+
# Add 'bound' trait for FAN devices
|
|
301
|
+
vol.Optional(SZ_BOUND_TO): vol.Any(None, vol.Match(DEVICE_ID_REGEX.ANY)),
|
|
299
302
|
}
|
|
300
303
|
)
|
|
301
304
|
SCH_TRAITS_HVAC = SCH_TRAITS_HVAC.extend(
|
ramses_tx/transport.py
CHANGED
|
@@ -1033,6 +1033,8 @@ class MqttTransport(_FullTransport, _MqttTransportAbstractor):
|
|
|
1033
1033
|
self._topic_base = validate_topic_path(self._broker_url.path)
|
|
1034
1034
|
self._topic_pub = ""
|
|
1035
1035
|
self._topic_sub = ""
|
|
1036
|
+
# Track if we've subscribed to a wildcard data topic (e.g. ".../+/rx")
|
|
1037
|
+
self._data_wildcard_topic = ""
|
|
1036
1038
|
|
|
1037
1039
|
self._mqtt_qos = int(parse_qs(self._broker_url.query).get("qos", ["0"])[0])
|
|
1038
1040
|
|
|
@@ -1143,17 +1145,30 @@ class MqttTransport(_FullTransport, _MqttTransportAbstractor):
|
|
|
1143
1145
|
# Subscribe to base topic to see 'online' messages
|
|
1144
1146
|
self.client.subscribe(self._topic_base) # hope to see 'online' message
|
|
1145
1147
|
|
|
1146
|
-
# Also subscribe to data topics with wildcard for reliability
|
|
1147
|
-
#
|
|
1148
|
-
|
|
1148
|
+
# Also subscribe to data topics with wildcard for reliability, but only
|
|
1149
|
+
# until a specific device topic is known. Once _topic_sub is set, avoid
|
|
1150
|
+
# overlapping subscriptions that would duplicate messages.
|
|
1151
|
+
if self._topic_base.endswith("/+") and not (
|
|
1152
|
+
hasattr(self, "_topic_sub") and self._topic_sub
|
|
1153
|
+
):
|
|
1149
1154
|
data_wildcard = self._topic_base.replace("/+", "/+/rx")
|
|
1150
1155
|
self.client.subscribe(data_wildcard, qos=self._mqtt_qos)
|
|
1156
|
+
self._data_wildcard_topic = data_wildcard
|
|
1151
1157
|
_LOGGER.debug(f"Subscribed to data wildcard: {data_wildcard}")
|
|
1152
1158
|
|
|
1153
1159
|
# If we already have specific topics, re-subscribe to them
|
|
1154
1160
|
if hasattr(self, "_topic_sub") and self._topic_sub:
|
|
1155
1161
|
self.client.subscribe(self._topic_sub, qos=self._mqtt_qos)
|
|
1156
1162
|
_LOGGER.debug(f"Re-subscribed to specific topic: {self._topic_sub}")
|
|
1163
|
+
# If we had a wildcard subscription, drop it to prevent duplicates
|
|
1164
|
+
if getattr(self, "_data_wildcard_topic", ""):
|
|
1165
|
+
try:
|
|
1166
|
+
self.client.unsubscribe(self._data_wildcard_topic)
|
|
1167
|
+
_LOGGER.debug(
|
|
1168
|
+
f"Unsubscribed data wildcard after specific subscribe: {self._data_wildcard_topic}"
|
|
1169
|
+
)
|
|
1170
|
+
finally:
|
|
1171
|
+
self._data_wildcard_topic = ""
|
|
1157
1172
|
|
|
1158
1173
|
def _on_connect_fail(
|
|
1159
1174
|
self,
|
|
@@ -1225,6 +1240,17 @@ class MqttTransport(_FullTransport, _MqttTransportAbstractor):
|
|
|
1225
1240
|
|
|
1226
1241
|
self.client.subscribe(self._topic_sub, qos=self._mqtt_qos)
|
|
1227
1242
|
|
|
1243
|
+
# If we previously subscribed to a wildcard data topic, unsubscribe now
|
|
1244
|
+
# to avoid duplicate delivery (wildcard and specific both matching)
|
|
1245
|
+
if getattr(self, "_data_wildcard_topic", ""):
|
|
1246
|
+
try:
|
|
1247
|
+
self.client.unsubscribe(self._data_wildcard_topic)
|
|
1248
|
+
_LOGGER.debug(
|
|
1249
|
+
f"Unsubscribed data wildcard after device online: {self._data_wildcard_topic}"
|
|
1250
|
+
)
|
|
1251
|
+
finally:
|
|
1252
|
+
self._data_wildcard_topic = ""
|
|
1253
|
+
|
|
1228
1254
|
# Only call connection_made on first connection, not reconnections
|
|
1229
1255
|
if not self._connection_established:
|
|
1230
1256
|
self._connection_established = True
|
|
@@ -1295,6 +1321,21 @@ class MqttTransport(_FullTransport, _MqttTransportAbstractor):
|
|
|
1295
1321
|
self._connection_established = True
|
|
1296
1322
|
self._make_connection(gwy_id=gateway_id) # type: ignore[arg-type]
|
|
1297
1323
|
|
|
1324
|
+
# Ensure we subscribe specifically to the device topic and drop the
|
|
1325
|
+
# wildcard subscription to prevent duplicates
|
|
1326
|
+
try:
|
|
1327
|
+
self.client.subscribe(self._topic_sub, qos=self._mqtt_qos)
|
|
1328
|
+
except Exception as err: # pragma: no cover - defensive
|
|
1329
|
+
_LOGGER.debug(f"Error subscribing specific topic: {err}")
|
|
1330
|
+
if getattr(self, "_data_wildcard_topic", ""):
|
|
1331
|
+
try:
|
|
1332
|
+
self.client.unsubscribe(self._data_wildcard_topic)
|
|
1333
|
+
_LOGGER.debug(
|
|
1334
|
+
f"Unsubscribed data wildcard after inferring device: {self._data_wildcard_topic}"
|
|
1335
|
+
)
|
|
1336
|
+
finally:
|
|
1337
|
+
self._data_wildcard_topic = ""
|
|
1338
|
+
|
|
1298
1339
|
try:
|
|
1299
1340
|
payload = json.loads(msg.payload)
|
|
1300
1341
|
except json.JSONDecodeError:
|
ramses_tx/version.py
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|