Skip to content

Add ZHA controller for Zigbee IR blasters, plus ACiQ mini-split codes (9000) - #1595

Closed
defl wants to merge 1 commit into
smartHomeHub:masterfrom
defl:zha-ts1201-aciq
Closed

defl wants to merge 1 commit into
smartHomeHub:masterfrom
defl:zha-ts1201-aciq

Conversation

@defl

@defl defl commented Sep 6, 2026

Copy link
Copy Markdown

What this adds

A ZhaController, so Zigbee IR blasters reached through ZHA can be used as SmartIR emitters. Today they cannot be used at all — there is no controller for them.

Tested end to end on a Tuya TS1201 (_TZ3290_j37rooaxrcdcqo5n, the ZS06 / UFO-R11 family) driving an ACiQ 12K console mini-split, on Home Assistant 2026.8.3.

This builds on the approach in #1178 (thanks @anmar), with three fixes:

  • That PR's check_encoding validates against ESPHOME_COMMANDS_ENCODING and raises "not supported by the ESPHome controller" — a copy-paste slip. This adds a real ZHA_COMMANDS_ENCODING = [ENC_BASE64, ENC_RAW].
  • controller_data errors now name every missing key at once, and invalid JSON is reported as such rather than raising ValueError from inside json.loads.
  • service_data is copied before params is added, so the caller's parsed config is not mutated.

The one non-obvious detail

The command string is passed to params.code verbatim. The blaster's ZHA quirk builds its own {"key_num":1,...,"key_code":…} envelope; wrapping the code here nests one envelope inside the other, and the blaster then transmits nothing at all — no error, no log line, no LED, no IR. It looks exactly like a dead device. That cost me an afternoon, so it is called out in the class docstring.

codes/climate/9000.json

ACiQ 9K/12K console mini-split (remote RG10R(M2S)/BGEFU1, a Midea OEM; the same handset ships on Blueridge XS2A). The 9000 band was unused, and #1178's own docs example uses 9000 for ZHA.

The frames are synthesised from the protocol, not captured one at a time: the generator reproduces all 27 codes captured from the physical remote bit-for-bit, which is what makes 600 entries practical to stand behind. Every lookup path SmartIR can request resolves.

Four properties of this device that explain the file's shape, all measured rather than assumed:

  • Keys are Fahrenheit. Several Fahrenheit steps collapse onto one Celsius code on the wire — 73 °F and 74 °F are both 23 °C — so 600 lookup paths resolve to 174 distinct frames.
  • The wire carries 17–30 °C only. The remote displays down to 60 °F but transmits 17 °C for 60, 61 and 62, so the bottom of its published range is cosmetic.
  • AUTO and DRY lock the fan, so every fan key in those modes maps to one frame.
  • FAN_ONLY carries no temperature, so every temperature key maps to one frame.

Swing is deliberately absent: on this unit swing and turbo are separate command frames rather than part of the state frame, so folding them into the command path would misrepresent the protocol.

Notes

Docs updated in README.md and CLIMATE.md. Happy to split the device file into its own PR if you would rather review the controller alone.

Zigbee IR blasters reached through ZHA had no controller, so a Tuya TS1201
(ZS06 / UFO-R11 family) could not be used as a SmartIR emitter at all.

ZhaController calls zha.issue_zigbee_cluster_command with the code placed in
params.code. The blaster's ZHA quirk builds its own {"key_num":1,...} envelope,
so the code is passed through verbatim; wrapping it here would nest one envelope
inside the other and the blaster then transmits nothing at all -- no error, no
LED, no IR. That failure is silent, hence the comment in the class docstring.

ZHA_COMMANDS_ENCODING accepts Base64 and Raw. Tuya blasters carry FastLZ
compressed microsecond durations as base64, which is neither converted nor
inspected on the way through.

codes/climate/9000.json covers the ACiQ 9K/12K console mini-split (remote
RG10R(M2S)/BGEFU1, a Midea OEM). The frames are synthesised from the protocol
rather than captured one by one, and the generator reproduces all 27 codes
captured from the physical remote bit-for-bit.

Notes on that device file, all measured rather than assumed:

- Keys are Fahrenheit because several Fahrenheit steps collapse onto one
  Celsius code on the wire (73F and 74F are both 23C), so 600 lookup paths
  resolve to 174 distinct frames.
- The wire carries 17-30C only. The remote displays down to 60F but transmits
  17C for 60, 61 and 62, so the bottom of its range is cosmetic.
- AUTO and DRY lock the fan on this unit, so every fan key in those modes maps
  to the same frame.
- FAN_ONLY carries no temperature, so every temperature key maps to one frame.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@defl

defl commented Sep 9, 2026

Copy link
Copy Markdown
Author

Withdrawing this — we ended up going a different way, and it seems fairer to close it than to leave it sitting in the queue.

The device this was written for (an ACiQ mini-split, Midea 0xB2 protocol) turns out to have a state space that is a pure function of mode, temperature and fan. A complete SmartIR device file for it came to 334 KB and 600 entries, which were 174 distinct values of that function. That is no criticism of the lookup-table design — it is exactly right for the captured-code devices SmartIR is built around, and it is why the project has such a good library. It just made a poor fit for a device whose protocol we had fully derived.

We also hit two things the command-path model could not express for this unit: swing and turbo are separate command frames rather than fields inside the state frame, and a press is three frames rather than one, with the trailer carrying the exact fan percentage (1–100), a half-degree bit, and the display-unit flag. Omitting that trailer makes the unit revert to a Celsius display, which took us a while to work out.

So it now lives as a small standalone integration instead: https://github.com/defl/zha-midea-ir — installable through HACS as a custom repository, for ACiQ / Blueridge / Midea units behind a Tuya TS1201 (ZS06 / UFO-R11) blaster on ZHA. It computes each frame at send time rather than shipping a table.

The ZHA controller itself is still worth having here, and shouldn't be lost with this PR. #1178 by @anmar does essentially the same thing and predates this by well over a year — anyone wanting Zigbee IR blasters in SmartIR should look there first. Two small notes from implementing it, in case they help whoever picks it up:

  • check_encoding in that PR validates against ESPHOME_COMMANDS_ENCODING and raises "not supported by the ESPHome controller" — a copy-paste slip. A ZHA controller wants its own list; Tuya blasters carry Base64.
  • The code string must reach params.code verbatim. The blaster's ZHA quirk builds its own {"key_num":1,…,"key_code":…} envelope, so pre-wrapping nests one envelope inside the other and the blaster then transmits nothing at all — no error, no log line, no LED. It looks exactly like dead hardware.

Thanks for SmartIR — the device-file library was genuinely useful for working out what shape our own data needed to be.

@defl defl closed this Sep 9, 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.

1 participant