Add support for Yeelink PTC Bath Heater v5/v6 (yeelink.bhf_light) with opt-in confirmed readback - #2935
Open
Zuz666 wants to merge 19 commits into
Open
Add support for Yeelink PTC Bath Heater v5/v6 (yeelink.bhf_light) with opt-in confirmed readback#2935Zuz666 wants to merge 19 commits into
Zuz666 wants to merge 19 commits into
Conversation
… heater v5/v6 Single codec table in templates.py drives both decode (physical gear -> logical Low/Auto/High) and encode (logical -> mode-specific physical gear) for climate fan_level and stays the only copy used by v5/v6 mappings. - heat_mode/cold_mode/vent_mode value-lists gain Auto (Off/Low/Auto/High) - fan_level value-list gains Auto (Low/Auto/High) - decode renders none for Idle, composite and out-of-domain gear codes instead of collapsing to Low - v5 heating/blow/ventilation templates match exact bh_mode tokens (fastwarm/warmwind_extra no longer report as active channels) - MiotPropConv.decode keeps None as None instead of expanding it into the full value-list
…mate In composite warmwind|venting / coolwind|venting states the climate entity unconditionally hides fan_mode, publishes diagnostic effective_fan_mode=high and rejects set_fan_mode without a device write. Idle and out-of-domain gear codes decoded as none also publish unavailable instead of a stale mode.
- value-lists and Off/Low/Auto/High option order for paired selects - singleton encode/decode for all Low/Auto/High combinations - exact climate mode default payloads (High) - composite/idle/unknown codes decode to none, never Off/Low - v5 official bool decoding from exact bh_mode tokens - v5/v6 switch converter discovery - climate composite contract: unavailable fan_mode, effective_fan_mode=high, set_fan_mode rejected without write
…er codes Spec 4.2: a singleton channel reporting a gear outside the mode-specific domain now publishes unavailable fan_mode together with the raw_fan_gear attribute instead of silently dropping the physical code. Valid, idle and composite states never carry the diagnostic attribute. The domain check reuses YEELINK_BHF_FAN_GEAR_MODES from the shared codec, no table copy.
…ccess Miio2MiotHelper.async_set_property/async_call_action returned code 1 for non-['ok'] device replies, but MIoT code 1 means 'operation not completed' and MiotResult.is_success treats it as success, so non-optimistic writes got a false ACK. Emit code -1 with an error detail instead. Regression tests drive the real async_set_properties path with async_send -> ['error'].
Write-initiated refreshes bypass the HA debouncer via async_refresh(), so a burst of writes (e.g. brightness slider) meant a full get_prop per step. _async_update now enforces a trailing-edge cooldown: the poll still runs, never sooner than update_interval after the previous one (spec 11.2). Also documents the non_optimistic all-coordinators fallback as a deliberate guard for foreign YAML customizes.
…annel Idle/drying states have no active warm/cold/vent token; the codec fallback would send set_bh_mode(['bh_off'|'drying', gear]) whose device behavior is undefined. set_fan_mode now raises HomeAssistantError without a device write, same as the composite contract.
…l interval Binding the write-initiated confirmed refresh cooldown to update_interval (30s for v5/v6) delayed post-write confirmation of fan mode, switches and selects by up to 30 seconds. The limiter now enforces only a short trailing-edge gap (3s, MIN_DEVICE_POLL_GAP_SECONDS): the confirmed poll still runs after every write, bursts coalesce, and the scheduled cadence stays at interval_seconds.
Zuz666
marked this pull request as ready for review
August 30, 2026 19:03
Contributor
Author
|
@al-one, thanks for your work on hass-xiaomi-miot! Friendly ping for a review when you have time. :) The PR adds Yeelink PTC bath heater v5/v6 support:
tests/test_yeelink_bhf_light.py: 166 passed. The description covers the design, verification and the live-device test run. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
MIoT spec links: https://home.miot-spec.com/spec/yeelink.bhf_light.v5 · https://home.miot-spec.com/spec/yeelink.bhf_light.v6
Summary
Adds hardware-verified support for Yeelink PTC Bath Heater v6 (
yeelink.bhf_light.v6)and fixes mode/gear decoding for v5 (closes #2528), plus an opt-in "non-optimistic
confirmed readback" mode that eliminates optimistic state flips after writes.
Notes on the spec / hardware
fan_speed_idx = [Warm][Air][Vent]; digits are mode-specificand gated by the corresponding
bh_modetoken (Warm: 1=Low/2=High; Air, Vent: 1=Low/3=High).Inactive digits can be stale (e.g.
131after switching modes) — naive decoding readsghost channels.
set_bh_mode ['windoff']stops heating/blow only;['ventingoff']stops ventilation only.{warmwind,coolwind,venting}_gear) returnprop_erron real hardware —they are computed in memory, never polled. Synthetic selects/switches: IID 114–116.
Changes
Yeelink v5/v6 —
climate.py,miio2miot_specs.py,templates.py,miot_specs_extend.jsondecimal gear packing with mode-token gating.
bh_modeis unread (no fallback codec guessing).Core: non-optimistic confirmed readback —
device.py,coordinator.pynon_optimistic: true): writes propagate errors instead of optimisticdispatch; state is confirmed by a fresh poll.
non_optimisticdevices.Tests —
tests/test_yeelink_bhf_light.py(+166 instances), official MIOT spec fixture.Note
Polls of one physical device (chunk coordinators, cloud statistics, write-initiated
refreshes) are now mutually exclusive for all models, not only
non_optimisticones.There is no artificial delay for legacy devices — a poll only ever waits for another
in-flight poll of the same device; this prevents interleaved requests on a single
miio transport.
Validation
Tested on my own Home Assistant instance (HA 2026.8.3, local/LAN mode) with both
physical v5 and v6 units:
targeted_off, stop_working, ha_setter, mixed), verified return to idle; live checks on v5.
tests/suite (166 in the new file),on HA stable and the minimum supported 2025.6.0.