Skip to content

Releases: enapt/haismart-local

v0.71.2 — air conditioners can be turned off again

Choose a tag to compare

@enapt enapt released this 16 Sep 04:38

Fixes a regression introduced in v0.70.0. Anyone on v0.70.0 or v0.71.1 with an air conditioner should update.

What was wrong

Turning an air conditioner off from Home Assistant failed — both climate.turn_off and climate.set_hvac_mode with off — with:

<name> does not accept that setting: onOffStatus='0' not in allowed ['false', 'true']

The service call returned HTTP 500 and nothing reached the unit.

v0.70.0 added a decoder built from Haier's published byte map so that every appliance category works out of the box. Air conditioners already had a hand-built decoder and keep using it for reads. One gate on the write side asked a different question — "did the byte map decode this report" instead of "is this an air conditioner" — and an air conditioner's report decodes both ways. A change made of a single attribute was therefore sent down the byte-map write path, which expects the values the manufacturer's model publishes rather than the values the air-conditioner path uses.

Turning on still worked, because setting a mode also carries the operating mode, which most families do not publish a single-parameter write for, so the change fell back to the air-conditioner path. climate.turn_on on its own was refused in the same way.

This affected air conditioners broadly: 26 of the 28 air-conditioner families in the shipped catalogue publish a write id for power. On a commercial cabinet, which publishes far more, eight controls were affected — power, setpoint, mode, fan, both vanes, quiet and boost.

The fix

The write gate now uses the same condition the read side does, so the two cannot drift apart again. Air conditioners keep their own write path; every other appliance category keeps the byte-map one.

Verified on real hardware: reproduced before the fix, and after it the appliance acknowledges the command it had been refusing.

Also

Nine new tests cover the air-conditioner write path, including a commercial cabinet's, all confirmed to fail against the previous code.

Reported and diagnosed by @shaybpm.

v0.71.1 — the first confirmed write to a non-air-conditioner

Choose a tag to compare

@enapt enapt released this 14 Sep 02:04

The first confirmed command to something that is not an air conditioner

Documentation only — no behavioural change whatsoever. The only edits inside shipped modules
are two docstrings. Update at your leisure; the integration behaves exactly as v0.71.0 did.

v0.70.0 brought support for every appliance category Haier's app covers, and it shipped with an
honest caveat repeated in every document and in all five translated READMEs:

⚠️ No command has ever been sent to a non-air-conditioner appliance by this project.

That is no longer true. The reporter of
#13 — a heat-pump water heater in Taiwan —
changed their setpoint from Home Assistant and the appliance accepted it.

It was verified from their diagnostics rather than taken on report, on three independent legs:

  • the appliance said yes — reply frames 0x02 then 0x06, 0x02 being the accept half of
    Haier's own accept/refuse pair, so that is the board's verdict and not the integration's optimism;
  • the value reached the wire — status byte 93 reads 21, and 21 + 30 = 51 °C on the model's own
    offset;
  • Haier's cloud agrees — the device shadow captured in the same download matches our decode on
    26 of 26 attributes, making it 130 of 130 across their five captures.

What that does and does not establish

It establishes the mechanism: a single-parameter write built from Haier's published byte map,
for an appliance class nobody working on this project owns, is accepted by real hardware.

It does not establish the whole register, and the documentation now says so at every point. What
is confirmed is one setting — the setpoint (5D01) — on one unit of one class. That unit's
other three writable settings, every other category's writes, and group-command writes generally
remain published, not proven.

What changed in this release

  • The claim above is corrected wherever it appeared: README.md, DEVICES.md,
    docs/appliances.md (where the water heater moves up a tier), docs/FUTURE_WORK.md, a test
    docstring, and all five translated READMEs (Bahasa Indonesia, Bahasa Melayu, Thai, Tiếng
    Việt, Filipino).
  • A standing note that writeType: I was unconfirmed on hardware outside one air-conditioner class
    is retired — this write falsifies it.

Thanks to @e57575959, who filed five diagnostics downloads with one
variable changed each, did the byte diff themselves, and then went and tested the write.

v0.71.0 — one swing control per vane axis

Choose a tag to compare

@enapt enapt released this 13 Sep 11:45

⚠️ Breaking: swing is now one control per vane axis

swing_mode no longer takes both / vertical / horizontal. If you have an automation that
sets one of those, it needs updating.

Old New
swing_mode: off swing_mode: off and swing_horizontal_mode: off
swing_mode: vertical swing_mode: on + swing_horizontal_mode: off
swing_mode: horizontal swing_mode: off + swing_horizontal_mode: on
swing_mode: both swing_mode: on and swing_horizontal_mode: on

An automation sending an old value now fails loudly rather than moving a vane it did not mean to.
The horizontal axis uses the climate.set_swing_horizontal_mode service.

Why. Home Assistant's own guidance is explicit — "this should only be implemented if the
integration has independent control of vertical and horizontal swing"
— and these appliances have
exactly that: the two axes are separate fields on the wire. The four-way control is Home Assistant's
legacy shape, for integrations that can only move both axes together.

What it fixes, which is the real point. The field behind each axis is not a switch. It is the
manufacturer's position enum — 8 to 12 values — and sweeping is only one of them:

windDirectionVertical Haier's own description
0 上下摆位置固定 — fixed
1 / 3 健康气流(上吹) / (下吹) — health airflow, up / down
2, 4, 5, 6, 7 positions 1–5
8 上下摆自动 — the sweep
9 alternate sweep

A single four-way control had to command both axes on every use, so it could only ever leave
each one on auto or fixed. Using it flattened a health-airflow setting or an alternate sweep to
a plain one, and knocked the other vane off its stop — silently, with no way back from the climate
card, because only the vane selects reach those states.

That was not a corner case. Across 20 reporter diagnostics the vane pairs are (0,0) (2,0) (2,3) (2,4) (2,6) (4,3) (8,7) — both axes on intermediate stops in most of them — and prior art from
other projects shows the same, one axis sweeping while the other sits on a stop.

Now each control writes exactly one field, and an axis already doing what you asked is not written
at all. A unit is offered only the axes its family can actually move.

Fixed: air-quality sensors on newer Home Assistant

Home Assistant changed the micro sign in its own unit constant between releases — µg/m³ (U+00B5)
became μg/m³ (U+03BC). The two are indistinguishable on screen. Generated PM2.5 and VOC sensors
published the old spelling and would be rejected on Home Assistant 2026.x, losing their device
class. They now publish Home Assistant's own constant whatever release you are on.

ⓘ No air conditioner was affected in 0.70.0 — those sensors come from the curated path, which
already did this correctly.

Also

  • The README now lists what each appliance category actually reads and controls — water heaters,
    refrigerators, washing machines, gas water heaters, sterilising cabinets, cooker hoods, gas hobs,
    heat-pump water heaters, dishwashers, steam ovens and air purifiers — generated from the
    manufacturer's maps rather than written by hand.
  • The manufacturer's vane vocabulary is documented in docs/VENDOR_LABELS.md, which had no vane
    section at all.

v0.70.0 — every Haier appliance category

Choose a tag to compare

@enapt enapt released this 13 Sep 10:21

The integration is no longer air-conditioner-only

Every appliance category Haier publishes a byte map for is now supported — water heaters
(electric, gas and heat-pump), refrigerators, washing machines, dishwashers, cooker hoods, gas hobs,
sterilising cabinets, ovens, air purifiers and more. 165 product types across 36 device classes.

There is no per-appliance code behind this. Haier publishes, per product class, the word, bit and
width of every field in a status report along with its scaling, option labels and fault table. The
integration ships that map and reads reports at those positions; a class it does not carry is
fetched on demand, unauthenticated and MD5-verified. Your appliance's own declaration then
decides which of those fields become entities, so a model without a feature does not get a dead
control for it.

Each field becomes the right kind of entity — number, dropdown, switch, sensor, binary sensor —
carrying the manufacturer's own unit, range and labels. Water heaters additionally get a real
water_heater entity with their own temperature range and operating modes.

Air conditioners are unchanged

This is worth stating plainly: an air conditioner's class routes to the existing hand-built entity
set and the platform list it always had, and the air-conditioner decoder has no changes in this
release. Verified by A/B on real hardware — identical entity count, states and supported features
before and after — plus a live setpoint write confirmed by the unit's own report.

What is proven, and what is not

⚠️ No command has ever been sent to a non-air-conditioner appliance. Writes for those classes
use the same single-parameter mechanism proven on air conditioners, with the command ids Haier
publishes for each class — but published is not proven. If your appliance refuses a command, the
integration records the refusal and retires that control rather than going on offering it.

Evidence by category is tabulated in docs/appliances.md. In short: air
conditioners are confirmed on hardware; the heat-pump water heater from #13 is confirmed end to end
from a real unit's report, cross-checked against Haier's own cloud on every value across four
downloads; a washing machine's traffic decodes correctly from a third-party capture; every other
category is supported by construction and has never been seen on hardware here.

If you have one, please open an issue either way
— including one that just works. A diagnostics download is the whole contribution.

Deliberately withheld

  • Group-command writes — a water heater's reservation times, a washer's programme. No capture
    anywhere shows one being sent to a non-AC appliance, and unlike a single-parameter write a wrong
    layout would change several settings at once with nothing to report it. Read-only until verified.
  • Classes published only in Haier's older V2 text profile — 10 classes, no bridge between its
    Chinese label keys and the English attribute names the declaration gate uses.

The setup screens now name your appliance correctly

Every config-flow screen, error and repair notice said "air conditioner". All of them now say
"appliance" — in all 31 languages, not just English.

⛔ Not a find-and-replace: in the inflected languages the replacement noun usually has a different
gender, and everything agreeing with it has to change too. The fix was to pick a replacement of the
same gender as the word it replaces (Czech/Slovak jednotka, Polish sprzęt, Russian прибор,
Greek μηχάνημα), which keeps every article, adjective and past participle correct by construction.
Every {placeholder} and every piece of markdown was verified byte-identical afterwards.

This also caught a translation that had been stale for some time: the "status format is not
recognised" repair was reworded in English a while back, and all 30 locale files had been left
saying "air conditioner".

ⓘ The translations remain machine-written and not natively reviewed. Corrections welcome.

Also in this release

  • A number platform, and generic entities across sensor / switch / select / binary_sensor.
  • Platforms are forwarded per entry, so a water heater is not given a thermostat.
  • Fault names come from the appliance's own table rather than an air conditioner's — a washing
    machine's fault 22 is a door-lock failure, not "indoor PM2.5 sensor failure".
  • Entity metadata is validated against Home Assistant's own device-class and state-class tables.
  • Every generated entity carries Haier's own identifier and description as attributes
    (haier_attribute, haier_description), so anything named oddly is easy to report precisely.

Closes #13.

v0.69.1 — a sign-in that cannot reach Haier says so

Choose a tag to compare

@enapt enapt released this 03 Sep 04:08

A sign-in that cannot reach Haier now says so. When Home Assistant itself cannot reach Haier's servers — a DNS or firewall problem on the Home Assistant host, or an outage — the sign-in screen said "Signed in successfully, but…", which was not true and sent people off to re-check a password that was never the problem. It now says that Haier's servers could not be reached and points at the host's internet access and name resolution, in all thirty-one languages. The same wording is used when attaching an account to an existing unit or re-fetching a key runs into the same wall. A device list that fails after a real sign-in keeps its own "signed in, but" message.

Also in this release: the protocol notes and every guide were read against the code and brought up to date — the central cabinets' one-setting-at-a-time control path, the refusal frame and its reason codes, both forms of the telemetry query and how its reply is decoded, and the manufacturer's device configuration as the source the byte maps are checked against.

Verified: 727 tests; deployed and A/B-verified on two classic wall units (identical entities and features before and after, only live readings differing).

v0.69.0 — the thermostat says what the unit is doing

Choose a tag to compare

@enapt enapt released this 02 Sep 16:15

The thermostat now says what the unit is doing, not only what it is set to. A unit set to Cool reads cooling while its compressor runs and idle once the room has reached the setpoint and it stops; Dry and Heat behave the same way, fan-only reads fan, and off reads off. Home Assistant draws this on its own: the tile card badges the entity icon with it (a snowflake while cooling, a clock while idle), and the thermostat card and the more-info dialog print it under the temperature. No dashboard configuration is needed. It comes straight from the compressor state the integration already reads from the appliance's running-status report, so it can never disagree with the Compressor sensor.

Honest where it cannot know. A unit that does not report its compressor shows no action at all rather than echoing the mode — a "cooling" badge on an idle unit is exactly the wrong answer, and the mode already stands beside it. Auto on a heat-pump unit with the compressor running is left unknown too, because nothing in the report says which way it is pumping; auto on a cooling-only unit reads cooling.

Suggested by the owner of a 0d012 roof cabinet — thank you.

Verified: 726 tests; deployed and A/B-verified on two classic wall units (identical entities and features before and after, only live readings differing), where cooling, idle and off were each observed live.

v0.68.0 — the display unit and four-way cassette louvres on the central cabinets

Choose a tag to compare

@enapt enapt released this 01 Sep 13:45

The central cabinets gain their display unit and four-way-cassette louvres — from Haier's own device map. A 0d012 cabinet that declares a Celsius/Fahrenheit display setting now gets a control for it, and a four-way cassette that declares its four independent louvres now gets a select for each, with every stop the model lists — fixed, five positions, auto, and the health-airflow sweep. The write commands and the positions they read back from are the manufacturer's own, taken from the device configuration that describes the wire byte-for-byte, so nothing here is guessed.

Offered only where the unit itself declares them, never as a phantom. The manufacturer's byte-map carries far more settings than any real cabinet exposes; a control appears only where a unit's own model declares the attribute — the same rule the vendor app uses — so a basic cabinet is not cluttered with switches for hardware it lacks. The louvres are offered on the appliance's own terms: each is written and then read back from its place in the report, and one that a cabinet does not actually move is retired on first use. The display unit is confirmed and kept to the per-attribute channel, so it does not appear on unrelated families.

Also in this release: the central-cabinet presence-airflow control writes the correct command, corrected against the manufacturer's configuration.

Verified: 722 tests; a regression pass over every stored capture (the wire decode is byte-for-byte unchanged); deployed and A/B-verified on two classic wall units (identical entities and features before and after, only live readings differing) with a live setpoint change-and-restore. The classic wall units cannot exercise the new central-cabinet controls, so the deploy proves no regression; the new controls are exercised against real 0d012 captures in the regression tests and validated against Haier's own configuration.

v0.67.1 — central-cabinet telemetry read where the appliance puts it

Choose a tag to compare

@enapt enapt released this 01 Sep 08:01

The central cabinets' running data is now read from where the appliance actually puts it. The 0d012 central air conditioners report their compressor and refrigeration figures at the end of the big-data frame — the same block the wall-mounted units use, further along a longer report — not at the byte positions the manufacturer's field list gives, which read a flat zero on the real hardware. Reading from the frame's length gives the true values, confirmed against three captures from an owner's cabinet as the compressor ran: the compressor frequency reads zero when idle and rises with load, the evaporator coil cools and the discharge line heats as it works, and the compressor's own on/off state is reported. Those readings, and a larger future model that follows the same convention, now decode without a per-model entry.

Power and current are not shown for a cabinet that does not measure them. These three-phase cabinets do not report electrical power: the power field reads zero and the current field sits at its fixed full-scale maximum whether the compressor is off or running hard, and the appliance's model carries no voltage, per-phase or energy reading anywhere. Rather than surface a constant that tracks nothing, the power and current entities are retired for such a unit, while its compressor frequency, temperatures and running state — which are real — are kept.

Verified: 718 tests; deployed and A/B-verified on two classic wall units (identical entities and features before and after, only live readings differing) with a live setpoint change-and-restore. The central-cabinet decode is exercised by real captures from an owner's 0d012 cabinet, carried in the regression tests.

v0.67.0 — central-cabinet running data, decoded and kept live

Choose a tag to compare

@enapt enapt released this 01 Sep 05:58

Central-cabinet running data is read, and it keeps being read. The 0d012 central air conditioners answer the telemetry query with a big-data report this integration now decodes, using the layout from the manufacturer's own protocol document for that appliance kind. Its power, compressor frequency and current, coil temperatures and compressor state sit exactly where the wall-mounted family's do, one block further into a longer frame — so where the appliance reports them, they now appear as entities.

A telemetry frame larger than any known layout is no longer thrown away. The report is recognised by what it is, not by its size, so a newer or larger model that answers the query keeps being polled and kept in a diagnostics download for a layout to be added — rather than being written off after a few cycles and never asked again. Only the entities that need a field map wait for one; the moment a build learns the layout, they fill in.

Sign-in says what actually went wrong. A phone or router that cannot reach Haier's servers at sign-in now reports that it could not connect, instead of "check your email and password" — so a network or DNS problem no longer sends you to re-check a password that was never the issue.

The module's address stays out of the diagnostics issue list. The repair notices this integration raises are now identified by the configuration entry rather than by the appliance's Wi-Fi MAC, so a diagnostics download you attach to a report no longer carries that address in its issue list. Any notice raised under the old scheme is cleared automatically.

Verified: 718 tests; deployed and A/B-verified on two classic wall units (identical entities and features before and after, only live readings differing) with a live setpoint change-and-restore. The central-cabinet telemetry decode needs a 0d012 cabinet to exercise, which these wall units are not; its regression test carries a real frame from one.

v0.66.1 — an unreachable Haier host no longer breaks setup, re-keying or sign-in

Choose a tag to compare

@enapt enapt released this 01 Sep 05:00

A Haier host that cannot be reached no longer breaks the integration. The cloud requests go through httpx, and what httpx raises when a host is unreachable — a DNS miss, a refused or unroutable connection, a timeout — is its own exception class, which descends from Exception directly rather than from OSError or TimeoutError. Every guard around a cloud call handles CloudError, OSError and TimeoutError, so those failures passed straight through them. The transport wrapper now translates them into the error every caller handles, naming the class and the host (POST uhome-sgp.haieriot.net: ConnectError: …) with the original attached as the cause.

Where it showed: the "Which model is this?" step of a manual add died with Home Assistant's generic Unknown error occurred. The model number resolves offline; the supplementary catalogue lookup that follows then failed on a host the network could not reach, and the whole flow failed with it — an air conditioner that could not be added at all, on account of a lookup that is optional. The same leak turned a key rotation whose token refresh could not reach the cloud into an unexpected poll error instead of the reauth repair. Reported by an owner whose resolver was handing out an address for Haier's account host that their ISP could not route; with the fix, the model step completes (the appliance falls back to the rules shipped with the integration), sign-in and re-keying report the failure instead of dying on it, and the CLI prints one line instead of a traceback.

The suite could not see this: its stand-in transport refuses with OSError, the one class every caller catches. The new flow test drives the real fetch through the real wrapper around a client that fails the way httpx fails, and fails against the previous build.

Verified: 715 tests; deployed and A/B-verified on two classic wall units (identical entities and features, only live readings differ) with a live setpoint change-and-restore. The fixed path itself needs an unreachable Haier host to exercise, which the box does not have; the regression test is its witness.