ramses-rf 0.22.40__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 +377 -513
- 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.40.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.40.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_rf/protocol → ramses_tx}/parsers.py +1266 -1003
- 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 -1576
- 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/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.40.dist-info/METADATA +0 -64
- ramses_rf-0.22.40.dist-info/RECORD +0 -42
- ramses_rf-0.22.40.dist-info/top_level.txt +0 -1
ramses_rf/system/zones.py
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
#
|
|
4
2
|
"""RAMSES RF - The evohome-compatible zones."""
|
|
3
|
+
|
|
5
4
|
from __future__ import annotations
|
|
6
5
|
|
|
6
|
+
import asyncio
|
|
7
7
|
import logging
|
|
8
8
|
import math
|
|
9
|
-
from
|
|
10
|
-
from
|
|
11
|
-
from datetime import timedelta as td
|
|
12
|
-
from typing import Any, TypeVar
|
|
9
|
+
from datetime import datetime as dt, timedelta as td
|
|
10
|
+
from typing import TYPE_CHECKING, Any, TypeVar
|
|
13
11
|
|
|
14
|
-
from
|
|
15
|
-
|
|
12
|
+
from ramses_rf import exceptions as exc
|
|
13
|
+
from ramses_rf.const import (
|
|
16
14
|
DEV_ROLE_MAP,
|
|
17
15
|
DEV_TYPE_MAP,
|
|
18
16
|
SZ_DOMAIN_ID,
|
|
@@ -26,11 +24,11 @@ from ..const import (
|
|
|
26
24
|
SZ_ZONE_IDX,
|
|
27
25
|
SZ_ZONE_TYPE,
|
|
28
26
|
ZON_MODE_MAP,
|
|
29
|
-
ZON_ROLE,
|
|
30
27
|
ZON_ROLE_MAP,
|
|
31
|
-
|
|
28
|
+
DevRole,
|
|
29
|
+
ZoneRole,
|
|
32
30
|
)
|
|
33
|
-
from
|
|
31
|
+
from ramses_rf.device import (
|
|
34
32
|
BdrSwitch,
|
|
35
33
|
Controller,
|
|
36
34
|
Device,
|
|
@@ -38,12 +36,9 @@ from ..device import (
|
|
|
38
36
|
TrvActuator,
|
|
39
37
|
UfhController,
|
|
40
38
|
)
|
|
41
|
-
from
|
|
42
|
-
from
|
|
43
|
-
from
|
|
44
|
-
from ..protocol.command import _mk_cmd
|
|
45
|
-
from ..protocol.const import SZ_PAYLOAD
|
|
46
|
-
from ..schemas import (
|
|
39
|
+
from ramses_rf.entity_base import Child, Entity, Parent, class_by_attr
|
|
40
|
+
from ramses_rf.helpers import shrink
|
|
41
|
+
from ramses_rf.schemas import (
|
|
47
42
|
SCH_TCS_DHW,
|
|
48
43
|
SCH_TCS_ZONES_ZON,
|
|
49
44
|
SZ_ACTUATORS,
|
|
@@ -53,98 +48,114 @@ from ..schemas import (
|
|
|
53
48
|
SZ_HTG_VALVE,
|
|
54
49
|
SZ_SENSOR,
|
|
55
50
|
)
|
|
56
|
-
from
|
|
51
|
+
from ramses_tx import Address, Command, Message, Priority
|
|
52
|
+
|
|
53
|
+
from .schedule import InnerScheduleT, OuterScheduleT, Schedule
|
|
54
|
+
|
|
55
|
+
if TYPE_CHECKING:
|
|
56
|
+
from ramses_tx import Packet
|
|
57
|
+
from ramses_tx.schemas import DeviceIdT, DevIndexT
|
|
58
|
+
from ramses_tx.typed_dicts import PayDictT
|
|
59
|
+
|
|
60
|
+
from .heat import Evohome, _MultiZoneT, _StoredHwT
|
|
61
|
+
|
|
57
62
|
|
|
58
63
|
# Kudos & many thanks to:
|
|
59
64
|
# - @dbmandrake: valve_position -> heat_demand transform
|
|
60
65
|
|
|
61
|
-
# TODO: add optional eavesdrop of zone_type
|
|
62
66
|
|
|
67
|
+
from ramses_rf.const import ( # noqa: F401, isort: skip, pylint: disable=unused-import
|
|
68
|
+
F9,
|
|
69
|
+
FA,
|
|
70
|
+
FC,
|
|
71
|
+
FF,
|
|
72
|
+
)
|
|
63
73
|
|
|
64
|
-
#
|
|
65
|
-
from ..protocol import ( # noqa: F401, isort: skip, pylint: disable=unused-import
|
|
74
|
+
from ramses_rf.const import ( # noqa: F401, isort: skip, pylint: disable=unused-import
|
|
66
75
|
I_,
|
|
67
76
|
RP,
|
|
68
77
|
RQ,
|
|
69
78
|
W_,
|
|
70
|
-
F9,
|
|
71
|
-
FA,
|
|
72
|
-
FC,
|
|
73
|
-
FF,
|
|
74
79
|
Code,
|
|
75
80
|
)
|
|
76
81
|
|
|
77
|
-
|
|
78
|
-
DEV_MODE = __dev_mode__
|
|
79
|
-
|
|
80
82
|
_LOGGER = logging.getLogger(__name__)
|
|
81
|
-
if DEV_MODE:
|
|
82
|
-
_LOGGER.setLevel(logging.DEBUG)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
_ZoneT = TypeVar("_ZoneT", bound="ZoneBase")
|
|
86
83
|
|
|
87
84
|
|
|
88
85
|
class ZoneBase(Child, Parent, Entity):
|
|
89
86
|
"""The Zone/DHW base class."""
|
|
90
87
|
|
|
91
|
-
_SLUG: str = None
|
|
92
|
-
|
|
93
|
-
|
|
88
|
+
_SLUG: str = None
|
|
89
|
+
|
|
90
|
+
_ROLE_ACTUATORS: str = None
|
|
91
|
+
_ROLE_SENSORS: str = None
|
|
94
92
|
|
|
95
|
-
def __init__(self, tcs, zone_idx: str) -> None:
|
|
93
|
+
def __init__(self, tcs: _MultiZoneT | _StoredHwT, zone_idx: str) -> None:
|
|
96
94
|
super().__init__(tcs._gwy)
|
|
97
95
|
|
|
96
|
+
# FIXME: ZZZ entities must know their parent device ID and their own idx
|
|
97
|
+
self._z_id = tcs.id # the responsible device is the controller
|
|
98
|
+
self._z_idx: DevIndexT = zone_idx # the zone idx (ctx), 00-0B (or 0F), HW (FA)
|
|
99
|
+
|
|
98
100
|
self.id: str = f"{tcs.id}_{zone_idx}"
|
|
99
101
|
|
|
100
|
-
self.tcs = tcs
|
|
102
|
+
self.tcs: Evohome = tcs
|
|
101
103
|
self.ctl: Controller = tcs.ctl
|
|
102
|
-
self._child_id = zone_idx
|
|
104
|
+
self._child_id: str = zone_idx
|
|
103
105
|
|
|
104
106
|
self._name = None # param attr
|
|
105
107
|
|
|
108
|
+
# Should be a private method
|
|
106
109
|
@classmethod
|
|
107
|
-
def create_from_schema(
|
|
110
|
+
def create_from_schema(
|
|
111
|
+
cls, tcs: _MultiZoneT, zone_idx: str, **schema: Any
|
|
112
|
+
) -> ZoneBase:
|
|
108
113
|
"""Create a CH/DHW zone for a TCS and set its schema attrs.
|
|
109
114
|
|
|
110
115
|
The appropriate Zone class should have been determined by a factory.
|
|
111
116
|
Can be a heating zone (of a klass), or the DHW subsystem (idx must be 'HW').
|
|
112
117
|
"""
|
|
113
118
|
|
|
114
|
-
zon = cls(tcs, zone_idx)
|
|
119
|
+
zon = cls(tcs, zone_idx) # type: ignore[arg-type]
|
|
115
120
|
zon._update_schema(**schema)
|
|
116
121
|
return zon
|
|
117
122
|
|
|
118
|
-
def _update_schema(self, **schema):
|
|
123
|
+
def _update_schema(self, **schema: Any) -> None:
|
|
119
124
|
raise NotImplementedError
|
|
120
125
|
|
|
121
126
|
def __repr__(self) -> str:
|
|
122
127
|
return f"{self.id} ({self._SLUG})"
|
|
123
128
|
|
|
124
|
-
def __lt__(self, other) -> bool:
|
|
129
|
+
def __lt__(self, other: object) -> bool:
|
|
125
130
|
if not isinstance(other, ZoneBase):
|
|
126
131
|
return NotImplemented
|
|
127
132
|
return self.idx < other.idx
|
|
128
133
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
134
|
+
@property
|
|
135
|
+
def idx(self) -> str:
|
|
136
|
+
return self._child_id
|
|
132
137
|
|
|
133
138
|
@property
|
|
134
|
-
def
|
|
135
|
-
"""Return the
|
|
136
|
-
return
|
|
139
|
+
def schema(self) -> dict[str, Any]:
|
|
140
|
+
"""Return the schema (can't change without destroying/re-creating entity)."""
|
|
141
|
+
return {}
|
|
137
142
|
|
|
138
143
|
@property
|
|
139
|
-
def
|
|
140
|
-
|
|
144
|
+
def params(self) -> dict[str, Any]:
|
|
145
|
+
"""Return configuration (can be changed by user)."""
|
|
146
|
+
return {}
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def status(self) -> dict[str, Any]:
|
|
150
|
+
"""Return the current state."""
|
|
151
|
+
return {}
|
|
141
152
|
|
|
142
153
|
|
|
143
|
-
class ZoneSchedule: # 0404
|
|
144
|
-
def __init__(self, *args, **kwargs) -> None:
|
|
154
|
+
class ZoneSchedule(ZoneBase): # 0404
|
|
155
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
145
156
|
super().__init__(*args, **kwargs)
|
|
146
157
|
|
|
147
|
-
self._schedule = Schedule(self) #
|
|
158
|
+
self._schedule = Schedule(self) # type: ignore[arg-type]
|
|
148
159
|
|
|
149
160
|
def _handle_msg(self, msg: Message) -> None:
|
|
150
161
|
super()._handle_msg(msg)
|
|
@@ -152,21 +163,24 @@ class ZoneSchedule: # 0404
|
|
|
152
163
|
if msg.code in (Code._0006, Code._0404):
|
|
153
164
|
self._schedule._handle_msg(msg)
|
|
154
165
|
|
|
155
|
-
async def get_schedule(self, *, force_io=
|
|
166
|
+
async def get_schedule(self, *, force_io: bool = False) -> InnerScheduleT | None:
|
|
156
167
|
await self._schedule.get_schedule(force_io=force_io)
|
|
157
168
|
return self.schedule
|
|
158
169
|
|
|
159
|
-
async def set_schedule(self, schedule) ->
|
|
160
|
-
await self._schedule.set_schedule(schedule)
|
|
170
|
+
async def set_schedule(self, schedule: OuterScheduleT) -> InnerScheduleT | None:
|
|
171
|
+
await self._schedule.set_schedule(schedule) # type: ignore[arg-type]
|
|
161
172
|
return self.schedule
|
|
162
173
|
|
|
163
174
|
@property
|
|
164
|
-
def schedule(self) ->
|
|
165
|
-
"""Return the latest
|
|
175
|
+
def schedule(self) -> InnerScheduleT | None:
|
|
176
|
+
"""Return the latest retrieved schedule (not guaranteed to be up to date)."""
|
|
177
|
+
# inner: [{"day_of_week": 0, "switchpoints": [...], {"day_of_week": 1, ...
|
|
178
|
+
# outer: {"zone_idx": "01", "schedule": <inner>
|
|
179
|
+
|
|
166
180
|
return self._schedule.schedule
|
|
167
181
|
|
|
168
182
|
@property
|
|
169
|
-
def schedule_version(self) ->
|
|
183
|
+
def schedule_version(self) -> int | None: # TODO: make int
|
|
170
184
|
"""Return the version number associated with the latest retrieved schedule."""
|
|
171
185
|
return self._schedule.version
|
|
172
186
|
|
|
@@ -178,12 +192,12 @@ class ZoneSchedule: # 0404
|
|
|
178
192
|
}
|
|
179
193
|
|
|
180
194
|
|
|
181
|
-
class DhwZone(ZoneSchedule
|
|
195
|
+
class DhwZone(ZoneSchedule): # CS92A
|
|
182
196
|
"""The DHW class."""
|
|
183
197
|
|
|
184
|
-
_SLUG: str =
|
|
198
|
+
_SLUG: str = ZoneRole.DHW
|
|
185
199
|
|
|
186
|
-
def __init__(self, tcs, zone_idx: str = "HW") -> None:
|
|
200
|
+
def __init__(self, tcs: _StoredHwT, zone_idx: str = "HW") -> None:
|
|
187
201
|
_LOGGER.debug("Creating a DHW for TCS: %s_HW (%s)", tcs.id, self.__class__)
|
|
188
202
|
|
|
189
203
|
if tcs.dhw:
|
|
@@ -193,20 +207,21 @@ class DhwZone(ZoneSchedule, ZoneBase): # CS92A # TODO: add Schedule
|
|
|
193
207
|
|
|
194
208
|
super().__init__(tcs, "HW")
|
|
195
209
|
|
|
196
|
-
|
|
197
|
-
self.
|
|
198
|
-
self.
|
|
210
|
+
# DhwZones have a sensor, but actuators are optional, depending on schema
|
|
211
|
+
self._dhw_sensor: DhwSensor | None = None
|
|
212
|
+
self._dhw_valve: BdrSwitch | None = None
|
|
213
|
+
self._htg_valve: BdrSwitch | None = None
|
|
199
214
|
|
|
200
215
|
def _setup_discovery_cmds(self) -> None:
|
|
201
216
|
# super()._setup_discovery_cmds()
|
|
202
217
|
|
|
203
218
|
for payload in (
|
|
204
|
-
f"00{DEV_ROLE_MAP.DHW}",
|
|
205
|
-
f"00{DEV_ROLE_MAP.HTG}",
|
|
206
|
-
f"01{DEV_ROLE_MAP.HTG}",
|
|
219
|
+
f"00{DEV_ROLE_MAP.DHW}", # sensor
|
|
220
|
+
f"00{DEV_ROLE_MAP.HTG}", # hotwater_valve
|
|
221
|
+
f"01{DEV_ROLE_MAP.HTG}", # heating_valve
|
|
207
222
|
):
|
|
208
223
|
self._add_discovery_cmd(
|
|
209
|
-
|
|
224
|
+
Command.from_attrs(RQ, self.ctl.id, Code._000C, payload), 60 * 60 * 24
|
|
210
225
|
)
|
|
211
226
|
|
|
212
227
|
self._add_discovery_cmd(Command.get_dhw_params(self.ctl.id), 60 * 60 * 6)
|
|
@@ -215,40 +230,40 @@ class DhwZone(ZoneSchedule, ZoneBase): # CS92A # TODO: add Schedule
|
|
|
215
230
|
self._add_discovery_cmd(Command.get_dhw_temp(self.ctl.id), 60 * 15)
|
|
216
231
|
|
|
217
232
|
def _handle_msg(self, msg: Message) -> None:
|
|
218
|
-
def eavesdrop_dhw_sensor(this, *, prev=None) -> None:
|
|
219
|
-
|
|
233
|
+
# def eavesdrop_dhw_sensor(this: Message, *, prev: Message | None = None) -> None:
|
|
234
|
+
# """Eavesdrop packets, or pairs of packets, to maintain the system state.
|
|
220
235
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
236
|
+
# There are only 2 ways to to find a controller's DHW sensor:
|
|
237
|
+
# 1. The 10A0 RQ/RP *from/to a 07:* (1x/4h) - reliable
|
|
238
|
+
# 2. Use sensor temp matching - non-deterministic
|
|
224
239
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
240
|
+
# Data from the CTL is considered more authoritative. The RQ is initiated by the
|
|
241
|
+
# DHW, so is not authoritative. The I/1260 is not to/from a controller, so is
|
|
242
|
+
# not useful.
|
|
243
|
+
# """
|
|
229
244
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
245
|
+
# # 10A0: RQ/07/01, RP/01/07: can get both parent controller & DHW sensor
|
|
246
|
+
# # 047 RQ --- 07:030741 01:102458 --:------ 10A0 006 00181F0003E4
|
|
247
|
+
# # 062 RP --- 01:102458 07:030741 --:------ 10A0 006 0018380003E8
|
|
233
248
|
|
|
234
|
-
|
|
235
|
-
|
|
249
|
+
# # 1260: I/07: can't get which parent controller - would need to match temps
|
|
250
|
+
# # 045 I --- 07:045960 --:------ 07:045960 1260 003 000911
|
|
236
251
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
252
|
+
# # 1F41: I/01: get parent controller, but not DHW sensor
|
|
253
|
+
# # 045 I --- 01:145038 --:------ 01:145038 1F41 012 000004FFFFFF1E060E0507E4
|
|
254
|
+
# # 045 I --- 01:145038 --:------ 01:145038 1F41 006 000002FFFFFF
|
|
240
255
|
|
|
241
|
-
|
|
256
|
+
# assert self._gwy.config.enable_eavesdrop, "Coding error"
|
|
242
257
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
258
|
+
# if all(
|
|
259
|
+
# (
|
|
260
|
+
# this.code == Code._10A0,
|
|
261
|
+
# this.verb == RP,
|
|
262
|
+
# this.src is self.ctl,
|
|
263
|
+
# isinstance(this.dst, DhwSensor),
|
|
264
|
+
# )
|
|
265
|
+
# ):
|
|
266
|
+
# self._get_dhw(sensor=this.dst)
|
|
252
267
|
|
|
253
268
|
assert (
|
|
254
269
|
msg.src is self.ctl
|
|
@@ -280,7 +295,7 @@ class DhwZone(ZoneSchedule, ZoneBase): # CS92A # TODO: add Schedule
|
|
|
280
295
|
# if self._gwy.config.enable_eavesdrop and not self.dhw_sensor:
|
|
281
296
|
# eavesdrop_dhw_sensor(msg)
|
|
282
297
|
|
|
283
|
-
def _update_schema(self, **schema):
|
|
298
|
+
def _update_schema(self, **schema: Any) -> None:
|
|
284
299
|
"""Update a DHW zone with new schema attrs.
|
|
285
300
|
|
|
286
301
|
Raise an exception if the new schema is not a superset of the existing schema.
|
|
@@ -290,38 +305,44 @@ class DhwZone(ZoneSchedule, ZoneBase): # CS92A # TODO: add Schedule
|
|
|
290
305
|
"""Set the heating valve relay for this DHW zone (13: only)."""
|
|
291
306
|
"""Set the hotwater valve relay for this DHW zone (13: only).
|
|
292
307
|
|
|
293
|
-
Check and
|
|
308
|
+
Check and ??? the DHW sensor (07:) of this system/CTL (if there is one).
|
|
294
309
|
|
|
295
310
|
There is only 1 way to eavesdrop a controller's DHW sensor:
|
|
296
311
|
1. The 10A0 RQ/RP *from/to a 07:* (1x/4h)
|
|
297
312
|
|
|
298
|
-
The RQ is initiated by the DHW, so is not
|
|
313
|
+
The RQ is initiated by the DHW, so is not authoritative (the CTL will RP any RQ).
|
|
299
314
|
The I/1260 is not to/from a controller, so is not useful.
|
|
300
|
-
"""
|
|
315
|
+
"""
|
|
301
316
|
|
|
302
317
|
schema = shrink(SCH_TCS_DHW(schema))
|
|
303
318
|
|
|
304
319
|
if dev_id := schema.get(SZ_SENSOR):
|
|
305
|
-
|
|
320
|
+
dhw_sensor = self._gwy.get_device(
|
|
306
321
|
dev_id, parent=self, child_id=FA, is_sensor=True
|
|
307
322
|
)
|
|
323
|
+
assert isinstance(dhw_sensor, DhwSensor) # mypy
|
|
324
|
+
self._dhw_sensor = dhw_sensor
|
|
308
325
|
|
|
309
|
-
if dev_id := schema.get(DEV_ROLE_MAP[
|
|
310
|
-
|
|
326
|
+
if dev_id := schema.get(DEV_ROLE_MAP[DevRole.HTG]):
|
|
327
|
+
dhw_valve = self._gwy.get_device(dev_id, parent=self, child_id=FA)
|
|
328
|
+
assert isinstance(dhw_valve, BdrSwitch) # mypy
|
|
329
|
+
self._dhw_valve = dhw_valve
|
|
311
330
|
|
|
312
|
-
if dev_id := schema.get(DEV_ROLE_MAP[
|
|
313
|
-
|
|
331
|
+
if dev_id := schema.get(DEV_ROLE_MAP[DevRole.HT1]):
|
|
332
|
+
htg_valve = self._gwy.get_device(dev_id, parent=self, child_id=F9)
|
|
333
|
+
assert isinstance(htg_valve, BdrSwitch) # mypy
|
|
334
|
+
self._htg_valve = htg_valve
|
|
314
335
|
|
|
315
336
|
@property
|
|
316
|
-
def sensor(self) -> DhwSensor: # self._dhw_sensor
|
|
337
|
+
def sensor(self) -> DhwSensor | None: # self._dhw_sensor
|
|
317
338
|
return self._dhw_sensor
|
|
318
339
|
|
|
319
340
|
@property
|
|
320
|
-
def hotwater_valve(self) -> BdrSwitch: # self._dhw_valve
|
|
341
|
+
def hotwater_valve(self) -> BdrSwitch | None: # self._dhw_valve
|
|
321
342
|
return self._dhw_valve
|
|
322
343
|
|
|
323
344
|
@property
|
|
324
|
-
def heating_valve(self) -> BdrSwitch: # self._htg_valve
|
|
345
|
+
def heating_valve(self) -> BdrSwitch | None: # self._htg_valve
|
|
325
346
|
return self._htg_valve
|
|
326
347
|
|
|
327
348
|
@property
|
|
@@ -329,44 +350,50 @@ class DhwZone(ZoneSchedule, ZoneBase): # CS92A # TODO: add Schedule
|
|
|
329
350
|
return "Stored HW"
|
|
330
351
|
|
|
331
352
|
@property
|
|
332
|
-
def config(self) ->
|
|
333
|
-
return self._msg_value(Code._10A0)
|
|
353
|
+
def config(self) -> dict[str, Any] | None: # 10A0
|
|
354
|
+
return self._msg_value(Code._10A0) # type: ignore[return-value]
|
|
334
355
|
|
|
335
356
|
@property
|
|
336
|
-
def mode(self) ->
|
|
337
|
-
return self._msg_value(Code._1F41)
|
|
357
|
+
def mode(self) -> dict[str, Any] | None: # 1F41
|
|
358
|
+
return self._msg_value(Code._1F41) # type: ignore[return-value]
|
|
338
359
|
|
|
339
360
|
@property
|
|
340
|
-
def setpoint(self) ->
|
|
341
|
-
return self._msg_value(Code._10A0, key=SZ_SETPOINT)
|
|
361
|
+
def setpoint(self) -> float | None: # 10A0
|
|
362
|
+
return self._msg_value(Code._10A0, key=SZ_SETPOINT) # type: ignore[return-value]
|
|
342
363
|
|
|
343
|
-
@setpoint.setter
|
|
344
|
-
def setpoint(self, value) -> None: # 10A0
|
|
364
|
+
@setpoint.setter # TODO: can value be None?
|
|
365
|
+
def setpoint(self, value: float) -> None: # 10A0
|
|
345
366
|
self.set_config(setpoint=value)
|
|
346
367
|
|
|
347
368
|
@property
|
|
348
|
-
def temperature(self) ->
|
|
349
|
-
return self._msg_value(Code._1260, key=SZ_TEMPERATURE)
|
|
369
|
+
def temperature(self) -> float | None: # 1260
|
|
370
|
+
return self._msg_value(Code._1260, key=SZ_TEMPERATURE) # type: ignore[return-value]
|
|
350
371
|
|
|
351
372
|
@property
|
|
352
|
-
def heat_demand(self) ->
|
|
353
|
-
return self._msg_value(Code._3150, key=SZ_HEAT_DEMAND)
|
|
373
|
+
def heat_demand(self) -> float | None: # 3150
|
|
374
|
+
return self._msg_value(Code._3150, key=SZ_HEAT_DEMAND) # type: ignore[return-value]
|
|
354
375
|
|
|
355
376
|
@property
|
|
356
|
-
def relay_demand(self) ->
|
|
357
|
-
return self._msg_value(Code._0008, key=SZ_RELAY_DEMAND)
|
|
377
|
+
def relay_demand(self) -> float | None: # 0008
|
|
378
|
+
return self._msg_value(Code._0008, key=SZ_RELAY_DEMAND) # type: ignore[return-value]
|
|
358
379
|
|
|
359
380
|
@property # only seen with FC, but seems should pair with 0008?
|
|
360
|
-
def relay_failsafe(self) ->
|
|
361
|
-
return self._msg_value(Code._0009, key=SZ_RELAY_FAILSAFE)
|
|
381
|
+
def relay_failsafe(self) -> float | None: # 0009
|
|
382
|
+
return self._msg_value(Code._0009, key=SZ_RELAY_FAILSAFE) # type: ignore[return-value]
|
|
362
383
|
|
|
363
|
-
def set_mode(
|
|
384
|
+
def set_mode(
|
|
385
|
+
self,
|
|
386
|
+
*,
|
|
387
|
+
mode: int | str | None = None,
|
|
388
|
+
active: bool | None = None,
|
|
389
|
+
until: dt | str | None = None,
|
|
390
|
+
) -> asyncio.Task[Packet]:
|
|
364
391
|
"""Set the DHW mode (mode, active, until)."""
|
|
365
|
-
return self._send_cmd(
|
|
366
|
-
Command.set_dhw_mode(self.ctl.id, mode=mode, active=active, until=until)
|
|
367
|
-
)
|
|
368
392
|
|
|
369
|
-
|
|
393
|
+
cmd = Command.set_dhw_mode(self.ctl.id, mode=mode, active=active, until=until)
|
|
394
|
+
return self._gwy.send_cmd(cmd, priority=Priority.HIGH, wait_for_reply=True)
|
|
395
|
+
|
|
396
|
+
def set_boost_mode(self) -> asyncio.Task[Packet]:
|
|
370
397
|
"""Enable DHW for an hour, despite any schedule."""
|
|
371
398
|
return self.set_mode(
|
|
372
399
|
mode=ZON_MODE_MAP.TEMPORARY,
|
|
@@ -374,12 +401,19 @@ class DhwZone(ZoneSchedule, ZoneBase): # CS92A # TODO: add Schedule
|
|
|
374
401
|
until=dt.now() + td(hours=1),
|
|
375
402
|
)
|
|
376
403
|
|
|
377
|
-
def reset_mode(self) ->
|
|
404
|
+
def reset_mode(self) -> asyncio.Task[Packet]: # 1F41
|
|
378
405
|
"""Revert the DHW to following its schedule."""
|
|
379
406
|
return self.set_mode(mode=ZON_MODE_MAP.FOLLOW)
|
|
380
407
|
|
|
381
|
-
def set_config(
|
|
408
|
+
def set_config(
|
|
409
|
+
self,
|
|
410
|
+
*,
|
|
411
|
+
setpoint: float | None = None,
|
|
412
|
+
overrun: int | None = None,
|
|
413
|
+
differential: float | None = None,
|
|
414
|
+
) -> asyncio.Task[Packet]:
|
|
382
415
|
"""Set the DHW parameters (setpoint, overrun, differential)."""
|
|
416
|
+
|
|
383
417
|
# dhw_params = self._msg_value(Code._10A0)
|
|
384
418
|
# if setpoint is None:
|
|
385
419
|
# setpoint = dhw_params[SZ_SETPOINT]
|
|
@@ -388,16 +422,15 @@ class DhwZone(ZoneSchedule, ZoneBase): # CS92A # TODO: add Schedule
|
|
|
388
422
|
# if differential is None:
|
|
389
423
|
# setpoint = dhw_params["differential"]
|
|
390
424
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
differential=differential,
|
|
397
|
-
)
|
|
425
|
+
cmd = Command.set_dhw_params(
|
|
426
|
+
self.ctl.id,
|
|
427
|
+
setpoint=setpoint,
|
|
428
|
+
overrun=overrun,
|
|
429
|
+
differential=differential,
|
|
398
430
|
)
|
|
431
|
+
return self._gwy.send_cmd(cmd, priority=Priority.HIGH)
|
|
399
432
|
|
|
400
|
-
def reset_config(self) ->
|
|
433
|
+
def reset_config(self) -> asyncio.Task[Packet]: # 10A0
|
|
401
434
|
"""Reset the DHW parameters to their default values."""
|
|
402
435
|
return self.set_config(setpoint=50, overrun=5, differential=1)
|
|
403
436
|
|
|
@@ -421,13 +454,13 @@ class DhwZone(ZoneSchedule, ZoneBase): # CS92A # TODO: add Schedule
|
|
|
421
454
|
return {a: getattr(self, a) for a in (SZ_TEMPERATURE, SZ_HEAT_DEMAND)}
|
|
422
455
|
|
|
423
456
|
|
|
424
|
-
class Zone(ZoneSchedule
|
|
457
|
+
class Zone(ZoneSchedule):
|
|
425
458
|
"""The Zone class for all zone types (but not DHW)."""
|
|
426
459
|
|
|
427
|
-
_SLUG: str = None
|
|
460
|
+
_SLUG: str = None
|
|
428
461
|
_ROLE_ACTUATORS: str = DEV_ROLE_MAP.ACT
|
|
429
462
|
|
|
430
|
-
def __init__(self, tcs, zone_idx: str) -> None:
|
|
463
|
+
def __init__(self, tcs: _MultiZoneT, zone_idx: str) -> None:
|
|
431
464
|
"""Create a heating zone.
|
|
432
465
|
|
|
433
466
|
The type of zone may not be known at instantiation. Even when it is known, zones
|
|
@@ -445,11 +478,11 @@ class Zone(ZoneSchedule, ZoneBase):
|
|
|
445
478
|
|
|
446
479
|
super().__init__(tcs, zone_idx)
|
|
447
480
|
|
|
448
|
-
self._sensor
|
|
449
|
-
self.actuators = []
|
|
450
|
-
self.actuator_by_id = {}
|
|
481
|
+
self._sensor: Device | None = None
|
|
482
|
+
self.actuators: list[Device] = []
|
|
483
|
+
self.actuator_by_id: dict[DeviceIdT, Device] = {}
|
|
451
484
|
|
|
452
|
-
def _update_schema(self,
|
|
485
|
+
def _update_schema(self, **schema: Any) -> None:
|
|
453
486
|
"""Update a heating zone with new schema attrs.
|
|
454
487
|
|
|
455
488
|
Raise an exception if the new schema is not a superset of the existing schema.
|
|
@@ -473,9 +506,9 @@ class Zone(ZoneSchedule, ZoneBase):
|
|
|
473
506
|
if klass == self._SLUG:
|
|
474
507
|
return
|
|
475
508
|
|
|
476
|
-
if klass ==
|
|
509
|
+
if klass == ZoneRole.VAL and self._SLUG not in (
|
|
477
510
|
None,
|
|
478
|
-
|
|
511
|
+
ZoneRole.ELE,
|
|
479
512
|
):
|
|
480
513
|
raise ValueError(f"Not a compatible zone class for {self}: {zone_type}")
|
|
481
514
|
|
|
@@ -483,15 +516,16 @@ class Zone(ZoneSchedule, ZoneBase):
|
|
|
483
516
|
raise ValueError(f"Not a known zone class (for {self}): {zone_type}")
|
|
484
517
|
|
|
485
518
|
if self._SLUG is not None:
|
|
486
|
-
raise
|
|
519
|
+
raise exc.SystemSchemaInconsistent(
|
|
487
520
|
f"{self} changed zone class: from {self._SLUG} to {klass}"
|
|
488
521
|
)
|
|
489
522
|
|
|
490
523
|
self.__class__ = ZONE_CLASS_BY_SLUG[klass]
|
|
491
524
|
_LOGGER.debug("Promoted a Zone: %s (%s)", self.id, self.__class__)
|
|
492
525
|
|
|
493
|
-
|
|
494
|
-
|
|
526
|
+
self._setup_discovery_cmds()
|
|
527
|
+
|
|
528
|
+
dev_id: DeviceIdT
|
|
495
529
|
|
|
496
530
|
# if schema.get(SZ_CLASS) == ZON_ROLE_MAP[ZON_ROLE.ACT]:
|
|
497
531
|
# schema.pop(SZ_CLASS)
|
|
@@ -510,11 +544,10 @@ class Zone(ZoneSchedule, ZoneBase):
|
|
|
510
544
|
# super()._setup_discovery_cmds()
|
|
511
545
|
|
|
512
546
|
for dev_role in (self._ROLE_ACTUATORS, DEV_ROLE_MAP.SEN):
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
60 * 60 * 24,
|
|
516
|
-
delay=0.5,
|
|
547
|
+
cmd = Command.from_attrs(
|
|
548
|
+
RQ, self.ctl.id, Code._000C, f"{self.idx}{dev_role}"
|
|
517
549
|
)
|
|
550
|
+
self._add_discovery_cmd(cmd, 60 * 60 * 24, delay=0.5)
|
|
518
551
|
|
|
519
552
|
self._add_discovery_cmd(
|
|
520
553
|
Command.get_zone_config(self.ctl.id, self.idx), 60 * 60 * 6, delay=30
|
|
@@ -534,26 +567,35 @@ class Zone(ZoneSchedule, ZoneBase):
|
|
|
534
567
|
) # longer dt as low yield (factory duration is 30 min): prefer eavesdropping
|
|
535
568
|
|
|
536
569
|
def _add_discovery_cmd(
|
|
537
|
-
self,
|
|
538
|
-
|
|
539
|
-
|
|
570
|
+
self,
|
|
571
|
+
cmd: Command,
|
|
572
|
+
interval: float,
|
|
573
|
+
*,
|
|
574
|
+
delay: float = 0,
|
|
575
|
+
timeout: float | None = None,
|
|
576
|
+
) -> None:
|
|
577
|
+
"""Schedule a command to run periodically.
|
|
578
|
+
|
|
579
|
+
Both `timeout` and `delay` are in seconds.
|
|
580
|
+
"""
|
|
540
581
|
super()._add_discovery_cmd(cmd, interval, delay=delay, timeout=timeout)
|
|
541
582
|
|
|
542
583
|
if cmd.code != Code._000C: # or cmd._ctx == f"{self.idx}{ZON_ROLE_MAP.SEN}":
|
|
543
584
|
return
|
|
544
585
|
|
|
545
586
|
if [t for t in self._discovery_cmds if t[-2:] in ZON_ROLE_MAP.HEAT_ZONES] and (
|
|
546
|
-
self._discovery_cmds.pop(f"{self.idx}{ZON_ROLE_MAP.ACT}",
|
|
587
|
+
self._discovery_cmds.pop(f"{self.idx}{ZON_ROLE_MAP.ACT}", [])
|
|
547
588
|
):
|
|
548
589
|
_LOGGER.warning(f"cmd({cmd}): inferior header removed from discovery")
|
|
549
590
|
|
|
550
|
-
if
|
|
551
|
-
self._discovery_cmds
|
|
591
|
+
if (
|
|
592
|
+
self._discovery_cmds.get(f"{self.idx}{ZON_ROLE_MAP.VAL}")
|
|
593
|
+
and (self._discovery_cmds[f"{self.idx}{ZON_ROLE_MAP.ELE}"])
|
|
552
594
|
):
|
|
553
595
|
_LOGGER.warning(f"cmd({cmd}): inferior header removed from discovery")
|
|
554
596
|
|
|
555
597
|
def _handle_msg(self, msg: Message) -> None:
|
|
556
|
-
def eavesdrop_zone_type(this, *, prev=None) -> None:
|
|
598
|
+
def eavesdrop_zone_type(this: Message, *, prev: Message | None = None) -> None:
|
|
557
599
|
"""TODO.
|
|
558
600
|
|
|
559
601
|
There are three ways to determine the type of a zone:
|
|
@@ -565,30 +607,30 @@ class Zone(ZoneSchedule, ZoneBase):
|
|
|
565
607
|
if this.code in (Code._0008, Code._0009):
|
|
566
608
|
assert self._SLUG in (
|
|
567
609
|
None,
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
610
|
+
ZoneRole.ELE,
|
|
611
|
+
ZoneRole.VAL,
|
|
612
|
+
ZoneRole.MIX,
|
|
571
613
|
), self._SLUG
|
|
572
614
|
|
|
573
615
|
if self._SLUG is None:
|
|
574
616
|
# this might eventually be: ZON_ROLE.VAL
|
|
575
|
-
self._update_schema(**{SZ_CLASS: ZON_ROLE_MAP[
|
|
617
|
+
self._update_schema(**{SZ_CLASS: ZON_ROLE_MAP[ZoneRole.ELE]})
|
|
576
618
|
|
|
577
619
|
elif this.code == Code._3150: # TODO: and this.verb in (I_, RP)?
|
|
578
620
|
# MIX/ELE don't 3150
|
|
579
621
|
assert self._SLUG in (
|
|
580
622
|
None,
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
623
|
+
ZoneRole.RAD,
|
|
624
|
+
ZoneRole.UFH,
|
|
625
|
+
ZoneRole.VAL,
|
|
584
626
|
), self._SLUG
|
|
585
627
|
|
|
586
628
|
if isinstance(this.src, TrvActuator):
|
|
587
|
-
self._update_schema(**{SZ_CLASS: ZON_ROLE_MAP[
|
|
629
|
+
self._update_schema(**{SZ_CLASS: ZON_ROLE_MAP[ZoneRole.RAD]})
|
|
588
630
|
elif isinstance(this.src, BdrSwitch):
|
|
589
|
-
self._update_schema(**{SZ_CLASS: ZON_ROLE_MAP[
|
|
631
|
+
self._update_schema(**{SZ_CLASS: ZON_ROLE_MAP[ZoneRole.VAL]})
|
|
590
632
|
elif isinstance(this.src, UfhController):
|
|
591
|
-
self._update_schema(**{SZ_CLASS: ZON_ROLE_MAP[
|
|
633
|
+
self._update_schema(**{SZ_CLASS: ZON_ROLE_MAP[ZoneRole.UFH]})
|
|
592
634
|
|
|
593
635
|
assert (
|
|
594
636
|
msg.src is self.ctl or msg.src.type == DEV_TYPE_MAP.UFC
|
|
@@ -625,64 +667,76 @@ class Zone(ZoneSchedule, ZoneBase):
|
|
|
625
667
|
)
|
|
626
668
|
|
|
627
669
|
# TODO: testing this concept, hoping to learn device_id of UFC
|
|
628
|
-
if msg.payload[SZ_ZONE_TYPE] == DEV_ROLE_MAP.UFH:
|
|
629
|
-
|
|
670
|
+
# if msg.payload[SZ_ZONE_TYPE] == DEV_ROLE_MAP.UFH:
|
|
671
|
+
# cmd = Command.from_attrs(
|
|
672
|
+
# RQ, self.ctl.id, Code._000C, f"{self.idx}{DEV_ROLE_MAP.UFH}"
|
|
673
|
+
# )
|
|
674
|
+
# self._send_cmd(cmd)
|
|
630
675
|
|
|
631
676
|
# If zone still doesn't have a zone class, maybe eavesdrop?
|
|
632
677
|
if self._gwy.config.enable_eavesdrop and self._SLUG in (
|
|
633
678
|
None,
|
|
634
|
-
|
|
679
|
+
ZoneRole.ELE,
|
|
635
680
|
):
|
|
636
681
|
eavesdrop_zone_type(msg)
|
|
637
682
|
|
|
638
|
-
def _msg_value(self, *args, **kwargs):
|
|
683
|
+
def _msg_value(self, *args: Any, **kwargs: Any) -> Any:
|
|
639
684
|
return super()._msg_value(*args, **kwargs, zone_idx=self.idx)
|
|
640
685
|
|
|
641
686
|
@property
|
|
642
|
-
def sensor(self) ->
|
|
687
|
+
def sensor(self) -> Device | None:
|
|
643
688
|
return self._sensor
|
|
644
689
|
|
|
645
690
|
@property
|
|
646
|
-
def heating_type(self) ->
|
|
647
|
-
|
|
648
|
-
|
|
691
|
+
def heating_type(self) -> str | None:
|
|
692
|
+
"""Return the type of the zone/DHW (e.g. electric_zone, stored_dhw)."""
|
|
693
|
+
|
|
694
|
+
if self._SLUG is None: # isinstance(self, ???)
|
|
695
|
+
return None
|
|
696
|
+
return ZON_ROLE_MAP[self._SLUG] # type: ignore[no-any-return]
|
|
649
697
|
|
|
650
698
|
@property
|
|
651
|
-
def name(self) ->
|
|
699
|
+
def name(self) -> str | None: # 0004
|
|
652
700
|
"""Return the name of the zone."""
|
|
653
|
-
|
|
701
|
+
|
|
702
|
+
if self._gwy._zzz:
|
|
703
|
+
msgs = self._gwy._zzz.get(code=Code._0004, src=self._z_id, ctx=self._z_idx)
|
|
704
|
+
return msgs[0].payload.get(SZ_NAME) if msgs else None
|
|
705
|
+
|
|
706
|
+
return self._msg_value(Code._0004, key=SZ_NAME) # type: ignore[no-any-return]
|
|
654
707
|
|
|
655
708
|
@name.setter
|
|
656
|
-
def name(self, value) -> None:
|
|
657
|
-
"
|
|
658
|
-
self._send_cmd(Command.set_zone_name(self.ctl.id, self.idx, value))
|
|
709
|
+
def name(self, value: str) -> None:
|
|
710
|
+
raise NotImplementedError("The setter has been deprecated, use: .set_name()")
|
|
659
711
|
|
|
660
712
|
@property
|
|
661
|
-
def config(self) ->
|
|
662
|
-
return self._msg_value(Code._000A)
|
|
713
|
+
def config(self) -> dict[str, Any] | None: # 000A
|
|
714
|
+
return self._msg_value(Code._000A) # type: ignore[no-any-return]
|
|
663
715
|
|
|
664
716
|
@property
|
|
665
|
-
def mode(self) ->
|
|
666
|
-
return self._msg_value(Code._2349)
|
|
717
|
+
def mode(self) -> dict[str, Any] | None: # 2349
|
|
718
|
+
return self._msg_value(Code._2349) # type: ignore[no-any-return]
|
|
667
719
|
|
|
668
720
|
@property
|
|
669
|
-
def setpoint(self) ->
|
|
670
|
-
return self._msg_value((Code._2309, Code._2349), key=SZ_SETPOINT)
|
|
721
|
+
def setpoint(self) -> float | None: # 2309 (2349 is a superset of 2309)
|
|
722
|
+
return self._msg_value((Code._2309, Code._2349), key=SZ_SETPOINT) # type: ignore[no-any-return]
|
|
671
723
|
|
|
672
|
-
@setpoint.setter
|
|
673
|
-
def setpoint(self, value) -> None: # 000A/2309
|
|
724
|
+
@setpoint.setter # TODO: can value be None?
|
|
725
|
+
def setpoint(self, value: float) -> None: # 000A/2309
|
|
674
726
|
"""Set the target temperature, until the next scheduled setpoint."""
|
|
727
|
+
|
|
675
728
|
if value is None:
|
|
676
|
-
self.reset_mode()
|
|
677
|
-
|
|
678
|
-
|
|
729
|
+
return self.reset_mode()
|
|
730
|
+
|
|
731
|
+
cmd = Command.set_zone_setpoint(self.ctl.id, self.idx, value)
|
|
732
|
+
self._gwy.send_cmd(cmd, priority=Priority.HIGH)
|
|
679
733
|
|
|
680
734
|
@property
|
|
681
|
-
def temperature(self) ->
|
|
682
|
-
return self._msg_value(Code._30C9, key=SZ_TEMPERATURE)
|
|
735
|
+
def temperature(self) -> float | None: # 30C9
|
|
736
|
+
return self._msg_value(Code._30C9, key=SZ_TEMPERATURE) # type: ignore[no-any-return]
|
|
683
737
|
|
|
684
738
|
@property
|
|
685
|
-
def heat_demand(self) ->
|
|
739
|
+
def heat_demand(self) -> float | None: # 3150
|
|
686
740
|
"""Return the zone's heat demand, estimated from its devices' heat demand."""
|
|
687
741
|
demands = [
|
|
688
742
|
d.heat_demand
|
|
@@ -692,28 +746,29 @@ class Zone(ZoneSchedule, ZoneBase):
|
|
|
692
746
|
return _transform(max(demands + [0])) if demands else None
|
|
693
747
|
|
|
694
748
|
@property
|
|
695
|
-
def window_open(self) ->
|
|
749
|
+
def window_open(self) -> bool | None: # 12B0
|
|
696
750
|
"""Return an estimate of the zone's current window_open state."""
|
|
697
|
-
return self._msg_value(Code._12B0, key=SZ_WINDOW_OPEN)
|
|
751
|
+
return self._msg_value(Code._12B0, key=SZ_WINDOW_OPEN) # type: ignore[no-any-return]
|
|
698
752
|
|
|
699
|
-
def _get_temp(self) ->
|
|
753
|
+
def _get_temp(self) -> asyncio.Task[Packet] | None:
|
|
700
754
|
"""Get the zone's latest temp from the Controller."""
|
|
701
755
|
return self._send_cmd(Command.get_zone_temp(self.ctl.id, self.idx))
|
|
702
756
|
|
|
703
|
-
def reset_config(self) ->
|
|
757
|
+
def reset_config(self) -> asyncio.Task[Packet]: # 000A
|
|
704
758
|
"""Reset the zone's parameters to their default values."""
|
|
705
759
|
return self.set_config()
|
|
706
760
|
|
|
707
761
|
def set_config(
|
|
708
762
|
self,
|
|
709
763
|
*,
|
|
710
|
-
min_temp=5,
|
|
711
|
-
max_temp=35,
|
|
764
|
+
min_temp: float = 5,
|
|
765
|
+
max_temp: float = 35,
|
|
712
766
|
local_override: bool = False,
|
|
713
767
|
openwindow_function: bool = False,
|
|
714
768
|
multiroom_mode: bool = False,
|
|
715
|
-
) ->
|
|
769
|
+
) -> asyncio.Task[Packet]:
|
|
716
770
|
"""Set the zone's parameters (min_temp, max_temp, etc.)."""
|
|
771
|
+
|
|
717
772
|
cmd = Command.set_zone_config(
|
|
718
773
|
self.ctl.id,
|
|
719
774
|
self.idx,
|
|
@@ -723,29 +778,41 @@ class Zone(ZoneSchedule, ZoneBase):
|
|
|
723
778
|
openwindow_function=openwindow_function,
|
|
724
779
|
multiroom_mode=multiroom_mode,
|
|
725
780
|
)
|
|
726
|
-
return self.
|
|
781
|
+
return self._gwy.send_cmd(cmd, priority=Priority.HIGH)
|
|
727
782
|
|
|
728
|
-
def reset_mode(self) ->
|
|
783
|
+
def reset_mode(self) -> asyncio.Task[Packet]: # 2349
|
|
729
784
|
"""Revert the zone to following its schedule."""
|
|
730
785
|
return self.set_mode(mode=ZON_MODE_MAP.FOLLOW)
|
|
731
786
|
|
|
732
|
-
def set_frost_mode(self) ->
|
|
787
|
+
def set_frost_mode(self) -> asyncio.Task[Packet]: # 2349
|
|
733
788
|
"""Set the zone to the lowest possible setpoint, indefinitely."""
|
|
734
789
|
return self.set_mode(mode=ZON_MODE_MAP.PERMANENT, setpoint=5) # TODO
|
|
735
790
|
|
|
736
|
-
def set_mode(
|
|
791
|
+
def set_mode(
|
|
792
|
+
self,
|
|
793
|
+
*,
|
|
794
|
+
mode: str | None = None,
|
|
795
|
+
setpoint: float | None = None,
|
|
796
|
+
until: dt | str | None = None,
|
|
797
|
+
) -> asyncio.Task[Packet]: # 2309/2349
|
|
737
798
|
"""Override the zone's setpoint for a specified duration, or indefinitely."""
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
else:
|
|
799
|
+
|
|
800
|
+
if mode is not None or until is not None: # Hometronics doesn't support 2349
|
|
741
801
|
cmd = Command.set_zone_mode(
|
|
742
802
|
self.ctl.id, self.idx, mode=mode, setpoint=setpoint, until=until
|
|
743
803
|
)
|
|
744
|
-
|
|
804
|
+
elif setpoint is not None: # unsure if Hometronics supports setpoint of None
|
|
805
|
+
cmd = Command.set_zone_setpoint(self.ctl.id, self.idx, setpoint)
|
|
806
|
+
else:
|
|
807
|
+
raise ValueError("Invalid mode/setpoint")
|
|
808
|
+
|
|
809
|
+
return self._gwy.send_cmd(cmd, priority=Priority.HIGH)
|
|
745
810
|
|
|
746
|
-
def set_name(self, name) ->
|
|
811
|
+
def set_name(self, name: str) -> asyncio.Task[Packet]:
|
|
747
812
|
"""Set the zone's name."""
|
|
748
|
-
|
|
813
|
+
|
|
814
|
+
cmd = Command.set_zone_name(self.ctl.id, self.idx, name)
|
|
815
|
+
return self._gwy.send_cmd(cmd, priority=Priority.HIGH)
|
|
749
816
|
|
|
750
817
|
@property
|
|
751
818
|
def schema(self) -> dict[str, Any]:
|
|
@@ -776,7 +843,7 @@ class EleZone(Zone): # BDR91A/T # TODO: 0008/0009/3150
|
|
|
776
843
|
|
|
777
844
|
# def __init__(self,... # NOTE: since zones are promotable, we can't use this here
|
|
778
845
|
|
|
779
|
-
_SLUG: str =
|
|
846
|
+
_SLUG: str = ZoneRole.ELE
|
|
780
847
|
_ROLE_ACTUATORS: str = DEV_ROLE_MAP.ELE
|
|
781
848
|
|
|
782
849
|
def _handle_msg(self, msg: Message) -> None:
|
|
@@ -790,15 +857,13 @@ class EleZone(Zone): # BDR91A/T # TODO: 0008/0009/3150
|
|
|
790
857
|
raise TypeError("WHAT 2")
|
|
791
858
|
|
|
792
859
|
@property
|
|
793
|
-
def heat_demand(self) ->
|
|
860
|
+
def heat_demand(self) -> float | None:
|
|
794
861
|
"""Return 0 as the zone's heat demand, as electric zones don't call for heat."""
|
|
795
862
|
return 0
|
|
796
863
|
|
|
797
864
|
@property
|
|
798
|
-
def relay_demand(self) ->
|
|
799
|
-
|
|
800
|
-
# return self._msgs[Code._0008].payload[SZ_RELAY_DEMAND]
|
|
801
|
-
return self._msg_value(Code._0008, key=SZ_RELAY_DEMAND)
|
|
865
|
+
def relay_demand(self) -> float | None: # 0008 (NOTE: CTLs won't RP|0008)
|
|
866
|
+
return self._msg_value(Code._0008, key=SZ_RELAY_DEMAND) # type: ignore[no-any-return]
|
|
802
867
|
|
|
803
868
|
@property
|
|
804
869
|
def status(self) -> dict[str, Any]:
|
|
@@ -816,7 +881,7 @@ class MixZone(Zone): # HM80 # TODO: 0008/0009/3150
|
|
|
816
881
|
|
|
817
882
|
# def __init__(self,... # NOTE: since zones are promotable, we can't use this here
|
|
818
883
|
|
|
819
|
-
_SLUG: str =
|
|
884
|
+
_SLUG: str = ZoneRole.MIX
|
|
820
885
|
_ROLE_ACTUATORS: str = DEV_ROLE_MAP.MIX
|
|
821
886
|
|
|
822
887
|
def _setup_discovery_cmds(self) -> None:
|
|
@@ -827,8 +892,8 @@ class MixZone(Zone): # HM80 # TODO: 0008/0009/3150
|
|
|
827
892
|
)
|
|
828
893
|
|
|
829
894
|
@property
|
|
830
|
-
def mix_config(self) ->
|
|
831
|
-
return self._msg_value(Code._1030)
|
|
895
|
+
def mix_config(self) -> PayDictT._1030:
|
|
896
|
+
return self._msg_value(Code._1030) # type: ignore[no-any-return]
|
|
832
897
|
|
|
833
898
|
@property
|
|
834
899
|
def params(self) -> dict[str, Any]:
|
|
@@ -843,7 +908,7 @@ class RadZone(Zone): # HR92/HR80
|
|
|
843
908
|
|
|
844
909
|
# def __init__(self,... # NOTE: since zones are promotable, we can't use this here
|
|
845
910
|
|
|
846
|
-
_SLUG: str =
|
|
911
|
+
_SLUG: str = ZoneRole.RAD
|
|
847
912
|
_ROLE_ACTUATORS: str = DEV_ROLE_MAP.RAD
|
|
848
913
|
|
|
849
914
|
|
|
@@ -852,11 +917,11 @@ class UfhZone(Zone): # HCC80/HCE80 # TODO: needs checking
|
|
|
852
917
|
|
|
853
918
|
# def __init__(self,... # NOTE: since zones are promotable, we can't use this here
|
|
854
919
|
|
|
855
|
-
_SLUG: str =
|
|
920
|
+
_SLUG: str = ZoneRole.UFH
|
|
856
921
|
_ROLE_ACTUATORS: str = DEV_ROLE_MAP.UFH
|
|
857
922
|
|
|
858
923
|
@property
|
|
859
|
-
def heat_demand(self) ->
|
|
924
|
+
def heat_demand(self) -> float | None: # 3150
|
|
860
925
|
"""Return the zone's heat demand, estimated from its devices' heat demand."""
|
|
861
926
|
if (demand := self._msg_value(Code._3150, key=SZ_HEAT_DEMAND)) is not None:
|
|
862
927
|
return _transform(demand)
|
|
@@ -868,11 +933,11 @@ class ValZone(EleZone): # BDR91A/T
|
|
|
868
933
|
|
|
869
934
|
# def __init__(self,... # NOTE: since zones are promotable, we can't use this here
|
|
870
935
|
|
|
871
|
-
_SLUG: str =
|
|
936
|
+
_SLUG: str = ZoneRole.VAL
|
|
872
937
|
_ROLE_ACTUATORS: str = DEV_ROLE_MAP.VAL
|
|
873
938
|
|
|
874
939
|
@property
|
|
875
|
-
def heat_demand(self) ->
|
|
940
|
+
def heat_demand(self) -> float | None: # 0008 (NOTE: not 3150)
|
|
876
941
|
"""Return the zone's heat demand, using relay demand as a proxy."""
|
|
877
942
|
return self.relay_demand
|
|
878
943
|
|
|
@@ -887,10 +952,19 @@ def _transform(valve_pos: float) -> float:
|
|
|
887
952
|
return math.floor((valve_pos - t1) * t1 / (t2 - t1) + t0 + 0.5) / 100
|
|
888
953
|
|
|
889
954
|
|
|
890
|
-
|
|
955
|
+
# e.g. {"RAD": RadZone}
|
|
956
|
+
ZONE_CLASS_BY_SLUG: dict[str, type[DhwZone] | type[Zone]] = class_by_attr(
|
|
957
|
+
__name__, "_SLUG"
|
|
958
|
+
)
|
|
891
959
|
|
|
892
960
|
|
|
893
|
-
def zone_factory(
|
|
961
|
+
def zone_factory(
|
|
962
|
+
tcs: _StoredHwT | _MultiZoneT,
|
|
963
|
+
idx: str,
|
|
964
|
+
*,
|
|
965
|
+
msg: Message | None = None,
|
|
966
|
+
**schema: Any,
|
|
967
|
+
) -> DhwZone | Zone:
|
|
894
968
|
"""Return the zone class for a given zone_idx/klass (Zone or DhwZone).
|
|
895
969
|
|
|
896
970
|
Some zones are promotable to a compatible sub class (e.g. ELE->VAL).
|
|
@@ -900,15 +974,15 @@ def zone_factory(tcs, idx: str, *, msg: Message = None, **schema) -> _ZoneT:
|
|
|
900
974
|
ctl_addr: Address,
|
|
901
975
|
idx: str,
|
|
902
976
|
*,
|
|
903
|
-
msg: Message = None,
|
|
977
|
+
msg: Message | None = None,
|
|
904
978
|
eavesdrop: bool = False,
|
|
905
|
-
**schema,
|
|
906
|
-
) -> type[
|
|
979
|
+
**schema: Any,
|
|
980
|
+
) -> type[DhwZone] | type[Zone]:
|
|
907
981
|
"""Return the initial zone class for a given zone_idx/klass (Zone or DhwZone)."""
|
|
908
982
|
|
|
909
983
|
# NOTE: for now, zones are always promoted after instantiation
|
|
910
984
|
|
|
911
|
-
# # a specified zone class always takes
|
|
985
|
+
# # a specified zone class always takes precedence (even if it is wrong)...
|
|
912
986
|
# if cls := ZONE_CLASS_BY_SLUG.get(schema.get(SZ_CLASS)):
|
|
913
987
|
# _LOGGER.debug(
|
|
914
988
|
# f"Using an explicitly-defined zone class for: {ctl_addr}_{idx} ({cls})"
|
|
@@ -937,10 +1011,16 @@ def zone_factory(tcs, idx: str, *, msg: Message = None, **schema) -> _ZoneT:
|
|
|
937
1011
|
)
|
|
938
1012
|
return Zone
|
|
939
1013
|
|
|
940
|
-
|
|
1014
|
+
zon: DhwZone | Zone = best_zon_class( # type: ignore[type-var]
|
|
941
1015
|
tcs.ctl.addr,
|
|
942
1016
|
idx,
|
|
943
1017
|
msg=msg,
|
|
944
1018
|
eavesdrop=tcs._gwy.config.enable_eavesdrop,
|
|
945
1019
|
**schema,
|
|
946
1020
|
).create_from_schema(tcs, idx, **schema)
|
|
1021
|
+
|
|
1022
|
+
# assert isinstance(zon, DhwZone | Zone) # mypy
|
|
1023
|
+
return zon
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
_ZoneT = TypeVar("_ZoneT", bound="ZoneBase")
|