Skip to content

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
al-one:masterfrom
Zuz666:feat/yeelink-bath-heater-v5-v6
Open

Add support for Yeelink PTC Bath Heater v5/v6 (yeelink.bhf_light) with opt-in confirmed readback#2935
Zuz666 wants to merge 19 commits into
al-one:masterfrom
Zuz666:feat/yeelink-bath-heater-v5-v6

Conversation

@Zuz666

@Zuz666 Zuz666 commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

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

  • v6 packs gears decimally: fan_speed_idx = [Warm][Air][Vent]; digits are mode-specific
    and gated by the corresponding bh_mode token (Warm: 1=Low/2=High; Air, Vent: 1=Low/3=High).
    Inactive digits can be stale (e.g. 131 after switching modes) — naive decoding reads
    ghost channels.
  • set_bh_mode ['windoff'] stops heating/blow only; ['ventingoff'] stops ventilation only.
  • Props 111–113 ({warmwind,coolwind,venting}_gear) return prop_err on real hardware —
    they are computed in memory, never polled. Synthetic selects/switches: IID 114–116.
  • v5 had inverted fan/ventilation modes and missing select properties (小米浴霸yeelink.bhf_light.v5 #2528).

Changes

Yeelink v5/v6climate.py, miio2miot_specs.py, templates.py, miot_specs_extend.json

  • Composite climate channels, mode switches, Auto fan mode via one shared mode-aware codec;
    decimal gear packing with mode-token gating.
  • Fan-mode writes are rejected while bh_mode is unread (no fallback codec guessing).

Core: non-optimistic confirmed readbackdevice.py, coordinator.py

  • Opt-in per device (non_optimistic: true): writes propagate errors instead of optimistic
    dispatch; state is confirmed by a fresh poll.
  • Write bursts coalesce into one trailing readback (generation counter).
  • A 3 s minimum poll gap applies only to non_optimistic devices.

Teststests/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_optimistic ones.
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:

  • 259/259 declarative matrix steps on v6 across 6 suites (single, companion,
    targeted_off, stop_working, ha_setter, mixed), verified return to idle; live checks on v5.
  • Automated: 216 test instances in the full tests/ suite (166 in the new file),
    on HA stable and the minimum supported 2025.6.0.

Zuz666 added 19 commits August 28, 2026 06:11
… 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
Zuz666 marked this pull request as ready for review August 30, 2026 19:03
@Zuz666

Zuz666 commented Aug 30, 2026

Copy link
Copy Markdown
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:

  • a mode-aware decimal gear codec with the composite fan-mode contract (climate)
  • synthetic bool switches for v6 (heating / blow / ventilation) decoded from the composite bh_mode; each switch setter issues a targeted set_bh_mode that toggles its channel without touching the others
  • confirmed readback for writes (non_optimistic): failed setters surface as errors, and the poll/limiter core was hardened accordingly (per-device poll serialization, short gap for confirmed-readback devices only, write bursts coalesced behind one trailing readback)

tests/test_yeelink_bhf_light.py: 166 passed. The description covers the design, verification and the live-device test run.

@Zuz666 Zuz666 changed the title ♨️ feat(yeelink): support Yeelink PTC Bath Heater v5/v6 with decimal gear codec & confirmed readback ♨️ feat(yeelink): support Yeelink PTC Bath Heater v5/v6 with decimal gear codec, virtual channel switches & confirmed readback Aug 30, 2026
@Zuz666 Zuz666 changed the title ♨️ feat(yeelink): support Yeelink PTC Bath Heater v5/v6 with decimal gear codec, virtual channel switches & confirmed readback Add support for Yeelink PTC Bath Heater v5/v6 (yeelink.bhf_light) with opt-in confirmed readback Aug 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

小米浴霸yeelink.bhf_light.v5

1 participant