Skip to content

vNext: Support Gree zone controllers - #552

Open
Twintop wants to merge 6 commits into
RobHofmann:5.0-devfrom
Twintop:zone-controller-support
Open

Twintop wants to merge 6 commits into
RobHofmann:5.0-devfrom
Twintop:zone-controller-support

Conversation

@Twintop

@Twintop Twintop commented Oct 10, 2026

Copy link
Copy Markdown

Context

This adds support for Gree zone controllers. It is part of porting the changes I made for my own zone controller (Gree LE60-13/GH) to this project. #547, #548 and #549 prepared for it. It also takes in the DeviceType idea that @p-monteiro suggested in #549. Two more PRs build on this one: the per mode memory of the zone target temperatures, and the same memory for every device. Background and the full plan: https://github.com/Twintop/ha-gree-zone-controller/blob/main/upstream/README.md

What a zone controller is

A zone controller drives the dampers of one ducted unit. Seen: LE60-13/GH with a ME31-00/C13 WiFi module, firmware 362001062617+U-W05SAV1.27.bin. It answers the scan like a VRF gateway, with subCnt above zero, but its sub-units are not indoor units:

  • Sub-unit 00 is the ducted unit (model id 5000). It answers Pow, Mod, WdSpd and AllErr, nothing more. Its Pow is the power of the whole system.
  • Sub-units 01 to 08 are the zones (model id 5001). A zone answers Pow (the damper), StTem (its target temperature, degrees Celsius minus 16) and a copy of Mod and WdSpd of the ducted unit.
  • Every sub-unit answers the info request with the name of the controller (GR-ZCntrlr_...), and the sub-device list calls every unit zone.

Symptoms on 5.0-dev

  • All sub-units are named zone, and the gateway device is labelled "VRF gateway".
  • The ducted unit numbers its modes 1 Cool, 2 Heat, 3 Dry, 4 Fan, 5 Auto. Read with the normal numbers, Fan shows as Heat, and Auto raises ValueError from OperationMode(5).
  • The ducted unit has no Tur column. Turbo is fan speed 6, so turbo is not offered, and fan speed 6 raises ValueError from FanSpeed(6).
  • Every zone gets a climate entity with the mode of the ducted unit, and no target temperature, because StTem is not polled.
  • Every fan change logs Property Quiet is unsupported on this device at error level. This happens on any unit without Quiet.

Change

Protocol layer:

  • DeviceType for every device, set after the info fetch: ac_unit (a standalone unit), vrf_unit (an indoor unit behind a VRF gateway), zone_controller (sub-unit 00 of a zone controller) and zone. The name the sub-unit reports and its MAC suffix tell them apart. Nothing is added to the config entry, so there is no migration. Only the two zone controller types change behaviour; ac_unit and vrf_unit work as before. The type is in the device diagnostics.
  • operation_mode and set_operation_mode() convert the zone controller mode numbers.
  • Turbo is fan speed 6 on the ducted unit: feature_turbo, set_feature_turbo() and supports_property(FEAT_TURBO_MODE) handle that. Leaving Cool or Heat drops turbo to High.
  • New GreeProp.ZONE_TARGET_TEMPERATURE (StTem). Only zones poll it (DeviceState.add_polled()), so all other units send the same columns as before. A test checks that.
  • Discovery names the sub-units AC unit and Zone 1 to Zone 8.

Home Assistant layer:

  • A zone gets no climate entity. It gets a switch with the name of the zone, and a Target Temperature number (16 to 30 degrees C).
  • The switch is the optional feature Zone Switch, so it can be left out for a zone that must always stay open. Turning such a zone off is refused by the controller or turns the whole system off. The feature is only offered on zones, and zones do not show the climate options.
  • GreeEntityDescription.device_filter limits an entity to some kinds of device when the props alone cannot tell.
  • The gateway device of a zone controller is labelled "Zone controller", from the type of a bound sub-unit. With no bound sub-unit the registry keeps what it has. The lookup is a small helper, so reconcile_vrf_controllers() stays simple.
  • The climate entity only sets Quiet and Turbo on units that have them.
  • en.json, icons.json, docs/protocol.md, docs/entities.md, docs/configuration.md, docs/architecture.md and supported-devices.md are updated.

Testing

  • pytest: 272 passed (255 on 5.0-dev, plus 17 new).ake controller that behaves like the real one: subDevform only, no cid, per unit columns, mode and fan mirrored to the zones, and an empty result above the column limit.
  • Ruff 0.17.0 passes. Mypy and Pylint show nothing neran them without Home Assistant installed, so Icompared the findings with the base branch.
  • Real device, LE60-13/GH on Home Assistant 2026.10, tities and states are right (no target temperature orswing on the AC unit, turbo offered), the gateway shows as "Zone controller" with the AC unit and zones linked, and a zone target temperature, a zone switch, the fan speed and Heat wi all reach the controller. No errors in the log.probe_device_limits() measured 29 columns. The module answers 30 with an empty result, and requests far above that (about 40 columns and more) made it reboot.

A Gree zone controller (seen: LE60-13/GH with a ME31-00/C13 WiFi module)
drives the dampers of one ducted unit. It answers like a VRF gateway, but
its sub-units are the ducted unit (MAC suffix 00) and up to eight zones
(01 to 08), and they behave differently from VRF indoor units.

Protocol layer:
- GreeDevice detects the role of a sub-unit after the info fetch: it
  reports the name of the controller (GR-ZCntrlr...), and the MAC suffix
  tells the ducted unit from a zone.
- The ducted unit numbers its modes 1 Cool, 2 Heat, 3 Dry, 4 Fan, 5 Auto.
  Before this, Fan showed as Heat and Auto raised. operation_mode and
  set_operation_mode convert for every unit with a zone role.
- It has no Tur column. Turbo is fan speed 6, only in Cool and Heat.
- A zone has a target temperature, StTem, in degrees Celsius minus 16.
  Only zones poll it, so other units send the same columns as before.
- Discovery names the sub-units "AC unit" and "Zone 1" to "Zone 8"
  instead of "zone" for every one of them.

Home Assistant layer:
- A zone gets no climate entity. It gets a switch with the name of the
  zone and a Target Temperature number.
- The switch is the optional feature Zone Switch, so it can be left out
  for a zone that must always stay open. It is only offered on zones, and
  zones do not show the climate options.
- GreeEntityDescription.device_filter limits an entity to some kinds of
  device when the props alone cannot tell.

Tested against a real LE60-13/GH (firmware 362001062617 V1.27) and against
a new fake controller in tests/fakes/zone.py.
ZoneRole only knew the sub-units of a zone controller. It is now DeviceType
and every device gets one after the info fetch: ac_unit (a standalone
unit), vrf_unit (an indoor unit behind a VRF gateway), zone_controller (the
ducted unit, sub-unit 00 of a zone controller) and zone. Only the two zone
controller types change behaviour, so standalone and VRF units work as
before. The type is in the device diagnostics. Suggested by @p-monteiro in
RobHofmann#549.

The controller device of a zone controller was labelled "VRF gateway". It
is now "Zone controller", taken from the type of a bound sub-unit. With no
bound sub-unit the registry keeps what it has. The lookup moved into a
small helper, so reconcile_vrf_controllers stays simple.
Every fan mode change called set_feature_quiet and set_feature_turbo, also
on a unit without those columns. The state then logged "Property Quiet is
unsupported on this device" at error level on every change. Seen on the
ducted unit of a zone controller, but any unit without Quiet or Turbo did
it. Units that have them work as before.

@RobHofmann RobHofmann left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Thanks for this, and for taking in the DeviceType idea. I ran the PR in my dev instance against my standalone test unit (encryption v1). The start is clean: same 29-column probe, Device type: ac_unit, and the poll still sends the same 20 columns, without StTem. A fan change still sends Quiet and Tur as before. pytest gives 272 passed. I could not test the zone parts myself, because I have no zone controller.

One blocker and one question.

1. translation_key=UNDEFINED breaks setup when no sub-unit is bound (blocker)

In reconcile_vrf_controllers(), when the controller device already exists and _controller_info() finds no bound sub-unit, model and translation_key stay UNDEFINED and go into async_get_or_create(). That works for model, but translation_key is typed str | None, and Home Assistant does this:

if translation_key:
    name = self._resolve_translated_name(config_entry, translation_key, translation_placeholders)

UNDEFINED is truthy, so the name lookup falls back to the key itself and then calls .format() on it. I ran Home Assistant's own _resolve_translated_name() with UNDEFINED in my dev container:

'vrf_controller' -> 'vrf_controller'
<UndefinedType._singleton: 0> -> RAISES AttributeError 'UndefinedType' object has no attribute 'format'

The call in async_setup_entry() has no try around it. So when a local VRF gateway is offline at Home Assistant start, I expect the whole entry to fail, including standalone units in the same entry, and without a retry because it is not ConfigEntryNotReady. That also hits plain VRF users, not only zone controllers. I did not run that full scenario end to end; the crash itself is reproduced. Mypy flags the same line (helpers.py:475, arg-type), and it is the only new mypy error in the changed files.

A simple fix: only pass model, translation_key and translation_placeholders when the type is known, for example by building the kwargs and leaving those keys out when they are UNDEFINED. A test with an existing controller device and no bound sub-unit would cover it.

2. Zone Switch default in a new setup (question)

async_step_device_options() uses the options of the previous sub-unit of the same controller as defaults (_options_by_controller), and the features default is defaults.get(CONF_FEATURES, valid_features). If the AC unit is configured first, its feature list has no zone, so as far as I can read it, Zone 1 opens with Zone Switch unticked. The other way round, the AC unit could get zone as a default that is not in its options. Did you see this on your controller, or did you tick it by hand? If it is real, filtering the copied defaults against valid_features would solve both cases.

When an existing controller device had no bound sub-unit at start, for
example a VRF gateway that is offline, reconcile_vrf_controllers passed
UNDEFINED as translation_key. Home Assistant types it str | None and checks
it with "if translation_key:", so it tried to format UNDEFINED and the
whole entry failed with AttributeError, with no retry. Found in review.

The model and the name arguments are now left out when the type is not
known, so the registry keeps what it has. A new device still gets the VRF
label. The decision moved into _controller_label.

Reproduced on Home Assistant 2026.10 with a VRF device whose gateway never
answers: before, the entry stayed in setup_error with every entity
unavailable; after, the entry loads and the other devices work.
When the device options step starts from the options of the device of the
same controller set up before, it copied all of them. So each sub-unit's
form showed the name of the device before it, and the features came from
a device of another kind: a zone after the AC unit started with Zone
Switch off, and the AC unit after a zone got a "zone" default it cannot
take. Found in review.

- Only the mode and feature lists are copied now, the same lists the
  copy by model already used. The name stays the device's own.
- The Zone Switch is never copied, because only an always open zone goes
  without it: a zone starts with it on, any other device without it.
- The feature default is filtered against the features the device has.
cleanup_device_connection_issues deleted issues from the issue registry
while it looped over registry.issues. After a device that had a "connection
failed" issue was removed from the config, the next setup deleted that
issue and then failed with "RuntimeError: dictionary changed size during
iteration", so the entry stayed in setup_error. A second stale issue was
never reached.

Found while testing the controller label fix: a VRF device whose gateway
never answers was removed from the YAML again, and the entry failed at the
next start. A manual reload worked, because the issue was already gone.
@Twintop

Twintop commented Oct 10, 2026

Copy link
Copy Markdown
Author

Thanks for testing it on your standalone unit. Both points are fixed, plus a bug I found while testing.

1. translation_key=UNDEFINED (blocker)

Confirmed end to end on Home Assistant 2026.10: with a VRF device whose gateway never answers in the same entry as the zone controller, the entry failed on every start, all its entities were unavailable, and there was no retry.

Fixed in 5bfa6cb. The new _controller_label() leaves out the model and name when no sub-unit is bound and the device already exists, so it keeps its label. A new device gets the VRF gateway label as before. The entry now loads.

Mypy missed it on my side because I had only tested in my Home Assistant instance, not with a local one in the dev environment, so every Home Assistant type was Any. I now run it against a local install with the repo's mypy.ini, and there are no new errors compared with 5.0-dev.

Question: A test for reconcile_vrf_controllers() needs Home Assistant in the test environment, and the suite only imports aiogree today. If you like, I can add pytest-homeassistant-custom-component in a separate PR, with this case as the first test.

2. Zone Switch default (question)

It is real. I had ticked it by hand. My sub-units report no ModelType, so the form falls back to the previous sub-unit's options, and that also copied its name. Fixed in d6abdcd:

  • only the mode lists and features are copied, the same keys as the model path;
  • zone is never copied, so a zone always starts with Zone Switch ticked and other units never get it;
  • the features default is filtered against valid_features.

3. Bug found while testing (already on 5.0-dev)

cleanup_device_connection_issues() deleted repair issues while looping over registry.issues. After a device with a "connection failed" issue was removed, the next start failed with RuntimeError: dictionary changed size during iteration. Fixed in d666fe1 by looping over a copy. Checked on my install: the entry loads, and both the issue and the stale gateway device are removed.

@Twintop
Twintop requested a review from RobHofmann October 10, 2026 12:47
# A zone controller and its sub-units report a name that starts with this.
ZONE_CONTROLLER_NAME_PREFIX = "GR-ZCntrlr"
# The model ids of the sub-units in the sub-device list.
ZONE_CONTROLLER_MID_AC_UNIT = "5000"

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.

Are we certain that all Gree Zone controllers are 5000/5001 and not only your device?

return self._device_type

@property
def zone_target_temperature(self) -> int | None:

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.

Do we need this? Can this logic not be wired to the existing target_temperature properties?

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.

3 participants