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
2 changes: 1 addition & 1 deletion .github/release-notes/2.0.0b16.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ See the cover docs for the details and the known limits.
- **Expose Home Assistant to the bus**: a declarative `expose` engine projects HA entities and the clock onto free SCS addresses: virtual WHO 1 / WHO 2 actuators that a wall control can drive (mirroring HA state back to its LEDs), WHO 4 virtual temperature probes, WHO 18 virtual energy meters and the WHO 13 bus clock. Custom actions run as Home Assistant scripts (#750).
- **Covers**: `class:` in `myhome.yaml` now reaches WHO 2 covers (a `curtain` is no longer shown as a shutter; #39); Venetian blind slat tilt as timed slat pulses (`slat_tilt`, `slat_time`) (#508); impulse garage and gate covers on WHO 1 relays with optional safety approval, door-entry strike locks and doorbell events (#667); native broadcast for general covers behind an interface (#679); a cover stop overtakes other queued frames (#694); cover kinematics split into pluggable strategies (#739).
- **Lighting**: 2-digit Area 0 point addresses `01`–`09` (#713); ON dimmers on a fresh install are probed for brightness support (#742); timed turn-on uses the OWNd builders (#659); lights the startup sweep already reported are not polled again (#701); area and group re-syncs only skip once every member has echoed, counting only the members' own echoes in the commanded state (#740, #749).
- **Thermoregulation (WHO 4)**: each zone asks for its actuators (dimension 20) after the startup sweep and after a reconnect, so `hvac_action` is known at start; the new `actuators` attribute lists them (#25, #44); season frames, a generic setpoint mode and the central unit's season (#657, #725); holiday and vacation program attributes (#689); zone humidity remembered and queried at startup (#712); F461 zones polled, AUTO on standalone zones, fan coils per season, season kept across restarts (#714, #715); `hvac_action` derived from the season when the actuator carries no heating flag (#721); standalone default follows the plant (#698); 10 s command timeouts on the F455 removed (#675); migrated to the `native_*` temperature API (#665).
- **Thermoregulation (WHO 4)**: each zone asks for its actuators (dimension 20) after the startup sweep and after a reconnect, so `hvac_action` is known at start; the new `actuators` attribute lists them (#25, #44); zone humidity is not polled on F450, F453, F453AV, F454, H4684, MH200, MH200N and MH202, where the emulated firmware did not forward `*#4*<WHERE>*60##` (own-firmware-oracle `findings/thermo-zone-requests.md`, emulated firmware; an OWNd profile flag `supports_heating_humidity_request` overrides the table); season frames, a generic setpoint mode and the central unit's season (#657, #725); holiday and vacation program attributes (#689); zone humidity remembered and queried at startup (#712); F461 zones polled, AUTO on standalone zones, fan coils per season, season kept across restarts (#714, #715); `hvac_action` derived from the season when the actuator carries no heating flag (#721); standalone default follows the plant (#698); 10 s command timeouts on the F455 removed (#675); migrated to the `native_*` temperature API (#665).
- **Sound system**: tuner frequency, preset and RDS read from WHO 22, with optional per-frequency pictures (#695); sound source commands and wall transport steps (#651); amplifiers no longer woken by volume status reports (#670); one decision engine for the power state (#683); an idle decoder can be claimed from a parked group (#723).
- **Scenarios**: WHO 25 Scenario Plus commands and events (#671); WHO 17 scene events (#656).
- **Gateways**: H4684 / LGRH4684 and MH201 identification (#652, #722); kernel and distribution in the diagnostic sweep (#711); user commands go ahead of status polls (#685); one frame scheduler with per-target ordering (#738); the HA web session is used for discovery (#757).
Expand Down
17 changes: 15 additions & 2 deletions custom_components/myhome/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -558,11 +558,17 @@ async def async_update(self) -> None:
# bus captures confirm querying #0#1 times out on plants without a physical 4-zone unit (#629).
# They receive setpoints via commands (*#4*#0#1*#14*T*M##), broadcast events, or restored state.
return
# Humidity is asked only where the gateway forwards the request: the emulated firmware
# of F450, F453, F453AV, F454, H4684, MH200, MH200N and MH202 did not (own-firmware-oracle
# thermo-zone-requests finding, emulated firmware). Whether those gateways put the probe's
# humidity on the event session is not traced; any *#4*<WHERE>*60*<HH>## delivered there
# still updates current_humidity (handle_event) without a poll.
wants_humidity = self._humidity and getattr(self._gateway_handler, "humidity_request_refused", False) is not True
if (
not self._central
and not self._gateway_supports_zone_status
and not self._fan
and not self._humidity
and not wants_humidity
):
# Physical SCS gateways (F455, F454, MH200N, MH202, etc.) do not support
# dimension-less status requests (*#4*Z##) for regular zones (1-99). The request
Expand Down Expand Up @@ -590,10 +596,17 @@ async def async_update(self) -> None:
await self._gateway_handler.send_status_request(
cast(OWNCommand, OWNHeatingCommand.parse(f"*#4*{self._full_where}*11##"))
)
if self._humidity:
if wants_humidity:
await self._gateway_handler.send_status_request(
cast(OWNCommand, OWNHeatingCommand.parse(f"*#4*{self._full_where}*60##"))
)
elif self._humidity:
LOGGER.debug(
"%s Zone %s: this gateway refuses humidity requests; not asked (a *#4*%s*60*HH## event still updates it)",
self._gateway_handler.log_id,
self._full_where,
self._full_where,
)

@callback
def _poll_answered(self, written: asyncio.Future[float], frames_before: int) -> None:
Expand Down
79 changes: 74 additions & 5 deletions custom_components/myhome/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,51 @@ def command_session_default(model: str | None) -> int:
"F461": {20: "*#4*#0*20##"},
"MYHOMESERVER1": {20: "*#4*#0*20##"},
}
# The models the two tables know, longest first so ``MH200N`` is not read as ``MH200``.
_ZONE_REQUEST_MODELS = sorted(ACTUATOR_REQUEST_REFUSED | set(ALL_ZONE_REQUESTS), key=len, reverse=True)
# Gateways whose emulated firmware did not forward the zone humidity request (``*#4*Z*60##``:
# nack and nothing on the bus) in the own-firmware-oracle thermo-zone-requests suite
# (findings/thermo-zone-requests.md on main, ``firmware_observed``, emulated firmware); F459,
# F460, F461 and MyHomeServer1 forwarded it, and the live F461 / MyHomeServer1 answer it.
# Read from the OWNd profile first (``supports_heating_humidity_request``: ``True`` / ``False``
# wins, ``None`` = unverified); this table is the fallback until the flag exists there.
HUMIDITY_REQUEST_REFUSED = frozenset({"F450", "F453", "F453AV", "F454", "H4684", "MH200", "MH200N", "MH202"})

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. Merge conflict with #792 (2ece4744): Rebase onto v2-phase1-architecture and union _ZONE_REQUEST_MODELS across KNOWN_GATEWAY_MODELS | ACTUATOR_REQUEST_REFUSED | HUMIDITY_REQUEST_REFUSED | set(ALL_ZONE_REQUESTS) while keeping one shared _model_key and _who4_target().
  2. Oracle TSV publication: There are zero physical traces on these 8 models, and the machine-readable thermo-zone-requests.tsv rows + results/mcp_index.json (OpenWebNet-HA/own-firmware-oracle@d3767f49) are still on oracle/update-suites rather than main. Please get the suites update merged to own-firmware-oracle main so HUMIDITY_REQUEST_REFUSED is backed by published oracle TSVs rather than prose-only findings.

# Every gateway model the request tables can be keyed on: the twelve images the oracle emulates
# (results/<P>/<V>, refusing and forwarding alike) and the other models OWNd profiles, so a
# forwarding model named with a brand prefix ("BTicino F461") is keyed as itself (``F461``)
# and not left as the whole string. The same key on every table of #792 / #793 / #794.
KNOWN_GATEWAY_MODELS = frozenset(
{
"F450", "F452", "F452V", "F453", "F453AV", "F454", "F455", "F459", "F460", "F461",
"H4684", "H4890", "MH200", "MH200N", "MH201", "MH202", "MYHOMESERVER1",
}
)
# The models the tables know, longest first so ``MH200N`` is not read as ``MH200``.
_ZONE_REQUEST_MODELS = sorted(
KNOWN_GATEWAY_MODELS | ACTUATOR_REQUEST_REFUSED | HUMIDITY_REQUEST_REFUSED | set(ALL_ZONE_REQUESTS),
key=len,
reverse=True,
)
# SSDP announces the H4684 as "HL4684" (manifest.json), which OWNd does not alias to its H4684
# profile yet (it knows h4684, l4684, n4684, nt4684, lgrh4684; #794 maps the same spelling), and
# "HC4684" is the other prefixed spelling of the same product: keyed on the oracle's product name.
_MODEL_SPELLINGS = {"HL4684": "H4684", "HC4684": "H4684"}


def _alnum(name: object) -> str:
"""A model string reduced to its upper-case letters and digits (``"BTicino F461"`` -> ``BTICINOF461``)."""
return "".join(character for character in str(name).upper() if character.isalnum())


def _resolved_model(word: str) -> str | None:
"""The model OWNd resolves one word of a model string to, or ``None`` for a word it does not know.

``L4684`` -> ``H4684``, ``003598`` -> ``F454``, ``03565`` -> ``MH200N`` (OWNd's aliases and
catalogue item numbers); the spellings OWNd has no alias for are mapped first.
"""
key = _MODEL_SPELLINGS.get(_alnum(word), word)
profile = get_gateway_profile(key)
if isinstance(profile, GenericGatewayProfile):
return None
return _alnum(profile.model_name)


# A WHO 4 zone address: ``Z``, ``#Z``, ``Z#N`` (actuator), each optionally behind an
Expand Down Expand Up @@ -405,15 +448,24 @@ def _model_key(self) -> str:
"""The gateway model the request tables are keyed on (``F461``, ``MYHOMESERVER1``).

A known OWNd profile names the model; a ``GenericGatewayProfile`` (also what
``get_gateway_profile("BTicino F461")`` returns) falls back to the configured name,
in which a known model is looked up as a substring (``supports_zone_status`` style).
``get_gateway_profile("BTicino F461")`` returns) falls back to the configured name.
There a brand-prefixed alias or catalogue number is resolved word by word through
OWNd (``"BTicino L4684"`` -> ``H4684``, ``"Legrand 003598"`` -> ``F454``), the SSDP
spelling ``HL4684`` is read as ``H4684``, and otherwise a known model is looked up as
a substring, longest first (``supports_zone_status`` style), so ``MH200N`` stays
distinct from ``MH200``. The words are the alphanumeric runs of the string, so
``"BTicino-L4684"`` and ``"F454 (003598)"`` resolve too.
"""
profile = getattr(self.gateway, "profile", None)
if profile is not None and not isinstance(profile, GenericGatewayProfile):
name = getattr(profile, "model_name", None) or self.model
else:
name = self.model
normalized = "".join(character for character in str(name).upper() if character.isalnum())
for word in re.findall(r"[A-Za-z0-9]+", str(name)):
resolved = _resolved_model(word)
if resolved is not None:
return resolved
normalized = _MODEL_SPELLINGS.get(_alnum(name), _alnum(name))
return next((model for model in _ZONE_REQUEST_MODELS if model in normalized), normalized)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

manifest.json (line 25) advertises SSDP "modelName": "HL4684", for which get_gateway_profile("HL4684") (as well as vendor-prefixed strings like "BTicino L4684") returns GenericGatewayProfile. Because "H4684" is not a substring of "HL4684" or "BTICINOL4684" — and SSDP technical identities are never overwritten by resolve_gateway_identity — an SSDP-discovered HL4684 / BTicino L4684 misses HUMIDITY_REQUEST_REFUSED.

Please normalize HL4684 / L4684 / HC4684 (and _SAME_PRODUCT brand variants, keeping MH200N distinct from MH200) before the table lookup.


def _who4_target(self) -> MyHOMEGatewayHandler:
Expand Down Expand Up @@ -517,6 +569,23 @@ async def _send_zone_requests(self) -> None:
except Exception: # a transport error must not end the task silently (#593)
LOGGER.exception("%s Zone requests %s could not be sent", self.log_id, frames)

@property
def humidity_request_refused(self) -> bool:
"""Whether this gateway drops humidity requests (``*#4*Z*60##``) instead of forwarding them.

The request goes where ``send_status_request`` sends it: a secondary gateway WHO 4 is
delegated to, or the warm standby while this primary is down, decides with its own
profile and table. The OWNd profile flag wins when it exists; otherwise the
oracle-derived table.
"""
target = self._who4_target()
if target is not self:
return target.humidity_request_refused
supported = getattr(getattr(self.gateway, "profile", None), "supports_heating_humidity_request", None)
if isinstance(supported, bool):
return not supported
return self._model_key in HUMIDITY_REQUEST_REFUSED

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

humidity_request_refused inspects self directly, whereas send_status_request (gateway.py:672-680) dispatches WHO 4 requests through _delegated_target and _failover_target.

If a primary MH202 / F454 delegates WHO=4 to a secondary F461 / MyHomeServer1 (or fails over to warm standby F461), this returns True and suppresses humidity polling even though the request would be sent through F461. Conversely, if primary F461 fails over to standby MH202, this returns False and sends *#4*Z*60## into the refusing standby.

Please route this through the same _who4_target() helper as #792:

target = self._who4_target()
if target is not self:
    return target.humidity_request_refused

and add test coverage for both delegation and warm-standby failover directions.


@property
def profile(self) -> Any:
"""Return gateway profile."""
Expand Down
2 changes: 1 addition & 1 deletion docs/configuration/climate.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Climate zones equipped with integrated humidity sensors (such as BTicino Living

* **Dynamic Auto-Detection**: When a climate zone emits a humidity reading on the SCS bus, the integration dynamically enables humidity tracking on that entity.
* **State Restoration Across Restarts**: The zone remembers that it supports humidity across Home Assistant restarts and restores its last known humidity value immediately.
* **Targeted Startup Query**: After startup (once the initial discovery sweep finishes), only climate zones that support humidity are queried with `*#4*<WHERE>*60##` to refresh the current reading. This avoids unnecessary bus traffic on zones without humidity sensors, and works across both software gateways (MyHomeServer1) and physical SCS gateways (such as F461, F454, F455, MH200N).
* **Targeted Startup Query**: After startup (once the initial discovery sweep finishes), only climate zones that support humidity are queried with `*#4*<WHERE>*60##` to refresh the current reading. This avoids unnecessary bus traffic on zones without humidity sensors. A live F461 and a live MyHomeServer1 answer the request (F459 and F460 forward it in the firmware emulation); on F450, F453, F453AV, F454, H4684, MH200, MH200N and MH202 the emulated firmware did not forward it (own-firmware-oracle `findings/thermo-zone-requests.md`, a `firmware_observed` verdict), so until the OWNd gateway profile states `supports_heating_humidity_request` their zones are not queried. Whether those gateways put a probe's humidity on the event session has not been traced; any `*#4*<WHERE>*60*<HH>##` event the gateway delivers still updates `current_humidity` without polling.
* **Optional YAML Configuration**: In advanced setups using legacy `/config/myhome.yaml`, humidity polling can be explicitly pre-enabled on a zone by setting `humidity: true` under the zone configuration.

---
Expand Down
Loading
Loading