Skip to content

vNext: YAML import for the gree_custom block - #512

Merged
RobHofmann merged 5 commits into
4.0-pre-releasefrom
feat/yaml-import
Sep 21, 2026
Merged

RobHofmann merged 5 commits into
4.0-pre-releasefrom
feat/yaml-import

Conversation

@RobHofmann

@RobHofmann RobHofmann commented Sep 20, 2026

Copy link
Copy Markdown
Owner

What

YAML import for the gree_custom: block in configuration.yaml. The integration already started an import flow per YAML item in async_setup, but async_step_import aborted with not_implemented and there was no CONFIG_SCHEMA, so the YAML was never validated or applied.

1. Schema

New config_schema.py with CONFIG_SCHEMA, for the shape in manual-configuration.yaml.

  • Device keys go through gree_extract_macs(), the same function discovery uses, so the controller MACs default the way discovery sets them: the device MAC for a normal unit, the first 12 characters for a VRF main device MAC ending in 00, and the part after @ for a key written as <mac>@<controller mac>. A VRF sub-device key without @ has no known local controller, so the schema requires mac_controller_local for it. A given controller MAC always wins over the derived one.
  • Connection keys get the same defaults the UI flow stores, so an imported entry has the shape from docs/config-entry.md. Option lists are only stored when written; the platforms already fall back to their defaults.
  • Validation: mode and feature lists against the known values, encryption_version in "0", "1", "2" (int accepted), target_temp_step a multiple of 0.5 between 0.5 and 5, scan_interval at least MIN_SCAN_INTERVAL, external sensors as entity ids. Every device needs a local block, or a top level cloud account plus a cloud block. Only one item may leave out cloud, because all local-only devices share one entry.
  • __init__.py imports the schema, which also clears the hassfest config_schema warning.

2. Import flow

async_step_import in config_flow.py:

  • Resolves the entry: local_only without a cloud block. With one, it looks for an entry that already stores the same email, region and password and reuses its unique_id, uid and token, so there is no cloud login on every restart. Only when there is none does it log in: on the first import, or after a change of email, region or password. A changed email still lands on the same entry, because the user id from the login is the unique_id. The README and manual-configuration.yaml say that every login ends the other sessions of the account (the Gree app logs out).
  • Skips devices that already belong to another entry, with an error in the log and a yaml_import_failed repair issue when nothing is left.
  • Creates the entry, or updates the existing one with async_update_reload_and_abort(..., reload_even_if_entry_is_unchanged=False). Unchanged YAML changes nothing. Changed YAML updates the entry and reloads it once. The reload waits for the entry's setup_lock, so it is safe while the entry is still being set up at startup. Devices dropped from the YAML are removed from the entry and the device registry.
  • The import does not bind. Entry setup already fetches the key, detects the version and finds a moved IP, and it raises the connection repair issue when the unit is offline.

The rules for users are in the README section and at the top of manual-configuration.yaml: the YAML is applied at every start and wins over UI changes to the same entry.

3. Docs

  • The README "Basic example" was still the 4.x flat shape. Replaced with a minimal gree_custom example plus the rules.
  • manual-configuration.yaml now matches the schema: faults is not a feature (it is auto-detected), the controller MACs are optional, the external sensors are omitted instead of "None", encryption_version shows "0".
  • docs/config-entry.md has a "YAML import" section. docs/architecture.md lists config_schema.py.

4. Dutch translation

New translations/nl.json with every key from en.json, including the two new keys above. Checked by script: same key set, same placeholders. Reviewed by a native speaker (the repo owner).

Testing

Real unit (U-CS532Z(LT)V3.75, encryption v1) on Home Assistant 2026.9.3:

Case Result
YAML for a device that exists in a UI-created entry, with a new name and feature list Entry updated before setup read it, one setup, no reload, switch.*_display_light created from the new feature
Restart with unchanged YAML Entry modified_at unchanged, one setup, no errors
Entry removed in the UI, restart Entry created with source: import, bound, six entities, same entity ids
check_config Prints the filled shape, no errors

Offline: a schema harness with 20 checks (the shipped example, MAC normalization, VRF controller MAC, every reject rule above). All pass.

ruff check and ruff format --check pass. Pylint and mypy were not run; neither was available.

Not tested: the cloud paths (no account here) and the "device already in another entry" path (needs two entries).

Notes

  • Invalid YAML fails the whole integration at startup, like every Home Assistant integration with a CONFIG_SCHEMA. UI entries then do not load either until the YAML is fixed.
  • manifest.json is not bumped.

The integration already started an import flow for every item under
gree_custom: in configuration.yaml, but async_step_import aborted with
not_implemented and there was no CONFIG_SCHEMA, so the YAML was never
validated and never applied.

Changes:

- config_schema.py: CONFIG_SCHEMA for the shape in manual-configuration.yaml.
  Normalizes MAC keys (separators and case), derives the controller MACs
  when they are not given, fills the connection defaults, validates the
  mode and feature lists, and checks that every device can be reached
  (a local block, or a cloud account plus a cloud block). Only one item
  may leave out the cloud block. __init__.py imports the schema so
  Home Assistant and hassfest find it.
- config_flow.py: async_step_import resolves the target entry (local-only
  or cloud account), skips devices that already belong to another entry,
  then creates the entry or updates it with async_update_reload_and_abort.
  Unchanged YAML causes no reload. Cloud login happens only when no entry
  stores the same email, region and password; otherwise the stored uid
  and token are reused.
- A yaml_import_failed repair issue and an import_failed abort reason.
- Docs: README example was still the 4.x flat shape. manual-configuration.yaml
  now matches the schema (faults was listed as a feature, the controller
  MACs are optional). docs/config-entry.md and docs/architecture.md
  describe the import.

Tested on HA 2026.9.3 against the real unit (U-CS532Z, encryption v1):
update of an existing UI entry (name and features applied, one setup,
no reload), unchanged YAML on restart (entry not modified, one setup),
and creation after the entry was removed (source import, bound, six
entities). Schema harness: 19/19 checks. ruff check and format pass.
Pylint and mypy were not run, neither is available here.
Complete nl.json with every key from en.json, including the new
import_failed abort reason and the yaml_import_failed repair issue.

return all(
stored.get(key) == cloud_conf.get(key)
for key in (CONF_EMAIL, CONF_REGION, CONF_PASSWORD)

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.

This might break if the user changes the email of the account. That's why the config entry unique_ids is the account ID, which is a fixed int, and can only be obtained after login. Here's the thing: logging in every time you import to check this will log you out of wherever you have logged in previously, like the Gree app, which may be annoying. Not saying it cannot be done, but at least inform the users in manual-configuration.yaml and the README about that behaviour.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Good point on the sessions. The match is only there to avoid a login when nothing changed. A login happens on the first import and after a change of email, region or password. A changed email fails the match, so there is one login, and the user id from that login is the unique_id, so the import still lands on the same entry. Without a login there is no uid to match on, so I kept the three YAML fields. I documented when a login happens and that it ends the other sessions (Gree app) in the README, manual-configuration.yaml and docs/config-entry.md in e222e2c.

password=cloud_conf[CONF_PASSWORD],
)
try:
credentials = await cloud_api.login()

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.

Same as the other comment. This will log you out of other sessions.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Same answer as above: this login only runs when no entry stores the same email, region and password, so on the first import or after one of those changes. Documented in e222e2c.

Comment thread manual-configuration.yaml Outdated
local: # optional, if ommitted requires cloud
mac_controller_local: "20fabb123456" # MAC Address of the local controller (see above) | required | str
local: # optional, if omitted requires cloud
mac_controller_local: "20fabb123456" # MAC Address of the local controller (see above) | optional | str | default = the controller MAC derived from the device MAC

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.

For VRF, this is not possible. It goes the other way around. Here is the MAC address of the device that holds the connection (with the IP), and the MAC key above has to be obtained by discovering subdevices.
Granted, the extract_mac function used will parse the value above and split it, but if the device is a VRF and only the subdevice MAC is provided above, the controller will not be derived.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

You are right, I had the direction wrong. Fixed in e222e2c: for a 14 character MAC with a local block, mac_controller_local is now required and the schema rejects the config with a message that says it must be the MAC of the unit that holds the connection. A normal unit still defaults to its own MAC. The cloud controller defaults to the first 12 characters, as protocol.md says. manual-configuration.yaml and docs/config-entry.md are updated, and the harness has a case for both.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Reworked in 57ee11c, back on gree_extract_macs for the device key, so it behaves like discovery: a 14 character main device MAC ending in 00 derives the first 12, and a key written as @ sets the controller for both local and cloud. A sub-device MAC without @ is rejected for a local block, with a message that names both ways to fix it. A given mac_controller_local or mac_controller_cloud still wins. manual-configuration.yaml and docs/config-entry.md describe the @ form.

- A VRF sub-device (14 character MAC) has its local controller in
  another unit, so mac_controller_local cannot be derived from the
  device MAC. The schema now requires it for a 14 character MAC with a
  local block, and rejects the config with a clear message otherwise.
  For a normal unit the default stays the device MAC. The cloud
  controller defaults to the first 12 characters, as protocol.md says.
- README, manual-configuration.yaml and docs/config-entry.md now say
  when a cloud login happens (first import, or a change of email,
  region or password) and that every login ends the other sessions of
  the account, so the Gree app logs out at that moment.

Schema harness: 20/20 checks.
The device key now goes through gree_extract_macs(), the same function
local and cloud discovery use. That brings back the two cases the last
commit dropped: a VRF main device MAC that ends in 00 derives the first
12 characters as controller, and a key written as <mac>@<controller mac>
sets the controller for a VRF sub-device. A given mac_controller_local
or mac_controller_cloud still wins over the derived value.

A VRF sub-device key without @ has no known local controller, so the
schema rejects it when a local block is given, with a message that
names both ways to fix it.

Schema harness: 22/22 checks.
SETUP_SCHEMA and the five setup_*_schema() builders now live next to
CONFIG_SCHEMA, without the leading underscore and with docstrings.
config_flow.py imports them. The temperature step form uses
MIN_TARGET_TEMP_STEP and MAX_TARGET_TEMP_STEP instead of the literals.
No behaviour change.
@RobHofmann

Copy link
Copy Markdown
Owner Author

Review nits done in 6554d16: SETUP_SCHEMA and the five setup_*_schema() builders moved to config_schema.py, public names with docstrings, config_flow.py imports them. The temperature step form now uses MIN_TARGET_TEMP_STEP and MAX_TARGET_TEMP_STEP. No behaviour change; the moved bodies are identical apart from the renames. The probatio IP and MAC validators are a good follow-up once HA supports them fully.

@RobHofmann
RobHofmann merged commit fd206d8 into 4.0-pre-release Sep 21, 2026
2 checks passed
@RobHofmann
RobHofmann deleted the feat/yaml-import branch September 21, 2026 13:41
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