Skip to content

vNext: Reconfigure menu with single-device edit - #554

Open
RobHofmann wants to merge 5 commits into
5.0-devfrom
feat/reconfigure-edit-only
Open

RobHofmann wants to merge 5 commits into
5.0-devfrom
feat/reconfigure-edit-only

Conversation

@RobHofmann

@RobHofmann RobHofmann commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner

Reconfigure now opens a menu, so one device can be edited without discovery and without a cloud login.

1. Reconfigure menu

Symptom. Reconfigure always ran discovery and walked through the forms of every device in the entry. On a cloud entry it also logged in again. The new token logs the user out of the Gree app.

Cause. async_step_reconfigure went straight into the setup chain: cloud_add (with login()), local_add, device_picker, then the forms for every picked device.

Change. Reconfigure shows a menu. Every action returns to it:

Item What it does Cloud login
Edit a device Radio list, then the connection and option forms of that one device No
Rediscover devices Discovery as before, but the picker lists only new devices (see below). Picked devices go through their forms. Never removes a device. Yes, cloud discovery needs it
Remove devices Multi-select of configured devices No
Save and reload Writes the entry once, reloads once No
  • Edit without a bind. When the bind settings are unchanged (key, uid, controller MACs, host, port, encryption version, prefer cloud), the running device from entry.runtime_data is reused. No bind runs. Changing only the scan interval or the timeout does not bind either.
  • Edit with a bind. When a bind setting changes, the device binds as before. A cloud bind uses an MQTT transport built from the stored token, without a login.
  • VRF. Each unit is edited on its own. Controller-level local settings (host, port, encryption version) are shared by every unit behind the same mac_controller_local. When an edit changes them, the other units get the same values. The form text says so.
  • Rediscover is additive. The picker lists devices that are not in the entry, and configured devices for which discovery found a connection they lack. An example is a cloud-only device that is now found locally. All listed devices are preselected. Every picked device goes through its forms, with the found host filled in. Configured devices with nothing new are not listed. If only their host moved, it is refreshed silently, like the DHCP step does. When nothing is new, a short step says so and returns to the menu, so the pending changes are kept.

2. Connection cleanup

  • Config flow. The flow closes its own MQTT and UDP transports. It closes the previous MQTT connection before a new cloud login, and closes all of them before the entry is written. async_remove closes them when the flow ends or the user closes it. Before this, every login in a flow left a connection open.
  • Entry unload. async_unload_entry shuts the coordinators down and then disconnects every transport explicitly. The last unbind already closed MQTT in the normal case. This also covers an unbind that fails. GreeCoordinator.async_shutdown now runs only once, because Home Assistant calls it again from the unload callbacks.
  • Log line. When the broker refuses the MQTT connection at setup, the reason is logged at warning level before reauth starts.

3. Refresh a refused cloud token

When the MQTT broker refuses the stored token with "Bad user name or password" (MQTT 3.1.1 rc 4 or 5, which paho reports as 134 or 135), the integration logs in once with the stored email, password and region. Then it stores the new token and binds again. This happens at entry setup, and when Edit a device binds a cloud device. If that login fails, or the new token is refused too, setup raises ConfigEntryAuthFailed as before, so reauth still works. There is one refresh per setup or flow, so it cannot loop. A login that returns another account is refused.

What is known about Gree sessions, from testing: a cloud login logs the Gree app out, and an MQTT connection with an existing token does not. Why Gree stops accepting a token is not known. It can happen soon after a login, and the cloud may rate-limit logins. The docs say exactly this, and suggest a separate account if it happens often.

4. Device registry lookups

device_registry.async_get_device is deprecated and stops working in Home Assistant 2027.8. All five calls (config_flow.py, migration.py) now go through get_device_by_identifier(). It uses async_get_device_by_identifier on Home Assistant 2026.8 and newer (checked per release). On older versions it falls back to async_get_device with an owner check, because hacs.json still allows 2026.3.

5. Cloud-only bind logged an error

A cloud-only bind in the config flow always passed a UDP transport, so aiogree logged No controller MAC provided for local transport at error level. This was already on 5.0-dev. The flow now uses the same rule as entry setup: a local transport only with a host and a local controller.

6. Untranslated keys

Some keys reached the UI without a text:

  • cannot_connect_mqtt in the connection form had no text in any language.
  • ConfigEntryAuthFailed got bad_credentials, no_account_info and "Failed to initiate Gree device" as plain messages, so the entry card showed them raw.
  • A successful reauth ended on the raw reason reauth_successful, and a second flow for the same account on already_in_progress.

All of them now have a text in en, nl and pt. The auth failures use translation keys (bad_credentials, no_account_info, device_bind_failed with the MAC). Home Assistant passes these keys on to the entry card (config_entries.py). tests/config_flow/test_translations.py reads every error, abort reason, step, menu option and exception key from the source, plus the reasons Home Assistant aborts with itself. It fails when one of them has no text in en, nl or pt.

7. YAML-managed entries

Reconfigure aborts with yaml_managed for an entry that the YAML import handled since the start. The YAML puts its own values back at the next start, so a UI edit would be lost. entry.source cannot be used for this: when an import updates an entry that was made in the UI, the source stays user. The import step records the unique_id in hass.data. That record lives only while Home Assistant runs, so the entry can be edited again after its YAML block is removed and Home Assistant restarts. The 4.x gree: block counts too, because the migration imports it into the local-only entry.

8. Reuse

Pedro asked for reuse, not parallel code paths. So:

  • One pending map. Reconfigure keeps one pending device map. Edit, rediscover and remove change it, and _async_finish writes it. Its registry cleanup and its move from local-only to cloud are unchanged.
  • Same forms. connection_options and device_options are the same steps. During reconfigure they return to the menu instead of finishing.
  • Shared helpers.
    • The bind code moved to _bind_device(), with the same logic.
    • _collect_device_configs() is shared by the setup path and the reconfigure path.
    • create_mqtt_transport() and async_disconnect_transports() in helpers.py are shared by __init__.py and the config flow.
    • setup_picker_schema got a multiple flag for the radio list.

Reauth is unchanged: async_step_reauth -> cloud_add -> the reauth branch of _async_finish. An auth failure still only asks for the cloud login.

Config flow tests

New tests/config_flow/ runs a real Home Assistant through pytest-homeassistant-custom-component 0.13.371 (Home Assistant 2026.10.0). It runs against the fake Gree Cloud over loopback HTTP, a fake MQTT broker that forwards to fake units, and fake units on loopback UDP. Every test counts the cloud logins, and checks that only the loaded entry has open connections. It runs in the existing CI job "Tests".

Each failure from the review has its own regression test:

Reported Test
Save and reload right after creating a cloud entry gave bad_credentials test_setup_refreshes_a_revoked_token_once
A3: Edit a device, Prefer cloud, cannot_connect_mqtt code 134 test_edit_with_prefer_cloud_refreshes_a_revoked_token
A6: cloud-only unit found on the LAN stayed on MQTT test_rediscover_finds_a_cloud_only_unit_on_the_lan
Picker listed a configured unit with nothing new same test
Only the new unit went through the forms test_rediscover_runs_the_forms_of_every_picked_unit
8 MQTT loops at shutdown test_flows_with_two_logins_and_an_edit_leave_no_client_open

They also cover: user setup with cloud and local, finish at once without a login, edit with and without a new bind, nothing new, remove, VRF host copy, the YAML abort, reauth when the stored-account login fails too, closing a flow after a login, and both branches of the registry helper.

Testing

Real unit (U-CS532Z(LT)V3.75, local-only entry), driven through the config flow API:

  • YAML abort: with the YAML blocks present, reconfigure aborts with yaml_managed.

  • Rename only: the log shows "unchanged, using its running bind". No discovery, one reload, and the new name is stored.

  • Bind setting changed: encryption version set to auto. A real bind ran in the flow, found v1, and stored "1".

  • Rediscover: local discovery found the unit and refreshed it without device forms. Back to the menu, saved, and the unit bound again after the reload.

  • Rediscover, nothing new: returns to the menu and saves. The reload is clean.

  • Add Integration: a fresh flow (local) finds the unit, binds it and creates the entry, and the entry binds.

Offline harness, 11/11 passed:

  • VRF copy: a sibling unit gets the new host, a unit behind another controller is untouched, and an options-only edit leaves the siblings alone.
  • Rediscover filter: a cloud-only device found locally is listed, an unconfigured device is listed, a device with nothing new is hidden, a device whose IP moved is hidden and refreshed, and nothing is removed.

pytest: 292 passed. Ruff check and format pass. mypy showed no new errors on the first commit; it was not run again on the later commits. The code was compiled under Python 3.14 in the dev container.

Pedro tested the first commit on his cloud entry: rediscover of a cloud-only unit with its IP (switches to local), remove, and edit. His failures are fixed in the later commits and covered by the regression tests above.

Notes

  • Token refresh not tried against the real Gree servers. It is covered by tests against the fakes only. @p-monteiro, on the latest commit: A3 (Edit a device, Prefer cloud) should now work with one login, and a plain reload should no longer start reauth.
  • Login rate limit. An entry whose token is refused at every load logs in once per load. If Gree rate-limits logins, many reloads could hit that limit. The warning Gree Cloud refused the stored token shows each refresh.
  • An abort loses the pending changes. A cloud login for another account (unique_id_mismatch) still aborts and loses the pending changes. Home Assistant has no way back to the menu from an abort.
  • VRF sub-unit gaining a local host. If a configured sub-unit gains a local host through rediscover, the controller rule also gives that host to the other configured units behind the same mac_controller_local.
  • Portuguese texts need review. The new pt.json texts were not written by a native speaker. Please check them.
  • Overlap with vNext: Support Gree zone controllers #552. vNext: Support Gree zone controllers #552 (zone controllers) also changes config_flow.py, config_schema.py and helpers.py. The PR that merges second needs a rebase.
  • Version. Bumped to 5.0.0-alpha.108.

Documentation

  • docs/configuration.md: the Reconfigure section is rewritten around the menu.
  • docs/config-entry.md: new "Reconfigure" section, and the YAML marking.
  • docs/architecture.md and docs/troubleshooting.md: updated for the menu and for when a cloud login happens.
  • docs/config-entry.md: "Token refresh" and "Connection lifetime" sections.
  • docs/development.md, AGENTS.md and .claude/skills/run-tests/SKILL.md: how to run the config flow tests and what they fake.

Reconfigure always ran discovery and walked through the forms of every
device. On a cloud entry it also logged in again, and the new token logs
the user out of the Gree app.

Reconfigure now shows a menu that loops back to itself:
- Edit a device: pick one device and run its connection and option
  forms. No discovery and no cloud login. When the bind settings are
  unchanged, the running device is reused and no bind runs. A changed
  cloud bind uses the stored MQTT token.
- Rediscover devices: the old discovery path, with a login for cloud
  entries. Configured devices only get their host and controller MACs
  refreshed; only new devices go through the forms. A cloud-only device
  does not get a local host.
- Remove devices.
- Save and reload: one write, one reload.

The changes collect in one pending device map. The existing steps are
reused: connection_options and device_options return to the menu during
reconfigure, the bind code moved to _bind_device(), and _async_finish
writes the pending map. create_mqtt_transport() is shared with
async_setup_entry. Controller-level local settings (host, port,
encryption version) are copied to the other units behind the same VRF
controller.

Reconfigure aborts for entries the YAML import manages, because the YAML
puts its values back at the next start. Reauth is unchanged.

Tested on a real unit (local-only entry): rename without a bind, bind
setting change with a real bind, rediscover, YAML abort. Offline harness
for the VRF copy and the cloud-only rule: 6/6. pytest 255 passed.

Bump to 5.0.0-alpha.108.
Review feedback on the reconfigure menu:

- Rediscover now lists only devices that add something: devices not in
  the entry, and configured devices for which discovery found a missing
  connection (a cloud-only device found locally). Every picked device
  goes through its forms, with the found host filled in. Other found
  devices get their host refreshed silently. Rediscover never removes a
  device. When nothing is new, a short step returns to the menu instead
  of aborting, so pending changes are kept.
- The config flow closes its own MQTT and UDP transports: the previous
  one before a new cloud login, all of them before the entry is written,
  and in async_remove when the flow ends or is closed.
- Entry unload shuts the coordinators down and then disconnects every
  transport explicitly, also when an unbind fails. The coordinator
  shutdown runs once.
- A refused MQTT connection at setup is logged with the broker's reason
  before reauth starts.

Tested on the real unit (local-only entry): rediscover with nothing new
returns to the menu and saves, reload is clean, and a fresh Add
Integration flow binds and creates the entry. Offline harness for the
VRF copy and the rediscover filter: 11/11.
@RobHofmann

Copy link
Copy Markdown
Owner Author

@p-monteiro thanks for the test run. The new commit addresses your points:

  • Rediscover now lists only new devices. That includes a configured cloud-only device that is now found locally. Every picked device goes through its forms, with the found IP filled in. It never removes a device. When nothing is new, it goes back to the menu.
  • MQTT loops. The config flow now closes its own connections: before a new login, before saving, and when the flow is closed. Entry unload also disconnects explicitly.

About bad_credentials on "Save and reload": I do not have a cause yet. Normal unload already closed the MQTT connection, so a leak on reload is unlikely. One guess: after the add flow you logged back in to the Gree app. If that makes the token stored in Home Assistant invalid, every reload fails, with or without this PR. Could you check two things?

  1. Log in to the app, then reload the cloud entry the normal way (entry menu > Reload, no reconfigure). Do you get bad_credentials too?
  2. When it happens, look for the new warning line Gree Cloud refused the MQTT connection for entry .... It shows the broker's reason.

Config flow tests (tests/config_flow/) run a real Home Assistant through
pytest-homeassistant-custom-component 0.13.371 (HA 2026.10.0), against
the fake Gree Cloud over loopback HTTP, a fake MQTT broker that forwards
to fake units, and fake units on loopback UDP. They cover user setup
with cloud and local, reconfigure (finish at once, edit with and without
a new bind, rediscover of a cloud-only unit found on the LAN, nothing
new, remove, VRF host copy), the YAML abort, reauth after a refused
token, and closing a flow after a login. Every test counts the cloud
logins and checks that only the loaded entry has open connections.

Device registry lookups now go through get_device_by_identifier(). It
uses async_get_device_by_identifier on HA 2026.8 and newer, because
async_get_device is deprecated and stops working in 2027.8, and falls
back to async_get_device with an owner check on older versions. This
replaces all five calls in config_flow.py and migration.py.

A cloud-only bind in the flow no longer passes a UDP transport. Before,
aiogree logged "No controller MAC provided for local transport" at
error level on every cloud-only bind. The flow now uses the same rule
as the entry setup: a local transport only with a host and a local
controller.

pytest: 271 passed.
When the MQTT broker refuses the stored token ("Bad user name or
password", MQTT 3.1.1 rc 4/5, which paho reports as 134/135), the
integration now logs in once with the stored email, password and region,
stores the new token and binds again. This happens at entry setup and in
reconfigure when Edit a device binds a cloud device. If that login fails
or the new token is refused too, setup raises ConfigEntryAuthFailed as
before, so reauth still works. One refresh per setup or flow, no loop.
A login that returns another account is refused.

Regression tests for every reported failure: reload with a refused
token, Edit a device with Prefer cloud and a refused token (code 134),
rediscover of a cloud-only unit found on the LAN (now also listed as
Local in Edit a device), two picked units both go through the forms,
and no MQTT client left open after two logins, an edit and a reload.

Docs describe what is known about Gree sessions: a cloud login logs the
app out, an MQTT connection does not, and why a token stops working is
not known (possibly a login rate limit).

pytest: 285 passed.
The connection form showed the raw key cannot_connect_mqtt, because it
had no text in any language. The entry card showed bad_credentials,
no_account_info or "Failed to initiate Gree device" as plain messages,
because ConfigEntryAuthFailed got them as text instead of a translation
key. A successful reauth ended on the raw reason reauth_successful, and
a second flow for the same account on already_in_progress.

- Texts for cannot_connect_mqtt, reauth_successful and
  already_in_progress in en, nl and pt.
- ConfigEntryAuthFailed now uses translation keys: bad_credentials,
  no_account_info and device_bind_failed (with the MAC).
- tests/config_flow/test_translations.py reads every error, abort
  reason, step, menu option and exception key from the source, plus the
  reasons Home Assistant aborts with itself, and fails when one has no
  text in en, nl or pt.

pytest: 292 passed.
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