Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 13 additions & 15 deletions custom_components/myhome/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -923,19 +923,17 @@ async def async_update(self) -> None:
and not self._fan
and not wants_humidity
):
# SCS-only gateways (F450, F453, F453AV, F454, F459, H4684, MH200, MH200N, MH202 in the
# firmware oracle; F455 and MH201 by field reports) expand the dimensionless zone status
# request *#4*Z## into four bus queries, dimension 0, 12, 14 and 13 ($0499ZZ3000 / 12 /
# 0E / 0D in trace-corpus.tsv). Against a 99-zone central unit every query is answered
# (F454 + #0, issue_629 log: *#4*1##..*#4*15## complete in ~1.3 s each with all five
# frames); a standalone thermostat answers only some of them, and the command session
# then ends with a NACK after 1.5-2 s (MH202 ACKs). The 10 s socket hang of #674 was an
# F455, not in the oracle, on actuator-only channels (*#4*Z*19*0*0##) where nothing
# answered. MyHomeServer1 replies to *#4*Z## from its cache (#649); F460 and F461 expand
# it into one request ($00499013000) and answer it (#714): those three keep the poll.
# Where the SCS-only gateway also drops the general request *#4*0##, the zone asks for
# its temperature below (_polls_temperature); everywhere else it waits for the sweep
# replies or for the thermostat's own reports.
# SCS-only gateways without multi-session *#4*Z## verification (F450, F453, F453AV, F459,
# H4684, MH200, MH200N in the firmware oracle; F455 and MH201 by field reports) expand
# the dimensionless zone status request *#4*Z## into four bus queries, dimension 0, 12, 14 and
# 13 ($0499ZZ3000 / 12 / 0E / 0D in trace-corpus.tsv). The 10 s socket hang of #674 was an F455,
# not in the oracle, on actuator-only channels (*#4*Z*19*0*0##) where nothing answered. F454
# (up to 4 command sessions) answers *#4*Z## in ~1.3 s and MH202 in ~160-250 ms with all five
# frames on both central units (#629) and standalone thermostats (#866); MyHomeServer1 replies
# from its cache (#649); F460 and F461 expand it into one request ($00499013000) and answer it
# (#714): those five keep the poll. Where an SCS-only gateway also drops the general request
# *#4*0##, the zone asks for its temperature below (_polls_temperature); everywhere else it waits
# for the sweep replies or for the thermostat's own reports.
return
if self._poll_health.should_skip(time.time()):
LOGGER.debug("%s %s did not answer its last polls; not asking again yet", self._gateway_handler.log_id, self._display_name)
Expand Down Expand Up @@ -963,8 +961,8 @@ async def async_update(self) -> None:
# is the bus TX timeout of an unpopulated zone (the QEMU images have no thermostat
# wired; the live F461 NACKs the unpopulated zone 1 the same way after 2.07 s, rows
# 374-376), not a verdict on a populated one. Until a command-session capture of
# *#4*Z*0## on an SCS-only gateway (F454 / MH200) exists, the result is left uncounted
# rather than risk marking a healthy zone unresponsive on a NACK.
# *#4*Z*0## on a single-session SCS-only gateway (MH200 / H4684) exists, the result is
# left uncounted rather than risk marking a healthy zone unresponsive on a NACK.
await self._gateway_handler.send_status_request(OWNHeatingCommand.get_temperature(self._full_where))
if self._fan and self._season_has_fan_coil():
await self._gateway_handler.send_status_request(
Expand Down
25 changes: 14 additions & 11 deletions custom_components/myhome/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,15 +211,16 @@ def command_session_default(model: str | None) -> int:
) # *#16*0*5## (sound-who16-source.tsv)
# Zones of these SCS-only gateways report nothing after the startup sweep, so each zone asks
# for its temperature (*#4*Z*0##, one bus query answered in ~120 ms on a populated zone): the
# models refusing *#4*0## whose *#4*Z## is expanded into four bus queries ($0499ZZ3000 / 12 /
# 0E / 0D in trace-corpus.tsv, ~1.3 s per zone when a central unit answers them all, a NACK
# after 1.5-2 s when a standalone thermostat answers only some), and the MH201, whose
# standalone thermostats stayed unknown after every restart (#185; its *#4*0## refusal is the
# issue_466 sweep trace, the MH201 is not in the oracle). F460, F461 and MyHomeServer1 refuse
# *#4*0## too but answer *#4*Z## themselves (F460 and F461 expand it into one request,
# $00499013000 in trace-corpus.tsv; MyHomeServer1 replies from its cache, #649), so their zones
# keep the zone status poll and are not listed here.
ZONE_TEMPERATURE_POLL = frozenset({"F450", "F454", "F459", "H4684", "MH200", "MH201", "MH202"})
# single-session or uncaptured models refusing *#4*0## whose *#4*Z## is expanded into four bus
# queries ($0499ZZ3000 / 12 / 0E / 0D in trace-corpus.tsv), and the MH201, whose standalone
# thermostats stayed unknown after every restart (#185; its *#4*0## refusal is the issue_466
# sweep trace, the MH201 is not in the oracle). F454, MH202, F460, F461 and MyHomeServer1 refuse
# *#4*0## too but answer *#4*Z## themselves (F454 supports up to 4 command sessions and answers
# all five frames in ~1.3 s on both central-unit #629 and standalone #866 plants; MH202 answers
# all five frames in ~160-250 ms, #866; F460 and F461 expand it into $00499013000 in trace-corpus.tsv;
# MyHomeServer1 replies from its cache, #649), so their zones keep the zone status poll and are not
# listed here.
ZONE_TEMPERATURE_POLL = frozenset({"F450", "F459", "H4684", "MH200", "MH201"})
# Zones behind an F422 bus interface (WHERE Z#4#II): zero of the twelve emulated gateways in
# own-firmware-oracle (findings/thermo-zone-requests.md) emit $06EC / $06E4 interface-routed SCS
# frames for WHO 4. Gateways either NACK/drop Z#4#II queries, truncate Z#4#II at # and cross-poll
Expand Down Expand Up @@ -507,6 +508,10 @@ def firmware(self) -> str | None:
@property
def supports_zone_status(self) -> bool:
"""Whether the gateway supports point-to-point zone status queries (*#4*Z##)."""
profile = getattr(getattr(self, "gateway", None), "profile", None)
supported = getattr(profile, "supports_heating_zone_status", None)
if isinstance(supported, bool):
return supported
# Check the configured model string, its alphanumeric form (so "BTicino H/L4684" matches
# "HL4684"), and the profile model _profile_for_model resolves it to (L4684, NT4684,
# H/L4684, HL4684, HC4684, and MyHOME_Suite catalogue codes such as 003598).
Expand All @@ -522,10 +527,8 @@ def supports_zone_status(self) -> bool:
"F450",
"F455",
"HL4684", # "BTicino H/L4684" resolves to no profile and "H4684" is not a substring of it
"F454",
"MH200",
"MH201",
"MH202",
"F452",
"F453",
"F458",
Expand Down
14 changes: 12 additions & 2 deletions custom_components/myhome/gateway_sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1421,8 +1421,18 @@ async def sending_loop(self, worker_id: int) -> None:
else:
_resolve_written(task, written_at)
self.handler._record_tx(written_at, task["message"])
if collected and isinstance(collected, list):
for resp in collected:
responses_to_dispatch: Any = collected
if responses_to_dispatch is None and task["is_status_request"]:
# A standalone zone status request (*#4*Z##) that answers only some of
# bt_H4690's four bus queries ends with *#*0## on the command session;
# OWNd preserves the partial frames in OWNNackError.responses.
responses_to_dispatch = getattr(
getattr(_command_session, "last_error", None),
"responses",
None,
)
if responses_to_dispatch and isinstance(responses_to_dispatch, list):
for resp in responses_to_dispatch:
raw_resp = str(resp)
if self.bus_monitor.has_frame_since(
task_start, direction="rx", raw=raw_resp
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration/climate.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ MyHOME climate entities support standard Home Assistant climate controls:
* **Target Temperature**: Adjustable setpoint slider in 0.5 °C increments.
The slider shows the setpoint the thermostat regulates to (dimension 12, knob offset included) whenever the zone has reported it; the commanded setpoint (dimension 14) is only shown until then. On an F461 the two can differ (21.0 commanded, 23.5 in force on a live capture), and the attribute `local_target_temperature` carries the same value. When the zone leaves `off`, the value reported while it was off is taken as the protection setpoint (#383), so the commanded setpoint is shown until the zone reports dimension 12 again; after a `set_temperature` call the next report, dimension 12 or 14, is shown. The value restored after a restart of Home Assistant already carries the saved `local_offset`, so the first dimension 13 report does not add it again.
* **Current Temperature**: Real-time ambient reading reported by the probe.
On SCS-only gateways whose firmware drops the general request `*#4*0##` (F450, F454, F459, H4684, MH200, MH201, MH202), each local zone asks for its own temperature with `*#4*<WHERE>*0##` after the startup sweep, so a standalone thermostat is not `unknown` until it next reports (#185). F460, F461 and MyHomeServer1 drop `*#4*0##` as well but answer the zone status request `*#4*<WHERE>##` themselves (the F460 and F461 expand it into the four dimension requests, the MyHomeServer1 replies from its cache), so their local zones keep that poll. A zone behind an F422 bus interface (`WHERE = <zone>#4#<interface>`) is never polled unless the OWNd profile sets `supports_interface_zone_requests = True` (see below), so it shows its restored reading until it reports by itself. After a restart the last reading is shown with the attribute `current_temperature_restored: true` until the zone reports again.
On single-session or uncaptured SCS-only gateways whose firmware drops the general request `*#4*0##` (F450, F459, H4684, MH200, MH201), each local zone asks for its own temperature with `*#4*<WHERE>*0##` after the startup sweep, so a standalone thermostat is not `unknown` until it next reports (#185). F454, MH202, F460, F461 and MyHomeServer1 drop `*#4*0##` as well but answer the zone status request `*#4*<WHERE>##` themselves (the F454 supports up to 4 command sessions and answers in ~1.3 s on both central-unit #629 and standalone #866 plants, the MH202 answers in ~160-250 ms, #866, the F460 and F461 expand it into one request, and the MyHomeServer1 replies from its cache), so their local zones keep that poll. A zone behind an F422 bus interface (`WHERE = <zone>#4#<interface>`) is never polled unless the OWNd profile sets `supports_interface_zone_requests = True` (see below), so it shows its restored reading until it reports by itself. After a restart the last reading is shown with the attribute `current_temperature_restored: true` until the zone reports again.
* **Current Humidity**: Ambient relative humidity percentage for zones equipped with humidity sensors (e.g. Living Now thermostats or humidity probes).
* **HVAC Action and `actuators`**: a local zone asks for the state of its actuators (`*#4*<WHERE>*20##`, one `*#4*#0*20##` for every local zone on an F461 or a MyHomeServer1) once the startup sweep is over and again after the event session reconnects, so `hvac_action` (`heating` / `cooling` / `idle`) is known right after start instead of after the first actuator switch. The attribute `actuators` lists each actuator as `on` or `off` (`{"1": "off", "2": "off"}`) and stays while the zone is off (every actuator `off`). F453, F453AV, H4684, MH200, MH200N and MH202 drop the request (firmware oracle), so their zones are not asked and wait for the actuators' own frames; on a shared bus the gateway that serves WHO 4 (delegated secondary or warm standby) decides. Secondary probes (`WHERE >= 100`) are never asked. Probe-less `F430/2` / `F430/4` relays (such as bathroom towel warmers that only emit Dimension 20 actuator status frames without a temperature probe) do not create phantom `climate` entities and can be controlled as `switch` entities via **Add thermo actuator / towel-warmer switch (WHO 4)** (see [Switches & Relays](switches.md)).
* **Zones Behind an `#4#II` Interface (`F422`)**: across all twelve emulated gateway firmwares in `own-firmware-oracle` (`findings/thermo-zone-requests.md`), **zero** gateways emit `$06EC`/`$06E4` interface-routed SCS telegrams for `WHO = 4`: gateways either NACK/drop `#4#II` queries, truncate `Z#4#II` at `#` and cross-poll local zone `Z` on the main bus (`F450`, `F454`, `F459`, `F460`, `F461`, `H4684`, `MH200`, `MyHomeServer1`), or on `MH202` (`1.0.24`) misparse trailing `#4#01` as local zone 4 actuator 1 and crash `coso` (`SIGSEGV`) on `*#4*Z#4#01##`. Unless an `OWNd` gateway profile explicitly declares `supports_interface_zone_requests = True`, zones configured with an `#4#II` interface are passive: neither polls (`*#4*Z#4#II##`, `*0##`, `*11##`, `*19##`, `*20##`, `*60##`) nor writes that would strip `#4#II` onto local main-bus zone `Z` are sent, while incoming event-session broadcasts update state normally (note that 1-digit inline shorthand `3#4#1`..`3#4#9` is normalized as zone `3` behind interface `01`..`09`; wire frames always use two-digit `#4#01`..`#4#15`). Dimensionless and zone-dimension polls on actuator or split addresses (`Z#N`, `Z#0`, `0#N`, `3#Z#N`) are likewise never sent.
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration/runtime_behaviour.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ After the event session is up, the integration sends a small set of general stat
| :--- | :--- | :--- |
| `*#1*0##` | Lighting | All gateways |
| `*#2*0##` | Automation / covers | All gateways |
| `*#4*0##` | Thermoregulation | All gateways except F450, F454, F459, F460, F461, H4684, MH200, MH201, MH202 and MyHomeServer1, whose firmware drops it (firmware oracle; the MH201 from a physical sweep trace, `tests/fixtures/traces/issue_466`); F460, F461 and MyHomeServer1 poll `*#4*<WHERE>##` per zone instead, and the zones of the SCS-only models (F450, F454, F459, H4684, MH200, MH201, MH202) ask `*#4*<WHERE>*0##` |
| `*#4*0##` | Thermoregulation | All gateways except F450, F454, F459, F460, F461, H4684, MH200, MH201, MH202 and MyHomeServer1, whose firmware drops it (firmware oracle; the MH201 from a physical sweep trace, `tests/fixtures/traces/issue_466`); F454, MH202, F460, F461 and MyHomeServer1 poll `*#4*<WHERE>##` per zone instead (#629, #649, #714, #866), and the zones of the remaining SCS-only models (F450, F459, H4684, MH200, MH201) ask `*#4*<WHERE>*0##` |
| `*#5*0##` | Burglar alarm | MH202, H4890 |
| `*#16*0*5##` | Sound system (dimension 5; lists every amplifier) | Gateways whose profile advertises WHO 16, except F450, F459, F460, F461, MH202 and MyHomeServer1, whose firmware drops it (firmware oracle) |
| `*#18*51*51##` | Energy management (meter 51 total energy) | F454, F455, MH201, MH202, MyHomeServer1, H4890 |
Expand Down
4 changes: 2 additions & 2 deletions docs/getting-started/hardware-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ During startup initial discovery, status queries are gated by the gateway profil
<!-- GATEWAY_DISCOVERY_START -->
| Gateway Model | Profile Subsystems (advertised) | Startup Sweep Requests (sent) | Profile Support Notes |
| :--- | :--- | :--- | :--- |
| **MH202** | WHO 1, 2, 4, 5, 16, 18 | WHO 1, 2, 5, 18 (firmware drops `*#4*0##`, `*#16*0*5##`) | Burglar Alarm (`*#5*0##`) and Energy (`*#18*51*...`) queries. WHO 4 and WHO 16 are advertised, but the `010024` firmware drops `*#4*0##` and `*#16*0*5##` (own-firmware-oracle), so the sweep leaves both out and each climate zone asks `*#4*<WHERE>*0##` itself. |
| **MH202** | WHO 1, 2, 4, 5, 16, 18 | WHO 1, 2, 5, 18 (firmware drops `*#4*0##`, `*#16*0*5##`) | Burglar Alarm (`*#5*0##`) and Energy (`*#18*51*...`) queries. WHO 4 and WHO 16 are advertised, but the `010024` firmware drops `*#4*0##` and `*#16*0*5##` (own-firmware-oracle), so the sweep leaves both out; climate zones are polled with `*#4*<WHERE>##` (#866). |
| **H4890 / AM4890** | WHO 1, 2, 4, 5, 16, 18 | WHO 1, 2, 4, 5, 16, 18 | Supports Burglar Alarm, Sound (`*#16*0*5##`), and Energy meter 51 queries. |
| **F454** | WHO 1, 2, 4, 16, 18 | WHO 1, 2, 16, 18 (firmware drops `*#4*0##`) | Full lighting, cover, sound (`bt_difson` firmware-verified), and energy meter 51 queries. The `020051` firmware drops `*#4*0##` (own-firmware-oracle), so each climate zone asks `*#4*<WHERE>*0##` itself. Alarm query omitted. |
| **F454** | WHO 1, 2, 4, 16, 18 | WHO 1, 2, 16, 18 (firmware drops `*#4*0##`) | Full lighting, cover, sound (`bt_difson` firmware-verified), and energy meter 51 queries. The `020051` firmware drops `*#4*0##` (own-firmware-oracle), so the sweep leaves it out; zones are polled with `*#4*<WHERE>##` (#629, #866). Alarm query omitted. |
| **F452 / F453 / F453AV** | WHO 1, 2, 4, 16, 18 | WHO 1, 2, 4, 16, 18 | Lighting, cover, climate, sound (`bt_difson` firmware-verified on F453/F453AV), and energy meter 51 queries. Alarm query omitted. |
| **F461** | WHO 1, 2, 4, 18 | WHO 1, 2, 18 (firmware drops `*#4*0##`) | Lighting, cover, and energy meter 51 queries. Sound is WHO 22 only (no `bt_difson`; every WHO 16 frame refused, own-firmware-oracle `020011`, confirmed live on 2.0.11). The firmware also drops `*#4*0##`, so the sweep leaves it out; zones are polled with `*#4*<WHERE>##`. Alarm query omitted. |
| **F455** | WHO 1, 2, 4, 18 | WHO 1, 2, 4, 18 | Entry-level gateway: lighting, cover, climate, and energy meter 51 queries. Sound (WHO 16/22) and Alarm (WHO 5) omitted per installer manual. |
Expand Down
4 changes: 2 additions & 2 deletions scripts/update_gateway_profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@
"profile_name": "MH202",
"pinned_whos": [1, 2, 4, 5, 16, 18],
"sweep_omits": [GENERAL_ZONE_STATUS, SOUND_STATUS],
"notes": "Burglar Alarm (`*#5*0##`) and Energy (`*#18*51*...`) queries. WHO 4 and WHO 16 are advertised, but the `010024` firmware drops `*#4*0##` and `*#16*0*5##` (own-firmware-oracle), so the sweep leaves both out and each climate zone asks `*#4*<WHERE>*0##` itself.",
"notes": "Burglar Alarm (`*#5*0##`) and Energy (`*#18*51*...`) queries. WHO 4 and WHO 16 are advertised, but the `010024` firmware drops `*#4*0##` and `*#16*0*5##` (own-firmware-oracle), so the sweep leaves both out; climate zones are polled with `*#4*<WHERE>##` (#866).",
},
{
"model": "**H4890 / AM4890**",
Expand All @@ -216,7 +216,7 @@
"profile_name": "F454",
"pinned_whos": [1, 2, 4, 16, 18],
"sweep_omits": [GENERAL_ZONE_STATUS],
"notes": "Full lighting, cover, sound (`bt_difson` firmware-verified), and energy meter 51 queries. The `020051` firmware drops `*#4*0##` (own-firmware-oracle), so each climate zone asks `*#4*<WHERE>*0##` itself. Alarm query omitted.",
"notes": "Full lighting, cover, sound (`bt_difson` firmware-verified), and energy meter 51 queries. The `020051` firmware drops `*#4*0##` (own-firmware-oracle), so the sweep leaves it out; zones are polled with `*#4*<WHERE>##` (#629, #866). Alarm query omitted.",
},
{
"model": "**F452 / F453 / F453AV**",
Expand Down
Loading
Loading