ramses-rf 0.22.2__py3-none-any.whl → 0.51.1__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_cli/__init__.py +18 -0
- ramses_cli/client.py +597 -0
- ramses_cli/debug.py +20 -0
- ramses_cli/discovery.py +405 -0
- ramses_cli/utils/cat_slow.py +17 -0
- ramses_cli/utils/convert.py +60 -0
- ramses_rf/__init__.py +31 -10
- ramses_rf/binding_fsm.py +787 -0
- ramses_rf/const.py +124 -105
- ramses_rf/database.py +297 -0
- ramses_rf/device/__init__.py +69 -39
- ramses_rf/device/base.py +187 -376
- ramses_rf/device/heat.py +540 -552
- ramses_rf/device/hvac.py +286 -171
- ramses_rf/dispatcher.py +153 -177
- ramses_rf/entity_base.py +478 -361
- ramses_rf/exceptions.py +82 -0
- ramses_rf/gateway.py +378 -514
- ramses_rf/helpers.py +57 -19
- ramses_rf/py.typed +0 -0
- ramses_rf/schemas.py +148 -194
- ramses_rf/system/__init__.py +16 -23
- ramses_rf/system/faultlog.py +363 -0
- ramses_rf/system/heat.py +295 -302
- ramses_rf/system/schedule.py +312 -198
- ramses_rf/system/zones.py +318 -238
- ramses_rf/version.py +2 -8
- ramses_rf-0.51.1.dist-info/METADATA +72 -0
- ramses_rf-0.51.1.dist-info/RECORD +55 -0
- {ramses_rf-0.22.2.dist-info → ramses_rf-0.51.1.dist-info}/WHEEL +1 -2
- ramses_rf-0.51.1.dist-info/entry_points.txt +2 -0
- {ramses_rf-0.22.2.dist-info → ramses_rf-0.51.1.dist-info/licenses}/LICENSE +1 -1
- ramses_tx/__init__.py +160 -0
- {ramses_rf/protocol → ramses_tx}/address.py +65 -59
- ramses_tx/command.py +1454 -0
- ramses_tx/const.py +903 -0
- ramses_tx/exceptions.py +92 -0
- {ramses_rf/protocol → ramses_tx}/fingerprints.py +56 -15
- {ramses_rf/protocol → ramses_tx}/frame.py +132 -131
- ramses_tx/gateway.py +338 -0
- ramses_tx/helpers.py +883 -0
- {ramses_rf/protocol → ramses_tx}/logger.py +67 -53
- {ramses_rf/protocol → ramses_tx}/message.py +155 -191
- ramses_tx/opentherm.py +1260 -0
- ramses_tx/packet.py +210 -0
- ramses_tx/parsers.py +2957 -0
- ramses_tx/protocol.py +801 -0
- ramses_tx/protocol_fsm.py +672 -0
- ramses_tx/py.typed +0 -0
- {ramses_rf/protocol → ramses_tx}/ramses.py +262 -185
- {ramses_rf/protocol → ramses_tx}/schemas.py +150 -133
- ramses_tx/transport.py +1471 -0
- ramses_tx/typed_dicts.py +492 -0
- ramses_tx/typing.py +181 -0
- ramses_tx/version.py +4 -0
- ramses_rf/discovery.py +0 -398
- ramses_rf/protocol/__init__.py +0 -59
- ramses_rf/protocol/backports.py +0 -42
- ramses_rf/protocol/command.py +0 -1561
- ramses_rf/protocol/const.py +0 -697
- ramses_rf/protocol/exceptions.py +0 -111
- ramses_rf/protocol/helpers.py +0 -390
- ramses_rf/protocol/opentherm.py +0 -1170
- ramses_rf/protocol/packet.py +0 -235
- ramses_rf/protocol/parsers.py +0 -2673
- ramses_rf/protocol/protocol.py +0 -613
- ramses_rf/protocol/transport.py +0 -1011
- ramses_rf/protocol/version.py +0 -10
- ramses_rf/system/hvac.py +0 -82
- ramses_rf-0.22.2.dist-info/METADATA +0 -64
- ramses_rf-0.22.2.dist-info/RECORD +0 -42
- ramses_rf-0.22.2.dist-info/top_level.txt +0 -1
ramses_rf/device/heat.py
CHANGED
|
@@ -1,27 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
|
|
3
|
-
#
|
|
4
|
-
"""RAMSES RF - a RAMSES-II protocol decoder & analyser.
|
|
2
|
+
"""RAMSES RF - devices from the CH/DHW (heat) domain."""
|
|
5
3
|
|
|
6
|
-
Heating devices.
|
|
7
|
-
"""
|
|
8
4
|
from __future__ import annotations
|
|
9
5
|
|
|
10
6
|
import logging
|
|
11
|
-
from
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from typing import TYPE_CHECKING, Any, Final
|
|
12
9
|
|
|
13
|
-
from
|
|
10
|
+
from ramses_rf import exceptions as exc
|
|
11
|
+
from ramses_rf.const import (
|
|
14
12
|
DEV_ROLE_MAP,
|
|
15
|
-
DEV_TYPE,
|
|
16
13
|
DEV_TYPE_MAP,
|
|
17
14
|
DOMAIN_TYPE_MAP,
|
|
18
15
|
SZ_DEVICES,
|
|
19
16
|
SZ_DOMAIN_ID,
|
|
20
17
|
SZ_HEAT_DEMAND,
|
|
21
18
|
SZ_PRESSURE,
|
|
22
|
-
SZ_PRIORITY,
|
|
23
19
|
SZ_RELAY_DEMAND,
|
|
24
|
-
SZ_RETRIES,
|
|
25
20
|
SZ_SETPOINT,
|
|
26
21
|
SZ_TEMPERATURE,
|
|
27
22
|
SZ_UFH_IDX,
|
|
@@ -30,92 +25,100 @@ from ..const import (
|
|
|
30
25
|
SZ_ZONE_MASK,
|
|
31
26
|
SZ_ZONE_TYPE,
|
|
32
27
|
ZON_ROLE_MAP,
|
|
33
|
-
|
|
28
|
+
DevType,
|
|
34
29
|
)
|
|
35
|
-
from
|
|
36
|
-
from
|
|
37
|
-
from
|
|
38
|
-
from
|
|
39
|
-
from
|
|
40
|
-
from
|
|
41
|
-
from
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
30
|
+
from ramses_rf.device import Device
|
|
31
|
+
from ramses_rf.entity_base import Child, Entity, Parent, class_by_attr
|
|
32
|
+
from ramses_rf.helpers import shrink
|
|
33
|
+
from ramses_rf.schemas import SCH_TCS, SZ_ACTUATORS, SZ_CIRCUITS
|
|
34
|
+
from ramses_tx import NON_DEV_ADDR, Command, Priority
|
|
35
|
+
from ramses_tx.const import SZ_NUM_REPEATS, SZ_PRIORITY, MsgId
|
|
36
|
+
from ramses_tx.opentherm import (
|
|
37
|
+
PARAMS_DATA_IDS,
|
|
38
|
+
SCHEMA_DATA_IDS,
|
|
39
|
+
STATUS_DATA_IDS,
|
|
40
|
+
SZ_MSG_ID,
|
|
41
|
+
SZ_MSG_NAME,
|
|
42
|
+
SZ_MSG_TYPE,
|
|
43
|
+
SZ_VALUE,
|
|
49
44
|
OtMsgType,
|
|
50
45
|
)
|
|
51
|
-
from
|
|
52
|
-
from
|
|
53
|
-
from .base import BatteryState, Device, DeviceHeat, Fakeable
|
|
46
|
+
from ramses_tx.ramses import CODES_OF_HEAT_DOMAIN_ONLY, CODES_ONLY_FROM_CTL
|
|
47
|
+
from ramses_tx.typed_dicts import PayDictT
|
|
54
48
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
RP,
|
|
59
|
-
RQ,
|
|
60
|
-
W_,
|
|
49
|
+
from .base import BatteryState, DeviceHeat, Fakeable
|
|
50
|
+
|
|
51
|
+
from ramses_rf.const import ( # noqa: F401, isort: skip, pylint: disable=unused-import
|
|
61
52
|
F9,
|
|
62
53
|
FA,
|
|
63
54
|
FC,
|
|
64
55
|
FF,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
from ramses_rf.const import ( # noqa: F401, isort: skip, pylint: disable=unused-import
|
|
59
|
+
I_,
|
|
60
|
+
RP,
|
|
61
|
+
RQ,
|
|
62
|
+
W_,
|
|
65
63
|
Code,
|
|
66
64
|
)
|
|
67
65
|
|
|
66
|
+
from ramses_tx.const import (
|
|
67
|
+
SZ_BOILER_OUTPUT_TEMP,
|
|
68
|
+
SZ_BOILER_RETURN_TEMP,
|
|
69
|
+
SZ_BOILER_SETPOINT,
|
|
70
|
+
SZ_BURNER_FAILED_STARTS,
|
|
71
|
+
SZ_BURNER_HOURS,
|
|
72
|
+
SZ_BURNER_STARTS,
|
|
73
|
+
SZ_CH_ACTIVE,
|
|
74
|
+
SZ_CH_ENABLED,
|
|
75
|
+
SZ_CH_MAX_SETPOINT,
|
|
76
|
+
SZ_CH_PUMP_HOURS,
|
|
77
|
+
SZ_CH_PUMP_STARTS,
|
|
78
|
+
SZ_CH_SETPOINT,
|
|
79
|
+
SZ_CH_WATER_PRESSURE,
|
|
80
|
+
SZ_COOLING_ACTIVE,
|
|
81
|
+
SZ_COOLING_ENABLED,
|
|
82
|
+
SZ_DHW_ACTIVE,
|
|
83
|
+
SZ_DHW_BLOCKING,
|
|
84
|
+
SZ_DHW_BURNER_HOURS,
|
|
85
|
+
SZ_DHW_BURNER_STARTS,
|
|
86
|
+
SZ_DHW_ENABLED,
|
|
87
|
+
SZ_DHW_FLOW_RATE,
|
|
88
|
+
SZ_DHW_PUMP_HOURS,
|
|
89
|
+
SZ_DHW_PUMP_STARTS,
|
|
90
|
+
SZ_DHW_SETPOINT,
|
|
91
|
+
SZ_DHW_TEMP,
|
|
92
|
+
SZ_FAULT_PRESENT,
|
|
93
|
+
SZ_FLAME_ACTIVE,
|
|
94
|
+
SZ_FLAME_SIGNAL_LOW,
|
|
95
|
+
SZ_MAX_REL_MODULATION,
|
|
96
|
+
SZ_OEM_CODE,
|
|
97
|
+
SZ_OTC_ACTIVE,
|
|
98
|
+
SZ_OUTSIDE_TEMP,
|
|
99
|
+
SZ_REL_MODULATION_LEVEL,
|
|
100
|
+
SZ_SUMMER_MODE,
|
|
101
|
+
)
|
|
102
|
+
|
|
68
103
|
if TYPE_CHECKING:
|
|
69
|
-
from
|
|
70
|
-
from
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
SZ_FLAME_SIGNAL_LOW = "flame_signal_low"
|
|
83
|
-
|
|
84
|
-
SZ_BOILER_OUTPUT_TEMP = "boiler_output_temp"
|
|
85
|
-
SZ_BOILER_RETURN_TEMP = "boiler_return_temp"
|
|
86
|
-
SZ_BOILER_SETPOINT = "boiler_setpoint"
|
|
87
|
-
SZ_CH_MAX_SETPOINT = "ch_max_setpoint"
|
|
88
|
-
SZ_CH_SETPOINT = "ch_setpoint"
|
|
89
|
-
SZ_CH_WATER_PRESSURE = "ch_water_pressure"
|
|
90
|
-
SZ_DHW_FLOW_RATE = "dhw_flow_rate"
|
|
91
|
-
SZ_DHW_SETPOINT = "dhw_setpoint"
|
|
92
|
-
SZ_DHW_TEMP = "dhw_temp"
|
|
93
|
-
SZ_MAX_REL_MODULATION = "max_rel_modularion"
|
|
94
|
-
SZ_OEM_CODE = "oem_code"
|
|
95
|
-
SZ_OUTSIDE_TEMP = "outside_temp"
|
|
96
|
-
SZ_REL_MODULATION_LEVEL = "rel_modulation_level"
|
|
97
|
-
|
|
98
|
-
SZ_CH_ACTIVE = "ch_active"
|
|
99
|
-
SZ_CH_ENABLED = "ch_enabled"
|
|
100
|
-
SZ_COOLING_ACTIVE = "cooling_active"
|
|
101
|
-
SZ_COOLING_ENABLED = "cooling_enabled"
|
|
102
|
-
SZ_DHW_ACTIVE = "dhw_active"
|
|
103
|
-
SZ_DHW_BLOCKING = "dhw_blocking"
|
|
104
|
-
SZ_DHW_ENABLED = "dhw_enabled"
|
|
105
|
-
SZ_FAULT_PRESENT = "fault_present"
|
|
106
|
-
SZ_FLAME_ACTIVE = "flame_active"
|
|
107
|
-
SZ_SUMMER_MODE = "summer_mode"
|
|
108
|
-
SZ_OTC_ACTIVE = "otc_active"
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
DEV_MODE = __dev_mode__ # and False
|
|
104
|
+
from ramses_rf.system import Evohome, Zone
|
|
105
|
+
from ramses_tx import Address, Message, Packet
|
|
106
|
+
from ramses_tx.opentherm import OtDataId
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
QOS_LOW = {SZ_PRIORITY: Priority.LOW} # FIXME: deprecate QoS in kwargs
|
|
110
|
+
QOS_MID = {SZ_PRIORITY: Priority.HIGH} # FIXME: deprecate QoS in kwargs
|
|
111
|
+
QOS_MAX = {SZ_PRIORITY: Priority.HIGH, SZ_NUM_REPEATS: 3} # FIXME: deprecate QoS...
|
|
112
|
+
|
|
113
|
+
#
|
|
114
|
+
# NOTE: All debug flags should be False for deployment to end-users
|
|
115
|
+
_DBG_ENABLE_DEPRECATION: Final[bool] = False
|
|
116
|
+
_DBG_EXTRA_OTB_DISCOVERY: Final[bool] = False
|
|
112
117
|
|
|
113
118
|
_LOGGER = logging.getLogger(__name__)
|
|
114
|
-
if DEV_MODE:
|
|
115
|
-
_LOGGER.setLevel(logging.DEBUG)
|
|
116
119
|
|
|
117
120
|
|
|
118
|
-
class Actuator(
|
|
121
|
+
class Actuator(DeviceHeat): # 3EF0, 3EF1 (for 10:/13:)
|
|
119
122
|
# .I --- 13:109598 --:------ 13:109598 3EF0 003 00C8FF # event-driven, 00/C8
|
|
120
123
|
# RP --- 13:109598 18:002563 --:------ 0008 002 00C8 # 00/C8, as abobe
|
|
121
124
|
# RP --- 13:109598 18:002563 --:------ 3EF1 007 0000BF-00BFC8FF # 00/C8, as above
|
|
@@ -127,10 +130,10 @@ class Actuator(Fakeable, DeviceHeat): # 3EF0, 3EF1 (for 10:/13:)
|
|
|
127
130
|
# RP --- 10:138926 34:010253 --:------ 3EF0 006 002E11-0000FF # 10:s only RP
|
|
128
131
|
# .I --- 13:209679 --:------ 13:209679 3EF0 003 00C8FF # 13:s only I
|
|
129
132
|
|
|
130
|
-
ACTUATOR_CYCLE = "actuator_cycle"
|
|
131
|
-
ACTUATOR_ENABLED = "actuator_enabled" # boolean
|
|
132
|
-
ACTUATOR_STATE = "actuator_state"
|
|
133
|
-
MODULATION_LEVEL = "modulation_level" # percentage (0.0-1.0)
|
|
133
|
+
ACTUATOR_CYCLE: Final = "actuator_cycle"
|
|
134
|
+
ACTUATOR_ENABLED: Final = "actuator_enabled" # boolean
|
|
135
|
+
ACTUATOR_STATE: Final = "actuator_state"
|
|
136
|
+
MODULATION_LEVEL: Final = "modulation_level" # percentage (0.0-1.0)
|
|
134
137
|
|
|
135
138
|
def _handle_msg(self, msg: Message) -> None: # NOTE: active
|
|
136
139
|
super()._handle_msg(msg)
|
|
@@ -138,22 +141,22 @@ class Actuator(Fakeable, DeviceHeat): # 3EF0, 3EF1 (for 10:/13:)
|
|
|
138
141
|
if isinstance(self, OtbGateway):
|
|
139
142
|
return
|
|
140
143
|
|
|
141
|
-
if
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
144
|
+
if self._gwy.config.disable_discovery:
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
# TODO: why are we doing this here? Should simply use dscovery poller!
|
|
148
|
+
if msg.code == Code._3EF0 and msg.verb == I_ and not self.is_faked:
|
|
149
|
+
# lf._send_cmd(Command.get_relay_demand(self.id), qos=QOS_LOW)
|
|
150
|
+
self._send_cmd(
|
|
151
|
+
Command.from_attrs(RQ, self.id, Code._3EF1, "00"), **QOS_LOW
|
|
152
|
+
) # actuator cycle
|
|
150
153
|
|
|
151
154
|
@property
|
|
152
|
-
def actuator_cycle(self) ->
|
|
155
|
+
def actuator_cycle(self) -> dict | None: # 3EF1
|
|
153
156
|
return self._msg_value(Code._3EF1)
|
|
154
157
|
|
|
155
158
|
@property
|
|
156
|
-
def actuator_state(self) ->
|
|
159
|
+
def actuator_state(self) -> dict | None: # 3EF0
|
|
157
160
|
return self._msg_value(Code._3EF0)
|
|
158
161
|
|
|
159
162
|
@property
|
|
@@ -166,11 +169,10 @@ class Actuator(Fakeable, DeviceHeat): # 3EF0, 3EF1 (for 10:/13:)
|
|
|
166
169
|
|
|
167
170
|
|
|
168
171
|
class HeatDemand(DeviceHeat): # 3150
|
|
169
|
-
|
|
170
|
-
HEAT_DEMAND = SZ_HEAT_DEMAND # percentage valve open (0.0-1.0)
|
|
172
|
+
HEAT_DEMAND: Final = SZ_HEAT_DEMAND # percentage valve open (0.0-1.0)
|
|
171
173
|
|
|
172
174
|
@property
|
|
173
|
-
def heat_demand(self) ->
|
|
175
|
+
def heat_demand(self) -> float | None: # 3150
|
|
174
176
|
return self._msg_value(Code._3150, key=self.HEAT_DEMAND)
|
|
175
177
|
|
|
176
178
|
@property
|
|
@@ -182,11 +184,10 @@ class HeatDemand(DeviceHeat): # 3150
|
|
|
182
184
|
|
|
183
185
|
|
|
184
186
|
class Setpoint(DeviceHeat): # 2309
|
|
185
|
-
|
|
186
|
-
SETPOINT = SZ_SETPOINT # degrees Celsius
|
|
187
|
+
SETPOINT: Final = SZ_SETPOINT # degrees Celsius
|
|
187
188
|
|
|
188
189
|
@property
|
|
189
|
-
def setpoint(self) ->
|
|
190
|
+
def setpoint(self) -> float | None: # 2309
|
|
190
191
|
return self._msg_value(Code._2309, key=self.SETPOINT)
|
|
191
192
|
|
|
192
193
|
@property
|
|
@@ -197,35 +198,22 @@ class Setpoint(DeviceHeat): # 2309
|
|
|
197
198
|
}
|
|
198
199
|
|
|
199
200
|
|
|
200
|
-
class Weather(
|
|
201
|
-
|
|
202
|
-
TEMPERATURE = SZ_TEMPERATURE # degrees Celsius
|
|
203
|
-
|
|
204
|
-
def _bind(self):
|
|
205
|
-
#
|
|
206
|
-
#
|
|
207
|
-
#
|
|
208
|
-
|
|
209
|
-
def callback(msg):
|
|
210
|
-
pass
|
|
211
|
-
|
|
212
|
-
super()._bind()
|
|
213
|
-
self._bind_request(Code._0002, callback=callback)
|
|
201
|
+
class Weather(DeviceHeat): # 0002
|
|
202
|
+
TEMPERATURE: Final = SZ_TEMPERATURE # TODO: deprecate
|
|
214
203
|
|
|
215
204
|
@property
|
|
216
|
-
def temperature(self) ->
|
|
217
|
-
return self._msg_value(Code._0002, key=
|
|
205
|
+
def temperature(self) -> float | None: # 0002
|
|
206
|
+
return self._msg_value(Code._0002, key=SZ_TEMPERATURE)
|
|
218
207
|
|
|
219
|
-
# @check_faking_enabled
|
|
220
208
|
@temperature.setter
|
|
221
|
-
def temperature(self, value) -> None:
|
|
222
|
-
|
|
223
|
-
|
|
209
|
+
def temperature(self, value: float | None) -> None:
|
|
210
|
+
"""Fake the outdoor temperature of the sensor."""
|
|
211
|
+
|
|
212
|
+
if not self.is_faked:
|
|
213
|
+
raise exc.DeviceNotFaked(f"{self}: Faking is not enabled")
|
|
214
|
+
|
|
224
215
|
cmd = Command.put_outdoor_temp(self.id, value)
|
|
225
|
-
|
|
226
|
-
# self._gwy.hgi.id if self == self._gwy.hgi._faked_thm else self.id, value
|
|
227
|
-
# )
|
|
228
|
-
self._send_cmd(cmd)
|
|
216
|
+
self._gwy.send_cmd(cmd, num_repeats=2, priority=Priority.HIGH)
|
|
229
217
|
|
|
230
218
|
@property
|
|
231
219
|
def status(self) -> dict[str, Any]:
|
|
@@ -235,7 +223,11 @@ class Weather(Fakeable, DeviceHeat): # 0002
|
|
|
235
223
|
}
|
|
236
224
|
|
|
237
225
|
|
|
238
|
-
class RelayDemand(
|
|
226
|
+
class RelayDemand(DeviceHeat): # 0008
|
|
227
|
+
# .I --- 01:054173 --:------ 01:054173 1FC9 018 03-0008-04D39D FC-3B00-04D39D 03-1FC9-04D39D
|
|
228
|
+
# .W --- 13:123456 01:054173 --:------ 1FC9 006 00-3EF0-35E240
|
|
229
|
+
# .I --- 01:054173 13:123456 --:------ 1FC9 006 00-FFFF-04D39D
|
|
230
|
+
|
|
239
231
|
# Some either 00/C8, others 00-C8
|
|
240
232
|
# .I --- 01:145038 --:------ 01:145038 0008 002 0314 # ZON valve zone (ELE too?)
|
|
241
233
|
# .I --- 01:145038 --:------ 01:145038 0008 002 F914 # HTG valve
|
|
@@ -245,72 +237,16 @@ class RelayDemand(Fakeable, DeviceHeat): # 0008
|
|
|
245
237
|
# RP --- 13:109598 18:199952 --:------ 0008 002 0000
|
|
246
238
|
# RP --- 13:109598 18:199952 --:------ 0008 002 00C8
|
|
247
239
|
|
|
248
|
-
RELAY_DEMAND = SZ_RELAY_DEMAND # percentage (0.0-1.0)
|
|
240
|
+
RELAY_DEMAND: Final = SZ_RELAY_DEMAND # percentage (0.0-1.0)
|
|
249
241
|
|
|
250
242
|
def _setup_discovery_cmds(self) -> None:
|
|
251
243
|
super()._setup_discovery_cmds()
|
|
252
244
|
|
|
253
|
-
if not self.
|
|
245
|
+
if not self.is_faked: # discover_flag & Discover.STATUS and
|
|
254
246
|
self._add_discovery_cmd(Command.get_relay_demand(self.id), 60 * 15)
|
|
255
247
|
|
|
256
|
-
def _handle_msg(self, msg: Message) -> None: # NOTE: active
|
|
257
|
-
if msg.src.id == self.id:
|
|
258
|
-
super()._handle_msg(msg)
|
|
259
|
-
return
|
|
260
|
-
|
|
261
|
-
if (
|
|
262
|
-
self._gwy.config.disable_sending
|
|
263
|
-
or not self._faked
|
|
264
|
-
or self._child_id is None
|
|
265
|
-
or self._child_id
|
|
266
|
-
not in (
|
|
267
|
-
v for k, v in msg.payload.items() if k in (SZ_DOMAIN_ID, SZ_ZONE_IDX)
|
|
268
|
-
)
|
|
269
|
-
):
|
|
270
|
-
return
|
|
271
|
-
|
|
272
|
-
if msg.code == Code._3EF0 and msg.verb == I_: # NOT RP
|
|
273
|
-
# should't use for RP as RQ's might be polled quite often
|
|
274
|
-
cmd = Command.get_relay_demand(self.id)
|
|
275
|
-
self._send_cmd(cmd, qos={SZ_PRIORITY: Priority.LOW, SZ_RETRIES: 1})
|
|
276
|
-
|
|
277
|
-
# elif msg.code == Code._0009: # can only be I, from a controller
|
|
278
|
-
# elif msg.code == Code._3B00...:
|
|
279
|
-
|
|
280
|
-
if not self._faked or msg.verb != RQ: # duplicated, above
|
|
281
|
-
return
|
|
282
|
-
|
|
283
|
-
# TODO: handle relay_failsafe, reply to RQs
|
|
284
|
-
if msg.code == Code._0008 and msg.verb == RQ: # NOTE: WIP for FAKING
|
|
285
|
-
# 076 I --- 01:054173 --:------ 01:054173 0008 002 037C
|
|
286
|
-
mod_level = msg.payload[self.RELAY_DEMAND]
|
|
287
|
-
if mod_level is not None:
|
|
288
|
-
mod_level = 1.0 if mod_level > 0 else 0
|
|
289
|
-
|
|
290
|
-
cmd = Command.put_actuator_state(self.id, mod_level)
|
|
291
|
-
qos = {SZ_PRIORITY: Priority.HIGH, SZ_RETRIES: 3}
|
|
292
|
-
[self._send_cmd(cmd, **qos) for _ in range(1)] # type: ignore[func-returns-value]
|
|
293
|
-
|
|
294
|
-
elif msg.code == Code._3EF1 and msg.verb == RQ: # NOTE: WIP for FAKING
|
|
295
|
-
mod_level = 1.0
|
|
296
|
-
|
|
297
|
-
cmd = Command.put_actuator_cycle(self.id, msg.src.id, mod_level, 600, 600)
|
|
298
|
-
qos = {SZ_PRIORITY: Priority.HIGH, SZ_RETRIES: 3}
|
|
299
|
-
[self._send_cmd(cmd, **qos) for _ in range(1)] # type: ignore[func-returns-value]
|
|
300
|
-
|
|
301
|
-
def _bind(self):
|
|
302
|
-
# .I --- 01:054173 --:------ 01:054173 1FC9 018 03-0008-04D39D FC-3B00-04D39D 03-1FC9-04D39D
|
|
303
|
-
# .W --- 13:123456 01:054173 --:------ 1FC9 006 00-3EF0-35E240
|
|
304
|
-
# .I --- 01:054173 13:123456 --:------ 1FC9 006 00-FFFF-04D39D
|
|
305
|
-
|
|
306
|
-
def callback(msg):
|
|
307
|
-
pass
|
|
308
|
-
|
|
309
|
-
super()._bind()
|
|
310
|
-
self._bind_waiting(Code._3EF0, callback=callback)
|
|
311
|
-
|
|
312
248
|
@property
|
|
313
|
-
def relay_demand(self) ->
|
|
249
|
+
def relay_demand(self) -> float | None: # 0008
|
|
314
250
|
return self._msg_value(Code._0008, key=self.RELAY_DEMAND)
|
|
315
251
|
|
|
316
252
|
@property
|
|
@@ -321,32 +257,22 @@ class RelayDemand(Fakeable, DeviceHeat): # 0008
|
|
|
321
257
|
}
|
|
322
258
|
|
|
323
259
|
|
|
324
|
-
class DhwTemperature(
|
|
325
|
-
|
|
326
|
-
TEMPERATURE = SZ_TEMPERATURE # degrees Celsius
|
|
327
|
-
|
|
328
|
-
def _bind(self):
|
|
329
|
-
#
|
|
330
|
-
#
|
|
331
|
-
#
|
|
332
|
-
|
|
333
|
-
def callback(msg):
|
|
334
|
-
self.set_parent(msg.src, child_id=FA, is_sensor=True)
|
|
335
|
-
|
|
336
|
-
super()._bind()
|
|
337
|
-
self._bind_request(Code._1260, callback=callback)
|
|
260
|
+
class DhwTemperature(DeviceHeat): # 1260
|
|
261
|
+
TEMPERATURE: Final = SZ_TEMPERATURE # TODO: deprecate
|
|
338
262
|
|
|
339
263
|
@property
|
|
340
|
-
def temperature(self) ->
|
|
341
|
-
return self._msg_value(Code._1260, key=
|
|
264
|
+
def temperature(self) -> float | None: # 1260
|
|
265
|
+
return self._msg_value(Code._1260, key=SZ_TEMPERATURE)
|
|
342
266
|
|
|
343
|
-
# @check_faking_enabled
|
|
344
267
|
@temperature.setter
|
|
345
|
-
def temperature(self, value) -> None:
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
self.
|
|
349
|
-
|
|
268
|
+
def temperature(self, value: float | None) -> None:
|
|
269
|
+
"""Fake the DHW temperature of the sensor."""
|
|
270
|
+
|
|
271
|
+
if not self.is_faked:
|
|
272
|
+
raise exc.DeviceNotFaked(f"{self}: Faking is not enabled")
|
|
273
|
+
|
|
274
|
+
cmd = Command.put_dhw_temp(self.id, value)
|
|
275
|
+
self._gwy.send_cmd(cmd, num_repeats=2, priority=Priority.HIGH)
|
|
350
276
|
|
|
351
277
|
@property
|
|
352
278
|
def status(self) -> dict[str, Any]:
|
|
@@ -356,31 +282,23 @@ class DhwTemperature(Fakeable, DeviceHeat): # 1260
|
|
|
356
282
|
}
|
|
357
283
|
|
|
358
284
|
|
|
359
|
-
class Temperature(
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
# .I --- 34:145039 01:054173 --:------ 1FC9 006 00-30C9-8A368F
|
|
364
|
-
|
|
365
|
-
def callback(msg): # TODO: needs work
|
|
366
|
-
"""Use the accept pkt to determine the zone/domain id."""
|
|
367
|
-
child_id = msg.payload[SZ_BINDINGS][0][0]
|
|
368
|
-
self.set_parent(msg.src, child_id=child_id, is_sensor=True)
|
|
369
|
-
|
|
370
|
-
super()._bind()
|
|
371
|
-
self._bind_request(Code._30C9, callback=callback)
|
|
372
|
-
|
|
285
|
+
class Temperature(DeviceHeat): # 30C9
|
|
286
|
+
# .I --- 34:145039 --:------ 34:145039 1FC9 012 00-30C9-8A368F 00-1FC9-8A368F
|
|
287
|
+
# .W --- 01:054173 34:145039 --:------ 1FC9 006 03-2309-04D39D # real CTL
|
|
288
|
+
# .I --- 34:145039 01:054173 --:------ 1FC9 006 00-30C9-8A368F
|
|
373
289
|
@property
|
|
374
|
-
def temperature(self) ->
|
|
290
|
+
def temperature(self) -> float | None: # 30C9
|
|
375
291
|
return self._msg_value(Code._30C9, key=SZ_TEMPERATURE)
|
|
376
292
|
|
|
377
|
-
# @check_faking_enabled
|
|
378
293
|
@temperature.setter
|
|
379
|
-
def temperature(self, value) -> None:
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
self.
|
|
383
|
-
|
|
294
|
+
def temperature(self, value: float | None) -> None:
|
|
295
|
+
"""Fake the indoor temperature of the sensor."""
|
|
296
|
+
|
|
297
|
+
if not self.is_faked:
|
|
298
|
+
raise exc.DeviceNotFaked(f"{self}: Faking is not enabled")
|
|
299
|
+
|
|
300
|
+
cmd = Command.put_sensor_temp(self.id, value)
|
|
301
|
+
self._gwy.send_cmd(cmd, num_repeats=2, priority=Priority.HIGH)
|
|
384
302
|
|
|
385
303
|
@property
|
|
386
304
|
def status(self) -> dict[str, Any]:
|
|
@@ -393,15 +311,19 @@ class Temperature(Fakeable, DeviceHeat): # 30C9
|
|
|
393
311
|
class RfgGateway(DeviceHeat): # RFG (30:)
|
|
394
312
|
"""The RFG100 base class."""
|
|
395
313
|
|
|
396
|
-
_SLUG
|
|
314
|
+
_SLUG = DevType.RFG
|
|
315
|
+
_STATE_ATTR = None
|
|
397
316
|
|
|
398
317
|
|
|
399
318
|
class Controller(DeviceHeat): # CTL (01):
|
|
400
319
|
"""The Controller base class."""
|
|
401
320
|
|
|
402
|
-
|
|
321
|
+
HEAT_DEMAND: Final = SZ_HEAT_DEMAND
|
|
403
322
|
|
|
404
|
-
|
|
323
|
+
_SLUG = DevType.CTL
|
|
324
|
+
_STATE_ATTR = HEAT_DEMAND
|
|
325
|
+
|
|
326
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
405
327
|
super().__init__(*args, **kwargs)
|
|
406
328
|
|
|
407
329
|
# self.ctl = None
|
|
@@ -413,10 +335,12 @@ class Controller(DeviceHeat): # CTL (01):
|
|
|
413
335
|
|
|
414
336
|
self.tcs._handle_msg(msg)
|
|
415
337
|
|
|
416
|
-
def _make_tcs_controller(
|
|
338
|
+
def _make_tcs_controller(
|
|
339
|
+
self, *, msg: Message | None = None, **schema: Any
|
|
340
|
+
) -> None: # CH/DHW
|
|
417
341
|
"""Attach a TCS (create/update as required) after passing it any msg."""
|
|
418
342
|
|
|
419
|
-
def get_system(*, msg=None, **schema) ->
|
|
343
|
+
def get_system(*, msg: Message | None = None, **schema: Any) -> Evohome:
|
|
420
344
|
"""Return a TCS (temperature control system), create it if required.
|
|
421
345
|
|
|
422
346
|
Use the schema to create/update it, then pass it any msg to handle.
|
|
@@ -425,7 +349,7 @@ class Controller(DeviceHeat): # CTL (01):
|
|
|
425
349
|
If a TCS is created, attach it to this device (which should be a CTL).
|
|
426
350
|
"""
|
|
427
351
|
|
|
428
|
-
from
|
|
352
|
+
from ramses_rf.system import system_factory
|
|
429
353
|
|
|
430
354
|
schema = shrink(SCH_TCS(schema))
|
|
431
355
|
|
|
@@ -447,17 +371,21 @@ class Controller(DeviceHeat): # CTL (01):
|
|
|
447
371
|
class Programmer(Controller): # PRG (23):
|
|
448
372
|
"""The Controller base class."""
|
|
449
373
|
|
|
450
|
-
_SLUG
|
|
374
|
+
_SLUG = DevType.PRG
|
|
451
375
|
|
|
452
376
|
|
|
453
377
|
class UfhController(Parent, DeviceHeat): # UFC (02):
|
|
454
378
|
"""The UFC class, the HCE80 that controls the UFH zones."""
|
|
455
379
|
|
|
456
|
-
|
|
380
|
+
HEAT_DEMAND: Final = SZ_HEAT_DEMAND
|
|
457
381
|
|
|
458
|
-
|
|
382
|
+
_SLUG = DevType.UFC
|
|
383
|
+
_STATE_ATTR = HEAT_DEMAND
|
|
459
384
|
|
|
460
|
-
|
|
385
|
+
_child_id = FA
|
|
386
|
+
_iz_controller = True
|
|
387
|
+
|
|
388
|
+
childs: list[UfhCircuit] # TODO: check (code so complex, not sure if this is true)
|
|
461
389
|
|
|
462
390
|
# 12:27:24.398 067 I --- 02:000921 --:------ 01:191718 3150 002 0360
|
|
463
391
|
# 12:27:24.546 068 I --- 02:000921 --:------ 01:191718 3150 002 065A
|
|
@@ -465,44 +393,38 @@ class UfhController(Parent, DeviceHeat): # UFC (02):
|
|
|
465
393
|
# 12:27:24.824 059 I --- 01:191718 --:------ 01:191718 3150 002 FC5C
|
|
466
394
|
# 12:27:24.857 067 I --- 02:000921 --:------ 02:000921 3150 006 0060-015A-025C
|
|
467
395
|
|
|
468
|
-
def __init__(self, *args, **kwargs) -> None:
|
|
396
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
469
397
|
super().__init__(*args, **kwargs)
|
|
470
398
|
|
|
471
|
-
self._child_id = FA # NOTE: domain_id, HACK: UFC
|
|
472
|
-
|
|
473
399
|
self.circuit_by_id = {f"{i:02X}": {} for i in range(8)}
|
|
474
400
|
|
|
475
|
-
self._setpoints: Message
|
|
476
|
-
self._heat_demand: Message
|
|
477
|
-
self._heat_demands: Message
|
|
478
|
-
self._relay_demand: Message
|
|
479
|
-
self._relay_demand_fa: Message
|
|
480
|
-
|
|
481
|
-
self._iz_controller = True
|
|
401
|
+
self._setpoints: Message | None = None
|
|
402
|
+
self._heat_demand: Message | None = None
|
|
403
|
+
self._heat_demands: Message | None = None
|
|
404
|
+
self._relay_demand: Message | None = None
|
|
405
|
+
self._relay_demand_fa: Message | None = None
|
|
482
406
|
|
|
483
407
|
def _setup_discovery_cmds(self) -> None:
|
|
484
408
|
super()._setup_discovery_cmds()
|
|
485
409
|
|
|
486
410
|
# Only RPs are: 0001, 0005/000C, 10E0, 000A/2309 & 22D0
|
|
487
411
|
|
|
488
|
-
self.
|
|
489
|
-
|
|
490
|
-
|
|
412
|
+
cmd = Command.from_attrs(RQ, self.id, Code._0005, f"00{DEV_ROLE_MAP.UFH}")
|
|
413
|
+
self._add_discovery_cmd(cmd, 60 * 60 * 24)
|
|
414
|
+
|
|
491
415
|
# TODO: this needs work
|
|
492
416
|
# if discover_flag & Discover.PARAMS: # only 2309 has any potential?
|
|
493
417
|
for ufc_idx in self.circuit_by_id:
|
|
494
|
-
self.
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
self.
|
|
498
|
-
|
|
499
|
-
)
|
|
418
|
+
cmd = Command.get_zone_config(self.id, ufc_idx)
|
|
419
|
+
self._add_discovery_cmd(cmd, 60 * 60 * 6)
|
|
420
|
+
|
|
421
|
+
cmd = Command.get_zone_setpoint(self.id, ufc_idx)
|
|
422
|
+
self._add_discovery_cmd(cmd, 60 * 60 * 6)
|
|
500
423
|
|
|
501
|
-
for ufc_idx in range(8):
|
|
424
|
+
for ufc_idx in range(8):
|
|
502
425
|
payload = f"{ufc_idx:02X}{DEV_ROLE_MAP.UFH}"
|
|
503
|
-
self.
|
|
504
|
-
|
|
505
|
-
)
|
|
426
|
+
cmd = Command.from_attrs(RQ, self.id, Code._000C, payload)
|
|
427
|
+
self._add_discovery_cmd(cmd, 60 * 60 * 24)
|
|
506
428
|
|
|
507
429
|
def _handle_msg(self, msg: Message) -> None:
|
|
508
430
|
super()._handle_msg(msg)
|
|
@@ -521,8 +443,12 @@ class UfhController(Parent, DeviceHeat): # UFC (02):
|
|
|
521
443
|
ufh_idx = f"{idx:02X}"
|
|
522
444
|
if not flag:
|
|
523
445
|
self.circuit_by_id[ufh_idx] = {SZ_ZONE_IDX: None}
|
|
524
|
-
|
|
525
|
-
|
|
446
|
+
# FIXME: this causing tests to fail when read-only protocol
|
|
447
|
+
# elif SZ_ZONE_IDX not in self.circuit_by_id[ufh_idx]:
|
|
448
|
+
# cmd = Command.from_attrs(
|
|
449
|
+
# RQ, self.ctl.id, Code._000C, f"{ufh_idx}{DEV_ROLE_MAP.UFH}"
|
|
450
|
+
# )
|
|
451
|
+
# self._send_cmd(cmd)
|
|
526
452
|
|
|
527
453
|
elif msg.code == Code._0008: # relay_demand, TODO: use msg DB?
|
|
528
454
|
if msg.payload.get(SZ_DOMAIN_ID) == FC:
|
|
@@ -545,7 +471,7 @@ class UfhController(Parent, DeviceHeat): # UFC (02):
|
|
|
545
471
|
# child_id=msg.payload[SZ_ZONE_IDX],
|
|
546
472
|
)
|
|
547
473
|
|
|
548
|
-
elif msg.code == Code._22C9: #
|
|
474
|
+
elif msg.code == Code._22C9: # setpoint_bounds
|
|
549
475
|
# .I --- 02:017205 --:------ 02:017205 22C9 024 00076C0A280101076C0A28010...
|
|
550
476
|
# .I --- 02:017205 --:------ 02:017205 22C9 006 04076C0A2801
|
|
551
477
|
self._setpoints = msg
|
|
@@ -557,6 +483,7 @@ class UfhController(Parent, DeviceHeat): # UFC (02):
|
|
|
557
483
|
self._heat_demand = msg
|
|
558
484
|
elif (
|
|
559
485
|
(zone_idx := msg.payload.get(SZ_ZONE_IDX))
|
|
486
|
+
and isinstance(msg.dst, Device)
|
|
560
487
|
and (tcs := msg.dst.tcs)
|
|
561
488
|
and (zone := tcs.zone_by_idx.get(zone_idx))
|
|
562
489
|
):
|
|
@@ -567,7 +494,10 @@ class UfhController(Parent, DeviceHeat): # UFC (02):
|
|
|
567
494
|
|
|
568
495
|
# "0008|FA/FC", "22C9|array", "22D0|none", "3150|ZZ/array(/FC?)"
|
|
569
496
|
|
|
570
|
-
|
|
497
|
+
# TODO: should be a private method
|
|
498
|
+
def get_circuit(
|
|
499
|
+
self, cct_idx: str, *, msg: Message | None = None, **schema: Any
|
|
500
|
+
) -> Any:
|
|
571
501
|
"""Return a UFH circuit, create it if required.
|
|
572
502
|
|
|
573
503
|
First, use the schema to create/update it, then pass it any msg to handle.
|
|
@@ -578,7 +508,7 @@ class UfhController(Parent, DeviceHeat): # UFC (02):
|
|
|
578
508
|
|
|
579
509
|
schema = {} # shrink(SCH_CCT(schema))
|
|
580
510
|
|
|
581
|
-
cct = self.child_by_id.get(cct_idx)
|
|
511
|
+
cct: UfhCircuit = self.child_by_id.get(cct_idx)
|
|
582
512
|
if not cct:
|
|
583
513
|
cct = UfhCircuit(self, cct_idx)
|
|
584
514
|
self.child_by_id[cct_idx] = cct
|
|
@@ -596,26 +526,26 @@ class UfhController(Parent, DeviceHeat): # UFC (02):
|
|
|
596
526
|
# return self.circuit_by_id
|
|
597
527
|
|
|
598
528
|
@property
|
|
599
|
-
def heat_demand(self) ->
|
|
529
|
+
def heat_demand(self) -> float | None: # 3150|FC (there is also 3150|FA)
|
|
600
530
|
return self._msg_value_msg(self._heat_demand, key=self.HEAT_DEMAND)
|
|
601
531
|
|
|
602
532
|
@property
|
|
603
|
-
def heat_demands(self) ->
|
|
533
|
+
def heat_demands(self) -> dict | None: # 3150|ufh_idx array
|
|
604
534
|
# return self._heat_demands.payload if self._heat_demands else None
|
|
605
535
|
return self._msg_value_msg(self._heat_demands)
|
|
606
536
|
|
|
607
537
|
@property
|
|
608
|
-
def relay_demand(self) ->
|
|
538
|
+
def relay_demand(self) -> dict | None: # 0008|FC
|
|
609
539
|
return self._msg_value_msg(self._relay_demand, key=SZ_RELAY_DEMAND)
|
|
610
540
|
|
|
611
541
|
@property
|
|
612
|
-
def relay_demand_fa(self) ->
|
|
542
|
+
def relay_demand_fa(self) -> dict | None: # 0008|FA
|
|
613
543
|
return self._msg_value_msg(self._relay_demand_fa, key=SZ_RELAY_DEMAND)
|
|
614
544
|
|
|
615
545
|
@property
|
|
616
|
-
def setpoints(self) ->
|
|
546
|
+
def setpoints(self) -> dict | None: # 22C9|ufh_idx array
|
|
617
547
|
if self._setpoints is None:
|
|
618
|
-
return
|
|
548
|
+
return None
|
|
619
549
|
|
|
620
550
|
return {
|
|
621
551
|
c[SZ_UFH_IDX]: {
|
|
@@ -648,17 +578,15 @@ class UfhController(Parent, DeviceHeat): # UFC (02):
|
|
|
648
578
|
}
|
|
649
579
|
|
|
650
580
|
|
|
651
|
-
class DhwSensor(DhwTemperature, BatteryState): # DHW (07): 10A0, 1260
|
|
581
|
+
class DhwSensor(DhwTemperature, BatteryState, Fakeable): # DHW (07): 10A0, 1260
|
|
652
582
|
"""The DHW class, such as a CS92."""
|
|
653
583
|
|
|
654
|
-
|
|
584
|
+
DHW_PARAMS: Final = "dhw_params"
|
|
655
585
|
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
_STATE_ATTR = SZ_TEMPERATURE
|
|
586
|
+
_SLUG: str = DevType.DHW
|
|
587
|
+
_STATE_ATTR = DhwTemperature.TEMPERATURE
|
|
660
588
|
|
|
661
|
-
def __init__(self, *args, **kwargs) -> None:
|
|
589
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
662
590
|
super().__init__(*args, **kwargs)
|
|
663
591
|
|
|
664
592
|
self._child_id = FA # NOTE: domain_id
|
|
@@ -666,13 +594,20 @@ class DhwSensor(DhwTemperature, BatteryState): # DHW (07): 10A0, 1260
|
|
|
666
594
|
def _handle_msg(self, msg: Message) -> None: # NOTE: active
|
|
667
595
|
super()._handle_msg(msg)
|
|
668
596
|
|
|
669
|
-
|
|
670
|
-
|
|
597
|
+
if self._gwy.config.disable_discovery:
|
|
598
|
+
return
|
|
599
|
+
|
|
600
|
+
# TODO: why are we doing this here? Should simply use dscovery poller!
|
|
601
|
+
# The following is required, as CTLs don't send spontaneously
|
|
602
|
+
if msg.code == Code._1260 and self.ctl:
|
|
671
603
|
# update the controller DHW temp
|
|
672
604
|
self._send_cmd(Command.get_dhw_temp(self.ctl.id))
|
|
673
605
|
|
|
606
|
+
async def initiate_binding_process(self) -> Packet:
|
|
607
|
+
return await super()._initiate_binding_process(Code._1260)
|
|
608
|
+
|
|
674
609
|
@property
|
|
675
|
-
def dhw_params(self) ->
|
|
610
|
+
def dhw_params(self) -> PayDictT._10A0 | None:
|
|
676
611
|
return self._msg_value(Code._10A0)
|
|
677
612
|
|
|
678
613
|
@property
|
|
@@ -683,95 +618,111 @@ class DhwSensor(DhwTemperature, BatteryState): # DHW (07): 10A0, 1260
|
|
|
683
618
|
}
|
|
684
619
|
|
|
685
620
|
|
|
686
|
-
class OutSensor(Weather): # OUT: 17
|
|
621
|
+
class OutSensor(Weather, Fakeable): # OUT: 17
|
|
687
622
|
"""The OUT class (external sensor), such as a HB85/HB95."""
|
|
688
623
|
|
|
689
|
-
_SLUG: str = DEV_TYPE.OUT
|
|
690
|
-
|
|
691
624
|
# LUMINOSITY = "luminosity" # lux
|
|
692
625
|
# WINDSPEED = "windspeed" # km/h
|
|
693
626
|
|
|
627
|
+
_SLUG = DevType.OUT
|
|
694
628
|
_STATE_ATTR = SZ_TEMPERATURE
|
|
695
629
|
|
|
630
|
+
# async def initiate_binding_process(self) -> Packet:
|
|
631
|
+
# return await super()._initiate_binding_process(...)
|
|
696
632
|
|
|
633
|
+
|
|
634
|
+
def _to_msg_id(data_id: OtDataId) -> MsgId:
|
|
635
|
+
return f"{data_id:02X}"
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
# NOTE: config.use_native_ot should enforces sends, but not reads from _msgz DB
|
|
697
639
|
class OtbGateway(Actuator, HeatDemand): # OTB (10): 3220 (22D9, others)
|
|
698
640
|
"""The OTB class, specifically an OpenTherm Bridge (R8810A Bridge)."""
|
|
699
641
|
|
|
700
642
|
# see: https://www.opentherm.eu/request-details/?post_ids=2944
|
|
701
643
|
# see: https://www.automatedhome.co.uk/vbulletin/showthread.php?6400-(New)-cool-mode-in-Evohome
|
|
702
644
|
|
|
703
|
-
_SLUG
|
|
704
|
-
|
|
645
|
+
_SLUG = DevType.OTB
|
|
705
646
|
_STATE_ATTR = SZ_REL_MODULATION_LEVEL
|
|
706
647
|
|
|
707
|
-
OT_TO_RAMSES = {
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
648
|
+
OT_TO_RAMSES: dict[MsgId, Code] = { # TODO: move to opentherm.py
|
|
649
|
+
MsgId._00: Code._3EF0, # master/slave status (actuator_state)
|
|
650
|
+
MsgId._01: Code._22D9, # boiler_setpoint
|
|
651
|
+
MsgId._0E: Code._3EF0, # max_rel_modulation_level (is a PARAM?)
|
|
652
|
+
MsgId._11: Code._3EF0, # rel_modulation_level (actuator_state, also Code._3EF1)
|
|
653
|
+
MsgId._12: Code._1300, # ch_water_pressure
|
|
654
|
+
MsgId._13: Code._12F0, # dhw_flow_rate
|
|
655
|
+
MsgId._19: Code._3200, # boiler_output_temp
|
|
656
|
+
MsgId._1A: Code._1260, # dhw_temp
|
|
657
|
+
MsgId._1B: Code._1290, # outside_temp
|
|
658
|
+
MsgId._1C: Code._3210, # boiler_return_temp
|
|
659
|
+
MsgId._38: Code._10A0, # dhw_setpoint (is a PARAM)
|
|
660
|
+
MsgId._39: Code._1081, # ch_max_setpoint (is a PARAM)
|
|
720
661
|
}
|
|
721
|
-
RAMSES_TO_OT
|
|
662
|
+
RAMSES_TO_OT: dict[Code, MsgId] = {
|
|
663
|
+
v: k for k, v in OT_TO_RAMSES.items() if v != Code._3EF0
|
|
664
|
+
} # also 10A0?
|
|
722
665
|
|
|
723
|
-
def __init__(self, *args, **kwargs) -> None:
|
|
666
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
724
667
|
super().__init__(*args, **kwargs)
|
|
725
668
|
|
|
726
669
|
self._child_id = FC # NOTE: domain_id
|
|
727
670
|
|
|
728
|
-
self._msgz[
|
|
671
|
+
self._msgz[Code._3220] = {RP: {}} # _msgz[Code._3220][RP][msg_id]
|
|
729
672
|
|
|
730
673
|
# lf._use_ot = self._gwy.config.use_native_ot
|
|
731
|
-
self._msgs_ot: dict[
|
|
674
|
+
self._msgs_ot: dict[MsgId, Message] = {}
|
|
732
675
|
# lf._msgs_ot_ctl_polled = {}
|
|
733
676
|
|
|
734
677
|
def _setup_discovery_cmds(self) -> None:
|
|
735
|
-
def which_cmd(use_native_ot: str, msg_id:
|
|
678
|
+
def which_cmd(use_native_ot: str, msg_id: MsgId) -> Command | None:
|
|
736
679
|
"""Create a OT cmd, or its RAMSES equivalent, depending."""
|
|
737
680
|
# we know RQ|3220 is an option, question is: use that, or RAMSES or nothing?
|
|
738
681
|
if use_native_ot in ("always", "prefer"):
|
|
739
682
|
return Command.get_opentherm_data(self.id, msg_id)
|
|
740
683
|
if msg_id in self.OT_TO_RAMSES: # is: in ("avoid", "never")
|
|
741
|
-
return
|
|
684
|
+
return Command.from_attrs(RQ, self.id, self.OT_TO_RAMSES[msg_id], "00")
|
|
742
685
|
if use_native_ot == "avoid":
|
|
743
686
|
return Command.get_opentherm_data(self.id, msg_id)
|
|
744
687
|
return None # use_native_ot == "never"
|
|
745
688
|
|
|
746
689
|
super()._setup_discovery_cmds()
|
|
747
690
|
|
|
748
|
-
# always send RQ|3EF0
|
|
749
|
-
self.
|
|
750
|
-
|
|
691
|
+
# always send at least one of RQ|3EF0 or RQ|3220|00 (status)
|
|
692
|
+
if self._gwy.config.use_native_ot != "never":
|
|
693
|
+
self._add_discovery_cmd(Command.get_opentherm_data(self.id, MsgId._00), 60)
|
|
751
694
|
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
695
|
+
if self._gwy.config.use_native_ot != "always":
|
|
696
|
+
self._add_discovery_cmd(
|
|
697
|
+
Command.from_attrs(RQ, self.id, Code._3EF0, "00"), 60
|
|
698
|
+
)
|
|
699
|
+
self._add_discovery_cmd( # NOTE: this code is a WIP
|
|
700
|
+
Command.from_attrs(RQ, self.id, Code._2401, "00"), 60
|
|
701
|
+
)
|
|
755
702
|
|
|
756
|
-
for
|
|
757
|
-
if cmd := which_cmd(self._gwy.config.use_native_ot,
|
|
703
|
+
for data_id in SCHEMA_DATA_IDS: # From OT v2.2: version numbers
|
|
704
|
+
if cmd := which_cmd(self._gwy.config.use_native_ot, _to_msg_id(data_id)):
|
|
705
|
+
self._add_discovery_cmd(cmd, 6 * 3600, delay=180)
|
|
706
|
+
|
|
707
|
+
for data_id in PARAMS_DATA_IDS: # params or L/T state
|
|
708
|
+
if cmd := which_cmd(self._gwy.config.use_native_ot, _to_msg_id(data_id)):
|
|
758
709
|
self._add_discovery_cmd(cmd, 3600, delay=90)
|
|
759
710
|
|
|
760
|
-
for
|
|
761
|
-
if
|
|
711
|
+
for data_id in STATUS_DATA_IDS: # except "00", see above
|
|
712
|
+
if data_id == 0x00:
|
|
762
713
|
continue
|
|
763
|
-
if cmd := which_cmd(self._gwy.config.use_native_ot,
|
|
714
|
+
if cmd := which_cmd(self._gwy.config.use_native_ot, _to_msg_id(data_id)):
|
|
764
715
|
self._add_discovery_cmd(cmd, 300, delay=15)
|
|
765
716
|
|
|
766
|
-
if
|
|
717
|
+
if _DBG_EXTRA_OTB_DISCOVERY: # TODO: these are WIP, but do vary in payload
|
|
767
718
|
for code in (
|
|
768
719
|
Code._2401, # WIP - modulation_level + flags?
|
|
769
720
|
Code._3221, # R8810A/20A
|
|
770
721
|
Code._3223, # R8810A/20A
|
|
771
722
|
):
|
|
772
|
-
self._add_discovery_cmd(
|
|
723
|
+
self._add_discovery_cmd(Command.from_attrs(RQ, self.id, code, "00"), 60)
|
|
773
724
|
|
|
774
|
-
if
|
|
725
|
+
if _DBG_EXTRA_OTB_DISCOVERY: # TODO: these are WIP, appear FIXED in payload
|
|
775
726
|
for code in (
|
|
776
727
|
Code._0150, # payload always "000000", R8820A only?
|
|
777
728
|
Code._1098, # payload always "00C8", R8820A only?
|
|
@@ -781,89 +732,92 @@ class OtbGateway(Actuator, HeatDemand): # OTB (10): 3220 (22D9, others)
|
|
|
781
732
|
Code._2410, # payload always "000000000000000000000000010000000100000C"
|
|
782
733
|
Code._2420, # payload always "0000001000000...
|
|
783
734
|
): # TODO: to test against BDR91T
|
|
784
|
-
|
|
785
|
-
|
|
735
|
+
self._add_discovery_cmd(
|
|
736
|
+
Command.from_attrs(RQ, self.id, code, "00"), 300
|
|
737
|
+
)
|
|
786
738
|
|
|
787
739
|
def _handle_msg(self, msg: Message) -> None:
|
|
788
740
|
super()._handle_msg(msg)
|
|
789
741
|
|
|
790
|
-
|
|
742
|
+
if msg.verb not in (I_, RP):
|
|
743
|
+
return
|
|
744
|
+
|
|
745
|
+
if msg.code == Code._3220:
|
|
746
|
+
self._handle_3220(msg)
|
|
747
|
+
elif msg.code in self.RAMSES_TO_OT:
|
|
748
|
+
self._handle_code(msg)
|
|
791
749
|
|
|
792
750
|
def _handle_3220(self, msg: Message) -> None:
|
|
793
|
-
|
|
751
|
+
"""Handle 3220-based messages."""
|
|
752
|
+
|
|
753
|
+
# NOTE: Reserved msgs have null data, but that msg_id may later be OK!
|
|
754
|
+
if msg.payload[SZ_MSG_TYPE] == OtMsgType.RESERVED:
|
|
755
|
+
return
|
|
756
|
+
|
|
757
|
+
# NOTE: Some msgs have invalid data, but that msg_id may later be OK!
|
|
758
|
+
if msg.payload.get(SZ_VALUE) is None:
|
|
794
759
|
return
|
|
795
760
|
|
|
796
|
-
msg_id
|
|
761
|
+
# msg_id is int in msg payload/opentherm.py, but MsgId (str) is in this module
|
|
762
|
+
msg_id = _to_msg_id(msg.payload[SZ_MSG_ID])
|
|
797
763
|
self._msgs_ot[msg_id] = msg
|
|
798
764
|
|
|
799
|
-
if
|
|
800
|
-
|
|
801
|
-
if msg_id != "73":
|
|
802
|
-
self._send_cmd(Command.get_opentherm_data(self.id, "73")) # oem code
|
|
765
|
+
if not _DBG_ENABLE_DEPRECATION: # FIXME: data gaps
|
|
766
|
+
return
|
|
803
767
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
768
|
+
reset = msg.payload[SZ_MSG_TYPE] not in (
|
|
769
|
+
OtMsgType.DATA_INVALID,
|
|
770
|
+
OtMsgType.UNKNOWN_DATAID,
|
|
771
|
+
OtMsgType.RESERVED, # but some are ?always reserved
|
|
772
|
+
)
|
|
773
|
+
self._deprecate_code_ctx(msg._pkt, ctx=msg_id, reset=reset)
|
|
809
774
|
|
|
810
|
-
|
|
811
|
-
|
|
775
|
+
def _handle_code(self, msg: Message) -> None:
|
|
776
|
+
"""Handle non-3220-based messages."""
|
|
812
777
|
|
|
813
|
-
|
|
814
|
-
#
|
|
815
|
-
#
|
|
816
|
-
#
|
|
817
|
-
#
|
|
818
|
-
|
|
819
|
-
OtMsgType.DATA_INVALID,
|
|
820
|
-
OtMsgType.UNKNOWN_DATAID,
|
|
821
|
-
# OtMsgType.RESERVED, # some always reserved, others sometimes so
|
|
822
|
-
)
|
|
823
|
-
self.deprecate_cmd(msg._pkt, ctx=msg_id, reset=reset)
|
|
778
|
+
if msg.code == Code._3EF0 and msg.verb == I_:
|
|
779
|
+
# NOTE: this is development/discovery code # chasing flags
|
|
780
|
+
# self._send_cmd(
|
|
781
|
+
# Command.get_opentherm_data(self.id, MsgId._00), **QOS_MID
|
|
782
|
+
# ) # FIXME: deprecate QoS in kwargs
|
|
783
|
+
return
|
|
824
784
|
|
|
825
|
-
|
|
826
|
-
if msg.code == Code._3EF0 and msg.verb == I_: # chasing flags
|
|
827
|
-
self._send_cmd(
|
|
828
|
-
Command.get_opentherm_data(self.id, "00"),
|
|
829
|
-
qos={SZ_PRIORITY: Priority.HIGH, SZ_RETRIES: 1},
|
|
830
|
-
)
|
|
785
|
+
if msg.code in (Code._10A0, Code._3EF1):
|
|
831
786
|
return
|
|
832
787
|
|
|
833
|
-
if
|
|
788
|
+
if not _DBG_ENABLE_DEPRECATION: # FIXME: data gaps
|
|
834
789
|
return
|
|
835
790
|
|
|
791
|
+
# TODO: can be temporarily 7FFF?
|
|
836
792
|
if msg._pkt.payload[2:] == "7FFF" or (
|
|
837
793
|
msg.code == Code._1300 and msg._pkt.payload[2:] == "09F6"
|
|
838
|
-
):
|
|
839
|
-
self.
|
|
794
|
+
): # latter is CH water pressure
|
|
795
|
+
self._deprecate_code_ctx(msg._pkt)
|
|
840
796
|
else:
|
|
841
|
-
self.
|
|
797
|
+
self._deprecate_code_ctx(msg._pkt, reset=True)
|
|
842
798
|
|
|
843
|
-
def _ot_msg_flag(self, msg_id, flag_idx) ->
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
return None
|
|
799
|
+
def _ot_msg_flag(self, msg_id: MsgId, flag_idx: int) -> bool | None:
|
|
800
|
+
flags: list = self._ot_msg_value(msg_id)
|
|
801
|
+
return bool(flags[flag_idx]) if flags else None
|
|
847
802
|
|
|
848
803
|
@staticmethod
|
|
849
|
-
def _ot_msg_name(msg) -> str:
|
|
804
|
+
def _ot_msg_name(msg: Message) -> str: # TODO: remove
|
|
850
805
|
return (
|
|
851
|
-
msg.payload[
|
|
852
|
-
if isinstance(msg.payload[
|
|
853
|
-
else f"{msg.payload[
|
|
806
|
+
msg.payload[SZ_MSG_NAME]
|
|
807
|
+
if isinstance(msg.payload[SZ_MSG_NAME], str)
|
|
808
|
+
else f"{msg.payload[SZ_MSG_ID]:02X}"
|
|
854
809
|
)
|
|
855
810
|
|
|
856
|
-
def _ot_msg_value(self, msg_id) ->
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
return self._msgs_ot[msg_id].payload.get(VALUE) # TODO: value_hb/_lb
|
|
811
|
+
def _ot_msg_value(self, msg_id: MsgId) -> int | float | list | None:
|
|
812
|
+
# data_id = int(msg_id, 16)
|
|
813
|
+
if (msg := self._msgs_ot.get(msg_id)) and not msg._expired:
|
|
814
|
+
# TODO: value_hb/_lb
|
|
815
|
+
return msg.payload.get(SZ_VALUE) # type: ignore[no-any-return]
|
|
816
|
+
return None
|
|
863
817
|
|
|
864
818
|
def _result_by_callback(
|
|
865
|
-
self, cbk_ot:
|
|
866
|
-
) ->
|
|
819
|
+
self, cbk_ot: Callable | None, cbk_ramses: Callable | None
|
|
820
|
+
) -> Any | None:
|
|
867
821
|
"""Return a value using OpenTherm or RAMSES as per `config.use_native_ot`."""
|
|
868
822
|
|
|
869
823
|
if self._gwy.config.use_native_ot == "always":
|
|
@@ -877,279 +831,297 @@ class OtbGateway(Actuator, HeatDemand): # OTB (10): 3220 (22D9, others)
|
|
|
877
831
|
return cbk_ot() if cbk_ot else None
|
|
878
832
|
return result_ramses # incl. use_native_ot == "never"
|
|
879
833
|
|
|
880
|
-
def _result_by_lookup(
|
|
834
|
+
def _result_by_lookup(
|
|
835
|
+
self,
|
|
836
|
+
code: Code,
|
|
837
|
+
/,
|
|
838
|
+
*,
|
|
839
|
+
key: str,
|
|
840
|
+
) -> Any | None:
|
|
881
841
|
"""Return a value using OpenTherm or RAMSES as per `config.use_native_ot`."""
|
|
882
842
|
# assert code in self.RAMSES_TO_OT and kwargs.get("key"):
|
|
883
843
|
|
|
884
844
|
if self._gwy.config.use_native_ot == "always":
|
|
885
845
|
return self._ot_msg_value(self.RAMSES_TO_OT[code])
|
|
846
|
+
|
|
886
847
|
if self._gwy.config.use_native_ot == "prefer":
|
|
887
848
|
if (result_ot := self._ot_msg_value(self.RAMSES_TO_OT[code])) is not None:
|
|
888
849
|
return result_ot
|
|
889
850
|
|
|
890
|
-
result_ramses = self._msg_value(code,
|
|
851
|
+
result_ramses = self._msg_value(code, key=key)
|
|
891
852
|
if self._gwy.config.use_native_ot == "avoid" and result_ramses is None:
|
|
892
853
|
return self._ot_msg_value(self.RAMSES_TO_OT[code])
|
|
854
|
+
|
|
893
855
|
return result_ramses # incl. use_native_ot == "never"
|
|
894
856
|
|
|
895
857
|
def _result_by_value(
|
|
896
|
-
self, result_ot:
|
|
897
|
-
) ->
|
|
858
|
+
self, result_ot: Any | None, result_ramses: Any | None
|
|
859
|
+
) -> Any | None:
|
|
898
860
|
"""Return a value using OpenTherm or RAMSES as per `config.use_native_ot`."""
|
|
861
|
+
#
|
|
899
862
|
|
|
900
863
|
if self._gwy.config.use_native_ot == "always":
|
|
901
864
|
return result_ot
|
|
865
|
+
|
|
902
866
|
if self._gwy.config.use_native_ot == "prefer":
|
|
903
867
|
if result_ot is not None:
|
|
904
868
|
return result_ot
|
|
905
869
|
|
|
870
|
+
#
|
|
906
871
|
elif self._gwy.config.use_native_ot == "avoid" and result_ramses is None:
|
|
907
872
|
return result_ot
|
|
873
|
+
|
|
908
874
|
return result_ramses # incl. use_native_ot == "never"
|
|
909
875
|
|
|
910
876
|
@property # TODO
|
|
911
|
-
def bit_2_4(self) ->
|
|
877
|
+
def bit_2_4(self) -> bool | None: # 2401 - WIP
|
|
912
878
|
return self._msg_flag(Code._2401, "_flags_2", 4)
|
|
913
879
|
|
|
914
880
|
@property # TODO
|
|
915
|
-
def bit_2_5(self) ->
|
|
881
|
+
def bit_2_5(self) -> bool | None: # 2401 - WIP
|
|
916
882
|
return self._msg_flag(Code._2401, "_flags_2", 5)
|
|
917
883
|
|
|
918
884
|
@property # TODO
|
|
919
|
-
def bit_2_6(self) ->
|
|
885
|
+
def bit_2_6(self) -> bool | None: # 2401 - WIP
|
|
920
886
|
return self._msg_flag(Code._2401, "_flags_2", 6)
|
|
921
887
|
|
|
922
888
|
@property # TODO
|
|
923
|
-
def bit_2_7(self) ->
|
|
889
|
+
def bit_2_7(self) -> bool | None: # 2401 - WIP
|
|
924
890
|
return self._msg_flag(Code._2401, "_flags_2", 7)
|
|
925
891
|
|
|
926
892
|
@property # TODO
|
|
927
|
-
def bit_3_7(self) ->
|
|
893
|
+
def bit_3_7(self) -> bool | None: # 3EF0 (byte 3, only OTB)
|
|
928
894
|
return self._msg_flag(Code._3EF0, "_flags_3", 7)
|
|
929
895
|
|
|
930
896
|
@property # TODO
|
|
931
|
-
def bit_6_6(self) ->
|
|
932
|
-
return self._msg_flag(Code._3EF0, "
|
|
897
|
+
def bit_6_6(self) -> bool | None: # 3EF0 ?dhw_enabled (byte 3, only R8820A?)
|
|
898
|
+
return self._msg_flag(Code._3EF0, "_flags_6", 6)
|
|
933
899
|
|
|
934
900
|
@property # TODO
|
|
935
|
-
def percent(self) ->
|
|
936
|
-
return self._msg_value(Code._2401, key=
|
|
901
|
+
def percent(self) -> float | None: # 2401 - WIP (~3150|FC)
|
|
902
|
+
return self._msg_value(Code._2401, key=SZ_HEAT_DEMAND)
|
|
937
903
|
|
|
938
904
|
@property # TODO
|
|
939
|
-
def value(self) ->
|
|
905
|
+
def value(self) -> int | None: # 2401 - WIP
|
|
940
906
|
return self._msg_value(Code._2401, key="_value_2")
|
|
941
907
|
|
|
942
908
|
@property
|
|
943
|
-
def boiler_output_temp(self) ->
|
|
909
|
+
def boiler_output_temp(self) -> float | None: # 3220|19, or 3200
|
|
910
|
+
# _LOGGER.warning(
|
|
911
|
+
# "code=%s, 3220=%s, both=%s",
|
|
912
|
+
# self._msg_value(Code._3200, key=SZ_TEMPERATURE),
|
|
913
|
+
# self._ot_msg_value(str(self.RAMSES_TO_OT[Code._3200])),
|
|
914
|
+
# self._result_by_lookup(Code._3200, key=SZ_TEMPERATURE),
|
|
915
|
+
# )
|
|
916
|
+
|
|
944
917
|
return self._result_by_lookup(Code._3200, key=SZ_TEMPERATURE)
|
|
945
918
|
|
|
946
919
|
@property
|
|
947
|
-
def boiler_return_temp(self) ->
|
|
920
|
+
def boiler_return_temp(self) -> float | None: # 3220|1C, or 3210
|
|
948
921
|
return self._result_by_lookup(Code._3210, key=SZ_TEMPERATURE)
|
|
949
922
|
|
|
950
923
|
@property
|
|
951
|
-
def boiler_setpoint(self) ->
|
|
924
|
+
def boiler_setpoint(self) -> float | None: # 3220|01, or 22D9
|
|
952
925
|
return self._result_by_lookup(Code._22D9, key=SZ_SETPOINT)
|
|
953
926
|
|
|
954
927
|
@property
|
|
955
|
-
def ch_max_setpoint(self) ->
|
|
928
|
+
def ch_max_setpoint(self) -> float | None: # 3220|39, or 1081
|
|
956
929
|
return self._result_by_lookup(Code._1081, key=SZ_SETPOINT)
|
|
957
930
|
|
|
958
|
-
@property # TODO
|
|
959
|
-
def ch_setpoint(self) ->
|
|
931
|
+
@property # TODO: no OT equivalent
|
|
932
|
+
def ch_setpoint(self) -> float | None: # 3EF0 (byte 7, only R8820A?)
|
|
960
933
|
return self._result_by_value(
|
|
961
934
|
None, self._msg_value(Code._3EF0, key=SZ_CH_SETPOINT)
|
|
962
935
|
)
|
|
963
936
|
|
|
964
937
|
@property
|
|
965
|
-
def ch_water_pressure(self) ->
|
|
966
|
-
|
|
967
|
-
return None if result == 25.5 else result # HACK: to make more rigourous
|
|
938
|
+
def ch_water_pressure(self) -> float | None: # 3220|12, or 1300
|
|
939
|
+
return self._result_by_lookup(Code._1300, key=SZ_PRESSURE)
|
|
968
940
|
|
|
969
941
|
@property
|
|
970
|
-
def dhw_flow_rate(self) ->
|
|
942
|
+
def dhw_flow_rate(self) -> float | None: # 3220|13, or 12F0
|
|
971
943
|
return self._result_by_lookup(Code._12F0, key=SZ_DHW_FLOW_RATE)
|
|
972
944
|
|
|
973
945
|
@property
|
|
974
|
-
def dhw_setpoint(self) ->
|
|
946
|
+
def dhw_setpoint(self) -> float | None: # 3220|38, or 10A0
|
|
975
947
|
return self._result_by_lookup(Code._10A0, key=SZ_SETPOINT)
|
|
976
948
|
|
|
977
949
|
@property
|
|
978
|
-
def dhw_temp(self) ->
|
|
950
|
+
def dhw_temp(self) -> float | None: # 3220|1A, or 1260
|
|
979
951
|
return self._result_by_lookup(Code._1260, key=SZ_TEMPERATURE)
|
|
980
952
|
|
|
981
|
-
@property # TODO
|
|
982
|
-
def max_rel_modulation(
|
|
983
|
-
self
|
|
984
|
-
) -> None | float: # 3220|0E, or 3EF0 (byte 8, only R8820A?)
|
|
985
|
-
if self._gwy.config.use_native_ot == "prefer": # HACK
|
|
953
|
+
@property # TODO: no reliable OT equivalent?
|
|
954
|
+
def max_rel_modulation(self) -> float | None: # 3220|0E, or 3EF0 (byte 8)
|
|
955
|
+
if self._gwy.config.use_native_ot == "prefer": # HACK: there'll always be 3EF0
|
|
986
956
|
return self._msg_value(Code._3EF0, key=SZ_MAX_REL_MODULATION)
|
|
987
957
|
return self._result_by_value(
|
|
988
|
-
self._ot_msg_value(
|
|
958
|
+
self._ot_msg_value(MsgId._0E), # NOTE: not reliable?
|
|
989
959
|
self._msg_value(Code._3EF0, key=SZ_MAX_REL_MODULATION),
|
|
990
960
|
)
|
|
991
961
|
|
|
992
962
|
@property
|
|
993
|
-
def oem_code(self) ->
|
|
994
|
-
return self._ot_msg_value(
|
|
963
|
+
def oem_code(self) -> float | None: # 3220|73, no known RAMSES equivalent
|
|
964
|
+
return self._ot_msg_value(MsgId._73)
|
|
995
965
|
|
|
996
966
|
@property
|
|
997
|
-
def outside_temp(self) ->
|
|
967
|
+
def outside_temp(self) -> float | None: # 3220|1B, 1290
|
|
998
968
|
return self._result_by_lookup(Code._1290, key=SZ_TEMPERATURE)
|
|
999
969
|
|
|
1000
|
-
@property #
|
|
1001
|
-
def rel_modulation_level(self) ->
|
|
1002
|
-
if self._gwy.config.use_native_ot == "prefer": # HACK
|
|
970
|
+
@property # TODO: no reliable OT equivalent?
|
|
971
|
+
def rel_modulation_level(self) -> float | None: # 3220|11, or 3EF0/3EF1
|
|
972
|
+
if self._gwy.config.use_native_ot == "prefer": # HACK: there'll always be 3EF0
|
|
1003
973
|
return self._msg_value((Code._3EF0, Code._3EF1), key=self.MODULATION_LEVEL)
|
|
1004
974
|
return self._result_by_value(
|
|
1005
|
-
self._ot_msg_value(
|
|
975
|
+
self._ot_msg_value(MsgId._11), # NOTE: not reliable?
|
|
1006
976
|
self._msg_value((Code._3EF0, Code._3EF1), key=self.MODULATION_LEVEL),
|
|
1007
977
|
)
|
|
1008
978
|
|
|
1009
|
-
@property #
|
|
1010
|
-
def ch_active(self) ->
|
|
1011
|
-
if self._gwy.config.use_native_ot == "prefer": # HACK
|
|
979
|
+
@property # TODO: no reliable OT equivalent?
|
|
980
|
+
def ch_active(self) -> bool | None: # 3220|00, or 3EF0 (byte 3)
|
|
981
|
+
if self._gwy.config.use_native_ot == "prefer": # HACK: there'll always be 3EF0
|
|
1012
982
|
return self._msg_value(Code._3EF0, key=SZ_CH_ACTIVE)
|
|
1013
983
|
return self._result_by_value(
|
|
1014
|
-
self._ot_msg_flag(
|
|
984
|
+
self._ot_msg_flag(MsgId._00, 8 + 1), # NOTE: not reliable?
|
|
1015
985
|
self._msg_value(Code._3EF0, key=SZ_CH_ACTIVE),
|
|
1016
986
|
)
|
|
1017
987
|
|
|
1018
|
-
@property #
|
|
1019
|
-
def ch_enabled(self) ->
|
|
1020
|
-
if self._gwy.config.use_native_ot == "prefer": # HACK
|
|
988
|
+
@property # TODO: no reliable OT equivalent?
|
|
989
|
+
def ch_enabled(self) -> bool | None: # 3220|00, or 3EF0 (byte 6)
|
|
990
|
+
if self._gwy.config.use_native_ot == "prefer": # HACK: there'll always be 3EF0
|
|
1021
991
|
return self._msg_value(Code._3EF0, key=SZ_CH_ENABLED)
|
|
1022
992
|
return self._result_by_value(
|
|
1023
|
-
self._ot_msg_flag(
|
|
993
|
+
self._ot_msg_flag(MsgId._00, 0), # NOTE: not reliable?
|
|
1024
994
|
self._msg_value(Code._3EF0, key=SZ_CH_ENABLED),
|
|
1025
995
|
)
|
|
1026
996
|
|
|
1027
997
|
@property
|
|
1028
|
-
def cooling_active(self) ->
|
|
1029
|
-
return self._result_by_value(self._ot_msg_flag(
|
|
998
|
+
def cooling_active(self) -> bool | None: # 3220|00, TODO: no known RAMSES
|
|
999
|
+
return self._result_by_value(self._ot_msg_flag(MsgId._00, 8 + 4), None)
|
|
1030
1000
|
|
|
1031
1001
|
@property
|
|
1032
|
-
def cooling_enabled(self) ->
|
|
1033
|
-
return self._result_by_value(self._ot_msg_flag(
|
|
1002
|
+
def cooling_enabled(self) -> bool | None: # 3220|00, TODO: no known RAMSES
|
|
1003
|
+
return self._result_by_value(self._ot_msg_flag(MsgId._00, 2), None)
|
|
1034
1004
|
|
|
1035
|
-
@property #
|
|
1036
|
-
def dhw_active(self) ->
|
|
1037
|
-
if self._gwy.config.use_native_ot == "prefer": # HACK
|
|
1005
|
+
@property # TODO: no reliable OT equivalent?
|
|
1006
|
+
def dhw_active(self) -> bool | None: # 3220|00, or 3EF0 (byte 3)
|
|
1007
|
+
if self._gwy.config.use_native_ot == "prefer": # HACK: there'll always be 3EF0
|
|
1038
1008
|
return self._msg_value(Code._3EF0, key=SZ_DHW_ACTIVE)
|
|
1039
1009
|
return self._result_by_value(
|
|
1040
|
-
self._ot_msg_flag(
|
|
1010
|
+
self._ot_msg_flag(MsgId._00, 8 + 2), # NOTE: not reliable?
|
|
1041
1011
|
self._msg_value(Code._3EF0, key=SZ_DHW_ACTIVE),
|
|
1042
1012
|
)
|
|
1043
1013
|
|
|
1044
1014
|
@property
|
|
1045
|
-
def dhw_blocking(self) ->
|
|
1046
|
-
return self._result_by_value(self._ot_msg_flag(
|
|
1015
|
+
def dhw_blocking(self) -> bool | None: # 3220|00, TODO: no known RAMSES
|
|
1016
|
+
return self._result_by_value(self._ot_msg_flag(MsgId._00, 6), None)
|
|
1047
1017
|
|
|
1048
1018
|
@property
|
|
1049
|
-
def dhw_enabled(self) ->
|
|
1050
|
-
return self._result_by_value(self._ot_msg_flag(
|
|
1019
|
+
def dhw_enabled(self) -> bool | None: # 3220|00, TODO: no known RAMSES
|
|
1020
|
+
return self._result_by_value(self._ot_msg_flag(MsgId._00, 1), None)
|
|
1051
1021
|
|
|
1052
1022
|
@property
|
|
1053
|
-
def fault_present(self) ->
|
|
1054
|
-
return self._result_by_value(self._ot_msg_flag(
|
|
1023
|
+
def fault_present(self) -> bool | None: # 3220|00, TODO: no known RAMSES
|
|
1024
|
+
return self._result_by_value(self._ot_msg_flag(MsgId._00, 8), None)
|
|
1055
1025
|
|
|
1056
|
-
@property #
|
|
1057
|
-
def flame_active(self) ->
|
|
1058
|
-
if self._gwy.config.use_native_ot == "prefer": # HACK
|
|
1059
|
-
return self._msg_value(Code._3EF0, key=
|
|
1026
|
+
@property # TODO: no reliable OT equivalent?
|
|
1027
|
+
def flame_active(self) -> bool | None: # 3220|00, or 3EF0 (byte 3)
|
|
1028
|
+
if self._gwy.config.use_native_ot == "prefer": # HACK: there'll always be 3EF0
|
|
1029
|
+
return self._msg_value(Code._3EF0, key="flame_on")
|
|
1060
1030
|
return self._result_by_value(
|
|
1061
|
-
self._ot_msg_flag(
|
|
1062
|
-
self._msg_value(Code._3EF0, key=
|
|
1031
|
+
self._ot_msg_flag(MsgId._00, 8 + 3), # NOTE: not reliable?
|
|
1032
|
+
self._msg_value(Code._3EF0, key="flame_on"),
|
|
1063
1033
|
)
|
|
1064
1034
|
|
|
1065
1035
|
@property
|
|
1066
|
-
def otc_active(self) ->
|
|
1067
|
-
return self._result_by_value(self._ot_msg_flag(
|
|
1036
|
+
def otc_active(self) -> bool | None: # 3220|00, TODO: no known RAMSES
|
|
1037
|
+
return self._result_by_value(self._ot_msg_flag(MsgId._00, 3), None)
|
|
1068
1038
|
|
|
1069
1039
|
@property
|
|
1070
|
-
def summer_mode(self) ->
|
|
1071
|
-
return self._result_by_value(self._ot_msg_flag(
|
|
1040
|
+
def summer_mode(self) -> bool | None: # 3220|00, TODO: no known RAMSES
|
|
1041
|
+
return self._result_by_value(self._ot_msg_flag(MsgId._00, 5), None)
|
|
1072
1042
|
|
|
1073
1043
|
@property
|
|
1074
|
-
def opentherm_schema(self) -> dict:
|
|
1075
|
-
result = {
|
|
1044
|
+
def opentherm_schema(self) -> dict[str, Any]:
|
|
1045
|
+
result: dict[str, Any] = {
|
|
1076
1046
|
self._ot_msg_name(v): v.payload
|
|
1077
1047
|
for k, v in self._msgs_ot.items()
|
|
1078
|
-
if self._supported_cmds_ctx.get(
|
|
1048
|
+
if self._supported_cmds_ctx.get(k) and int(k, 16) in SCHEMA_DATA_IDS
|
|
1079
1049
|
}
|
|
1080
1050
|
return {
|
|
1081
|
-
m: {k: v for k, v in p.items() if k.startswith(
|
|
1051
|
+
m: {k: v for k, v in p.items() if k.startswith(SZ_VALUE)}
|
|
1082
1052
|
for m, p in result.items()
|
|
1083
1053
|
}
|
|
1084
1054
|
|
|
1085
1055
|
@property
|
|
1086
|
-
def opentherm_counters(self) -> dict:
|
|
1087
|
-
# for msg_id in ("71", "72", ...):
|
|
1056
|
+
def opentherm_counters(self) -> dict[str, Any]: # all are U16
|
|
1088
1057
|
return {
|
|
1089
|
-
SZ_BURNER_HOURS: self._ot_msg_value(
|
|
1090
|
-
SZ_BURNER_STARTS: self._ot_msg_value(
|
|
1091
|
-
SZ_BURNER_FAILED_STARTS: self._ot_msg_value(
|
|
1092
|
-
SZ_CH_PUMP_HOURS: self._ot_msg_value(
|
|
1093
|
-
SZ_CH_PUMP_STARTS: self._ot_msg_value(
|
|
1094
|
-
SZ_DHW_BURNER_HOURS: self._ot_msg_value(
|
|
1095
|
-
SZ_DHW_BURNER_STARTS: self._ot_msg_value(
|
|
1096
|
-
SZ_DHW_PUMP_HOURS: self._ot_msg_value(
|
|
1097
|
-
SZ_DHW_PUMP_STARTS: self._ot_msg_value(
|
|
1098
|
-
SZ_FLAME_SIGNAL_LOW: self._ot_msg_value(
|
|
1099
|
-
} # 0x73 is OEM diagnostic code...
|
|
1058
|
+
SZ_BURNER_HOURS: self._ot_msg_value(MsgId._78),
|
|
1059
|
+
SZ_BURNER_STARTS: self._ot_msg_value(MsgId._74),
|
|
1060
|
+
SZ_BURNER_FAILED_STARTS: self._ot_msg_value(MsgId._71),
|
|
1061
|
+
SZ_CH_PUMP_HOURS: self._ot_msg_value(MsgId._79),
|
|
1062
|
+
SZ_CH_PUMP_STARTS: self._ot_msg_value(MsgId._75),
|
|
1063
|
+
SZ_DHW_BURNER_HOURS: self._ot_msg_value(MsgId._7B),
|
|
1064
|
+
SZ_DHW_BURNER_STARTS: self._ot_msg_value(MsgId._77),
|
|
1065
|
+
SZ_DHW_PUMP_HOURS: self._ot_msg_value(MsgId._7A),
|
|
1066
|
+
SZ_DHW_PUMP_STARTS: self._ot_msg_value(MsgId._76),
|
|
1067
|
+
SZ_FLAME_SIGNAL_LOW: self._ot_msg_value(MsgId._72),
|
|
1068
|
+
} # 0x73 is not a counter: is OEM diagnostic code...
|
|
1100
1069
|
|
|
1101
1070
|
@property
|
|
1102
|
-
def opentherm_params(self) -> dict:
|
|
1071
|
+
def opentherm_params(self) -> dict[str, Any]: # F8_8, U8, {"hb": S8, "lb": S8}
|
|
1103
1072
|
result = {
|
|
1104
1073
|
self._ot_msg_name(v): v.payload
|
|
1105
1074
|
for k, v in self._msgs_ot.items()
|
|
1106
|
-
if self._supported_cmds_ctx.get(
|
|
1075
|
+
if self._supported_cmds_ctx.get(k) and int(k, 16) in PARAMS_DATA_IDS
|
|
1107
1076
|
}
|
|
1108
1077
|
return {
|
|
1109
|
-
m: {k: v for k, v in p.items() if k.startswith(
|
|
1078
|
+
m: {k: v for k, v in p.items() if k.startswith(SZ_VALUE)}
|
|
1110
1079
|
for m, p in result.items()
|
|
1111
1080
|
}
|
|
1112
1081
|
|
|
1113
1082
|
@property
|
|
1114
|
-
def opentherm_status(self) -> dict:
|
|
1115
|
-
return {
|
|
1116
|
-
SZ_BOILER_OUTPUT_TEMP: self._ot_msg_value(
|
|
1117
|
-
SZ_BOILER_RETURN_TEMP: self._ot_msg_value(
|
|
1118
|
-
SZ_BOILER_SETPOINT: self._ot_msg_value(
|
|
1119
|
-
SZ_CH_MAX_SETPOINT: self._ot_msg_value(
|
|
1120
|
-
SZ_CH_WATER_PRESSURE: self._ot_msg_value(
|
|
1121
|
-
SZ_DHW_FLOW_RATE: self._ot_msg_value(
|
|
1122
|
-
SZ_DHW_SETPOINT: self._ot_msg_value(
|
|
1123
|
-
SZ_DHW_TEMP: self._ot_msg_value(
|
|
1124
|
-
SZ_OEM_CODE: self._ot_msg_value(
|
|
1125
|
-
SZ_OUTSIDE_TEMP: self._ot_msg_value(
|
|
1126
|
-
SZ_REL_MODULATION_LEVEL: self._ot_msg_value(
|
|
1083
|
+
def opentherm_status(self) -> dict[str, Any]: # F8_8, U16 (only OEM_CODE) or bool
|
|
1084
|
+
return { # most these are in: STATUS_DATA_IDS
|
|
1085
|
+
SZ_BOILER_OUTPUT_TEMP: self._ot_msg_value(MsgId._19),
|
|
1086
|
+
SZ_BOILER_RETURN_TEMP: self._ot_msg_value(MsgId._1C),
|
|
1087
|
+
SZ_BOILER_SETPOINT: self._ot_msg_value(MsgId._01),
|
|
1088
|
+
# SZ_CH_MAX_SETPOINT: self._ot_msg_value(MsgId._39), # in PARAMS_DATA_IDS
|
|
1089
|
+
SZ_CH_WATER_PRESSURE: self._ot_msg_value(MsgId._12),
|
|
1090
|
+
SZ_DHW_FLOW_RATE: self._ot_msg_value(MsgId._13),
|
|
1091
|
+
# SZ_DHW_SETPOINT: self._ot_msg_value(MsgId._38), # in PARAMS_DATA_IDS
|
|
1092
|
+
SZ_DHW_TEMP: self._ot_msg_value(MsgId._1A),
|
|
1093
|
+
SZ_OEM_CODE: self._ot_msg_value(MsgId._73),
|
|
1094
|
+
SZ_OUTSIDE_TEMP: self._ot_msg_value(MsgId._1B),
|
|
1095
|
+
SZ_REL_MODULATION_LEVEL: self._ot_msg_value(MsgId._11),
|
|
1096
|
+
#
|
|
1097
|
+
# SZ...: self._ot_msg_value(MsgId._05), # in STATUS_DATA_IDS
|
|
1098
|
+
# SZ...: self._ot_msg_value(MsgId._18), # in STATUS_DATA_IDS
|
|
1127
1099
|
#
|
|
1128
|
-
SZ_CH_ACTIVE: self._ot_msg_flag(
|
|
1129
|
-
SZ_CH_ENABLED: self._ot_msg_flag(
|
|
1130
|
-
SZ_COOLING_ACTIVE: self._ot_msg_flag(
|
|
1131
|
-
SZ_COOLING_ENABLED: self._ot_msg_flag(
|
|
1132
|
-
SZ_DHW_ACTIVE: self._ot_msg_flag(
|
|
1133
|
-
SZ_DHW_BLOCKING: self._ot_msg_flag(
|
|
1134
|
-
SZ_DHW_ENABLED: self._ot_msg_flag(
|
|
1135
|
-
SZ_FAULT_PRESENT: self._ot_msg_flag(
|
|
1136
|
-
SZ_FLAME_ACTIVE: self._ot_msg_flag(
|
|
1137
|
-
SZ_SUMMER_MODE: self._ot_msg_flag(
|
|
1138
|
-
SZ_OTC_ACTIVE: self._ot_msg_flag(
|
|
1100
|
+
SZ_CH_ACTIVE: self._ot_msg_flag(MsgId._00, 8 + 1),
|
|
1101
|
+
SZ_CH_ENABLED: self._ot_msg_flag(MsgId._00, 0),
|
|
1102
|
+
SZ_COOLING_ACTIVE: self._ot_msg_flag(MsgId._00, 8 + 4),
|
|
1103
|
+
SZ_COOLING_ENABLED: self._ot_msg_flag(MsgId._00, 2),
|
|
1104
|
+
SZ_DHW_ACTIVE: self._ot_msg_flag(MsgId._00, 8 + 2),
|
|
1105
|
+
SZ_DHW_BLOCKING: self._ot_msg_flag(MsgId._00, 6),
|
|
1106
|
+
SZ_DHW_ENABLED: self._ot_msg_flag(MsgId._00, 1),
|
|
1107
|
+
SZ_FAULT_PRESENT: self._ot_msg_flag(MsgId._00, 8),
|
|
1108
|
+
SZ_FLAME_ACTIVE: self._ot_msg_flag(MsgId._00, 8 + 3),
|
|
1109
|
+
SZ_SUMMER_MODE: self._ot_msg_flag(MsgId._00, 5),
|
|
1110
|
+
SZ_OTC_ACTIVE: self._ot_msg_flag(MsgId._00, 3),
|
|
1139
1111
|
}
|
|
1140
1112
|
|
|
1141
1113
|
@property
|
|
1142
|
-
def ramses_schema(self) ->
|
|
1114
|
+
def ramses_schema(self) -> PayDictT.EMPTY:
|
|
1143
1115
|
return {}
|
|
1144
1116
|
|
|
1145
1117
|
@property
|
|
1146
|
-
def ramses_params(self) -> dict:
|
|
1118
|
+
def ramses_params(self) -> dict[str, float | None]:
|
|
1147
1119
|
return {
|
|
1148
1120
|
SZ_MAX_REL_MODULATION: self.max_rel_modulation,
|
|
1149
1121
|
}
|
|
1150
1122
|
|
|
1151
1123
|
@property
|
|
1152
|
-
def ramses_status(self) -> dict:
|
|
1124
|
+
def ramses_status(self) -> dict[str, Any]:
|
|
1153
1125
|
return {
|
|
1154
1126
|
SZ_BOILER_OUTPUT_TEMP: self._msg_value(Code._3200, key=SZ_TEMPERATURE),
|
|
1155
1127
|
SZ_BOILER_RETURN_TEMP: self._msg_value(Code._3210, key=SZ_TEMPERATURE),
|
|
@@ -1230,11 +1202,10 @@ class OtbGateway(Actuator, HeatDemand): # OTB (10): 3220 (22D9, others)
|
|
|
1230
1202
|
}
|
|
1231
1203
|
|
|
1232
1204
|
|
|
1233
|
-
class Thermostat(BatteryState, Setpoint, Temperature): # THM (..):
|
|
1205
|
+
class Thermostat(BatteryState, Setpoint, Temperature, Fakeable): # THM (..):
|
|
1234
1206
|
"""The THM/STA class, such as a TR87RF."""
|
|
1235
1207
|
|
|
1236
|
-
_SLUG
|
|
1237
|
-
|
|
1208
|
+
_SLUG = DevType.THM
|
|
1238
1209
|
_STATE_ATTR = SZ_TEMPERATURE
|
|
1239
1210
|
|
|
1240
1211
|
def _handle_msg(self, msg: Message) -> None:
|
|
@@ -1274,25 +1245,29 @@ class Thermostat(BatteryState, Setpoint, Temperature): # THM (..):
|
|
|
1274
1245
|
elif self._iz_controller is False: # TODO: raise CorruptStateError
|
|
1275
1246
|
_LOGGER.error(f"{msg!r} # IS_CONTROLLER (21): was FALSE, now True")
|
|
1276
1247
|
|
|
1248
|
+
async def initiate_binding_process(self) -> Packet:
|
|
1249
|
+
return await super()._initiate_binding_process(
|
|
1250
|
+
(Code._2309, Code._30C9, Code._0008)
|
|
1251
|
+
)
|
|
1252
|
+
|
|
1277
1253
|
|
|
1278
1254
|
class BdrSwitch(Actuator, RelayDemand): # BDR (13):
|
|
1279
1255
|
"""The BDR class, such as a BDR91.
|
|
1280
1256
|
|
|
1281
|
-
BDR91s can be used in six
|
|
1257
|
+
BDR91s can be used in six distinct modes, including:
|
|
1282
1258
|
- x2 boiler controller (FC/TPI): either traditional, or newer heat pump-aware
|
|
1283
1259
|
- x1 electric heat zones (0x/ELE)
|
|
1284
1260
|
- x1 zone valve zones (0x/VAL)
|
|
1285
1261
|
- x2 DHW thingys (F9/DHW, FA/DHW)
|
|
1286
1262
|
"""
|
|
1287
1263
|
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
ACTIVE = "active"
|
|
1291
|
-
TPI_PARAMS = "tpi_params"
|
|
1264
|
+
ACTIVE: Final = "active"
|
|
1265
|
+
TPI_PARAMS: Final = "tpi_params"
|
|
1292
1266
|
|
|
1267
|
+
_SLUG = DevType.BDR
|
|
1293
1268
|
_STATE_ATTR = "active"
|
|
1294
1269
|
|
|
1295
|
-
# def __init__(self, *args, **kwargs) -> None:
|
|
1270
|
+
# def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
1296
1271
|
# super().__init__(*args, **kwargs)
|
|
1297
1272
|
|
|
1298
1273
|
# if kwargs.get(SZ_DOMAIN_ID) == FC: # TODO: F9/FA/FC, zone_idx
|
|
@@ -1316,41 +1291,42 @@ class BdrSwitch(Actuator, RelayDemand): # BDR (13):
|
|
|
1316
1291
|
|
|
1317
1292
|
super()._setup_discovery_cmds()
|
|
1318
1293
|
|
|
1319
|
-
if self.
|
|
1294
|
+
if self.is_faked:
|
|
1320
1295
|
return
|
|
1321
1296
|
|
|
1322
1297
|
self._add_discovery_cmd(Command.get_tpi_params(self.id), 6 * 3600) # params
|
|
1323
1298
|
self._add_discovery_cmd(
|
|
1324
|
-
|
|
1299
|
+
Command.from_attrs(RQ, self.id, Code._3EF1, "00"),
|
|
1325
1300
|
60 if self._child_id in (F9, FA, FC) else 300,
|
|
1326
1301
|
) # status
|
|
1327
1302
|
|
|
1328
1303
|
@property
|
|
1329
|
-
def active(self) ->
|
|
1304
|
+
def active(self) -> bool | None: # 3EF0, 3EF1
|
|
1330
1305
|
"""Return the actuator's current state."""
|
|
1331
1306
|
result = self._msg_value((Code._3EF0, Code._3EF1), key=self.MODULATION_LEVEL)
|
|
1332
1307
|
return None if result is None else bool(result)
|
|
1333
1308
|
|
|
1334
1309
|
@property
|
|
1335
|
-
def role(self) ->
|
|
1310
|
+
def role(self) -> str | None:
|
|
1336
1311
|
"""Return the role of the BDR91A (there are six possibilities)."""
|
|
1337
1312
|
|
|
1338
1313
|
# TODO: use self._parent?
|
|
1339
1314
|
if self._child_id in DOMAIN_TYPE_MAP:
|
|
1340
1315
|
return DOMAIN_TYPE_MAP[self._child_id]
|
|
1341
|
-
elif self._parent:
|
|
1342
|
-
|
|
1316
|
+
elif self._parent and isinstance(self._parent, Zone):
|
|
1317
|
+
# TODO: remove need for isinstance
|
|
1318
|
+
return self._parent.heating_type
|
|
1343
1319
|
|
|
1344
|
-
# if Code._3B00 in
|
|
1320
|
+
# if Code._3B00 in _msgs and _msgs[Code._3B00].verb == I_:
|
|
1345
1321
|
# self._is_tpi = True
|
|
1346
|
-
# if Code._1FC9 in
|
|
1347
|
-
# if Code._3B00 in
|
|
1322
|
+
# if Code._1FC9 in _msgs and _msgs[Code._1FC9].verb == RP:
|
|
1323
|
+
# if Code._3B00 in _msgs[Code._1FC9].raw_payload:
|
|
1348
1324
|
# self._is_tpi = True
|
|
1349
1325
|
|
|
1350
1326
|
return None
|
|
1351
1327
|
|
|
1352
1328
|
@property
|
|
1353
|
-
def tpi_params(self) ->
|
|
1329
|
+
def tpi_params(self) -> PayDictT._10A0 | None:
|
|
1354
1330
|
return self._msg_value(Code._1100)
|
|
1355
1331
|
|
|
1356
1332
|
@property
|
|
@@ -1378,21 +1354,20 @@ class BdrSwitch(Actuator, RelayDemand): # BDR (13):
|
|
|
1378
1354
|
class TrvActuator(BatteryState, HeatDemand, Setpoint, Temperature): # TRV (04):
|
|
1379
1355
|
"""The TRV class, such as a HR92."""
|
|
1380
1356
|
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
WINDOW_OPEN = SZ_WINDOW_OPEN # boolean
|
|
1357
|
+
WINDOW_OPEN: Final = SZ_WINDOW_OPEN
|
|
1384
1358
|
|
|
1359
|
+
_SLUG = DevType.TRV
|
|
1385
1360
|
_STATE_ATTR = SZ_HEAT_DEMAND
|
|
1386
1361
|
|
|
1387
1362
|
@property
|
|
1388
|
-
def heat_demand(self) ->
|
|
1363
|
+
def heat_demand(self) -> float | None: # 3150
|
|
1389
1364
|
if (heat_demand := super().heat_demand) is None:
|
|
1390
1365
|
if self._msg_value(Code._3150) is None and self.setpoint is False:
|
|
1391
1366
|
return 0 # instead of None (no 3150s sent when setpoint is False)
|
|
1392
1367
|
return heat_demand
|
|
1393
1368
|
|
|
1394
1369
|
@property
|
|
1395
|
-
def window_open(self) ->
|
|
1370
|
+
def window_open(self) -> bool | None: # 12B0
|
|
1396
1371
|
return self._msg_value(Code._12B0, key=self.WINDOW_OPEN)
|
|
1397
1372
|
|
|
1398
1373
|
@property
|
|
@@ -1404,14 +1379,16 @@ class TrvActuator(BatteryState, HeatDemand, Setpoint, Temperature): # TRV (04):
|
|
|
1404
1379
|
|
|
1405
1380
|
|
|
1406
1381
|
class JimDevice(Actuator): # BDR (08):
|
|
1407
|
-
_SLUG: str =
|
|
1382
|
+
_SLUG: str = DevType.JIM
|
|
1383
|
+
_STATE_ATTR = None
|
|
1408
1384
|
|
|
1409
1385
|
|
|
1410
1386
|
class JstDevice(RelayDemand): # BDR (31):
|
|
1411
|
-
_SLUG: str =
|
|
1387
|
+
_SLUG: str = DevType.JST
|
|
1388
|
+
_STATE_ATTR = None
|
|
1412
1389
|
|
|
1413
1390
|
|
|
1414
|
-
class UfhCircuit(Entity):
|
|
1391
|
+
class UfhCircuit(Child, Entity): # FIXME
|
|
1415
1392
|
"""The UFH circuit class (UFC:circuit is much like CTL/TCS:zone).
|
|
1416
1393
|
|
|
1417
1394
|
NOTE: for circuits, there's a difference between :
|
|
@@ -1419,82 +1396,93 @@ class UfhCircuit(Entity):
|
|
|
1419
1396
|
- `self.tcs.ctl`: the Evohome controller
|
|
1420
1397
|
"""
|
|
1421
1398
|
|
|
1422
|
-
_SLUG: str = None
|
|
1399
|
+
_SLUG: str = None
|
|
1400
|
+
_STATE_ATTR = None
|
|
1423
1401
|
|
|
1424
|
-
def __init__(self, ufc, ufh_idx: str) -> None:
|
|
1402
|
+
def __init__(self, ufc: UfhController, ufh_idx: str) -> None:
|
|
1425
1403
|
super().__init__(ufc._gwy)
|
|
1426
1404
|
|
|
1405
|
+
# FIXME: ZZZ entities must know their parent device ID and their own idx
|
|
1406
|
+
self._z_id = ufc.id
|
|
1407
|
+
self._z_idx = ufh_idx
|
|
1408
|
+
|
|
1427
1409
|
self.id: str = f"{ufc.id}_{ufh_idx}"
|
|
1428
1410
|
|
|
1429
1411
|
self.ufc: UfhController = ufc
|
|
1430
1412
|
self._child_id = ufh_idx
|
|
1431
1413
|
|
|
1432
1414
|
# TODO: _ctl should be: .ufc? .ctl?
|
|
1433
|
-
self._ctl: Controller = None
|
|
1434
|
-
self._zone:
|
|
1415
|
+
self._ctl: Controller = None
|
|
1416
|
+
self._zone: Zone | None = None
|
|
1435
1417
|
|
|
1436
1418
|
# def __str__(self) -> str:
|
|
1437
1419
|
# return f"{self.id} ({self._zone and self._zone._child_id})"
|
|
1438
1420
|
|
|
1421
|
+
def _update_schema(self, **kwargs: Any) -> None:
|
|
1422
|
+
raise NotImplementedError
|
|
1423
|
+
|
|
1439
1424
|
def _handle_msg(self, msg: Message) -> None:
|
|
1440
1425
|
super()._handle_msg(msg)
|
|
1441
1426
|
|
|
1442
|
-
#
|
|
1443
|
-
|
|
1427
|
+
if msg.code != Code._000C or not msg.payload[SZ_DEVICES]: # zone_devices
|
|
1428
|
+
return
|
|
1444
1429
|
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1430
|
+
# FIXME: is messy
|
|
1431
|
+
if not (dev_ids := msg.payload[SZ_DEVICES]):
|
|
1432
|
+
return
|
|
1433
|
+
if len(dev_ids) != 1:
|
|
1434
|
+
raise exc.PacketPayloadInvalid("No devices")
|
|
1449
1435
|
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1436
|
+
# ctl = self._gwy.device_by_id.get(dev_ids[0])
|
|
1437
|
+
ctl: Controller = self._gwy.get_device(dev_ids[0])
|
|
1438
|
+
if not ctl or (self._ctl and self._ctl is not ctl):
|
|
1439
|
+
raise exc.PacketPayloadInvalid("No CTL")
|
|
1440
|
+
self._ctl = ctl
|
|
1455
1441
|
|
|
1456
|
-
|
|
1457
|
-
|
|
1442
|
+
ctl._make_tcs_controller()
|
|
1443
|
+
# self.set_parent(ctl.tcs)
|
|
1458
1444
|
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1445
|
+
zon = ctl.tcs.get_htg_zone(msg.payload[SZ_ZONE_IDX])
|
|
1446
|
+
if not zon:
|
|
1447
|
+
raise exc.PacketPayloadInvalid("No Zone")
|
|
1448
|
+
if self._zone and self._zone is not zon:
|
|
1449
|
+
raise exc.PacketPayloadInvalid("Wrong Zone")
|
|
1450
|
+
self._zone = zon
|
|
1465
1451
|
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1452
|
+
if self.ufc not in self._zone.actuators:
|
|
1453
|
+
schema = {SZ_ACTUATORS: [self.ufc.id], SZ_CIRCUITS: [self.id]}
|
|
1454
|
+
self._zone._update_schema(**schema)
|
|
1469
1455
|
|
|
1470
1456
|
@property
|
|
1471
1457
|
def ufx_idx(self) -> str:
|
|
1472
1458
|
return self._child_id
|
|
1473
1459
|
|
|
1474
1460
|
@property
|
|
1475
|
-
def zone_idx(self) ->
|
|
1461
|
+
def zone_idx(self) -> str | None:
|
|
1476
1462
|
if self._zone:
|
|
1477
1463
|
return self._zone._child_id
|
|
1464
|
+
return None
|
|
1478
1465
|
|
|
1479
1466
|
|
|
1480
|
-
|
|
1467
|
+
# e.g. {"CTL": Controller}
|
|
1468
|
+
HEAT_CLASS_BY_SLUG: dict[str, type[DeviceHeat]] = class_by_attr(__name__, "_SLUG")
|
|
1481
1469
|
|
|
1482
1470
|
_HEAT_VC_PAIR_BY_CLASS = {
|
|
1483
|
-
|
|
1484
|
-
|
|
1471
|
+
DevType.DHW: ((I_, Code._1260),),
|
|
1472
|
+
DevType.OTB: ((I_, Code._3220), (RP, Code._3220)),
|
|
1485
1473
|
}
|
|
1486
1474
|
|
|
1487
1475
|
|
|
1488
1476
|
def class_dev_heat(
|
|
1489
|
-
dev_addr: Address, *, msg: Message = None, eavesdrop: bool = False
|
|
1490
|
-
) -> type[
|
|
1477
|
+
dev_addr: Address, *, msg: Message | None = None, eavesdrop: bool = False
|
|
1478
|
+
) -> type[DeviceHeat]:
|
|
1491
1479
|
"""Return a device class, but only if the device must be from the CH/DHW group.
|
|
1492
1480
|
|
|
1493
1481
|
May return a device class, DeviceHeat (which will need promotion).
|
|
1494
1482
|
"""
|
|
1495
1483
|
|
|
1496
1484
|
if dev_addr.type in DEV_TYPE_MAP.THM_DEVICES:
|
|
1497
|
-
return HEAT_CLASS_BY_SLUG[
|
|
1485
|
+
return HEAT_CLASS_BY_SLUG[DevType.THM]
|
|
1498
1486
|
|
|
1499
1487
|
try:
|
|
1500
1488
|
slug = DEV_TYPE_MAP.slug(dev_addr.type)
|