Skip to content

Base ZGP device support - #868

Merged
puddly merged 26 commits into
zigpy:devfrom
puddly:puddly/zgp-split
Oct 27, 2026
Merged

puddly merged 26 commits into
zigpy:devfrom
puddly:puddly/zgp-split

Conversation

@puddly

@puddly puddly commented Aug 22, 2026 •

Copy link
Copy Markdown
Contributor

Counterpart of zigpy/zigpy#1874. ZGP devices do not participate in automatic entity discovery at all, as no known devices emit useful capability information.

A basic quirk matching system is added for ZGP devices. An interrelated PR for zha-device-handlers will use it to create event entities.


Part of OpenHomeFoundation/roadmap#195

@codecov

codecov Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.56%. Comparing base (68f4c9e) to head (2a8ab10).
⚠️ Report is 8 commits behind head on dev.

Additional details and impacted files
@@            Coverage Diff             @@
##              dev     #868      +/-   ##
==========================================
+ Coverage   97.23%   97.56%   +0.32%     
==========================================
  Files          61       61              
  Lines       10704    10944     +240     
==========================================
+ Hits        10408    10677     +269     
+ Misses        296      267      -29     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@puddly
puddly marked this pull request as ready for review October 23, 2026 15:30
Copilot AI balanced review requested due to automatic review settings October 23, 2026 15:30

Copilot AI left a comment

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.

🔵 Needs a closer look

The shared device lifecycle refactor warrants human review because quirk-provided non-ZCL entity initialization and recommissioning remain untested.

1 open issue
What changed in this PR

Adds base Green Power device support to ZHA, complementing zigpy’s support and enabling quirk-provided entities without automatic ZCL discovery.

Changes:

  • Separates shared device behavior from Zigbee-specific functionality.
  • Adds Green Power quirk matching, device wrapping, and recommissioning support.
  • Adds tests for Green Power properties, diagnostics, matching, and lifecycle handling.
File Description
zha/​zigbee/​group.py Uses typed Zigbee device lookups for group operations.
zha/​zigbee/​device.py Introduces shared and Green Power device classes while preserving the Device alias.
zha/​quirks.py Adds Green Power matching and registry support.
zha/​application/​helpers.py Uses the shared active-coordinator availability check.
zha/​application/​gateway.py Supports multiple device types during creation and recommissioning.
tests/​test_green_power_device.py Tests Green Power devices and quirk resolution.

Review effort: Balanced

Comment thread zha/zigbee/device.py

@zigpy-review-bot zigpy-review-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nice split: BaseDevice / ZigbeeDevice / GreenPowerDevice is clean, the Zigbee-only behaviour moves over unchanged, and the GP quirk registry mirrors the Zigbee one. I checked out the head and ran the full suite (1487 passed), mypy zha/ (clean) and ruff check. Nothing blocking in the library itself; the main open question is the Home Assistant side, then a few small points.

Verified (5 checks)
  • Full pytest tests on head 2277c5f: 1487 passed. mypy zha/ and ruff check zha tests: clean.
  • _device_factory agrees with BaseDevice.new for every non-coordinator case (quirk factory, unquirked GPD, plain Zigbee), so the reinterview "same factory → rebuild in place" decision matches what new would have built.
  • GreenPowerDeviceMatch.matches: the IEEE prefix is compared most-significant-byte first (EUI64.serialize() is LSB-first, reversed here), checked against zigpy's synthetic_ieee.
  • The moved ZigbeeDevice methods match the old Device ones, and Device = ZigbeeDevice keeps existing imports and isinstance checks working.
  • The remaining .is_coordinator users in the repo (discovery.py, group.py) go through group members, which are always ZigbeeDevice.

Points to address

  • Home Assistant's use of gateway.devices. The lib now exposes GPDs through the same devices dict that HA iterates assuming Zigbee devices. For example ZHADeviceProxy.zha_device_info reads self.device.device.endpoints.items() unconditionally (HA zha/helpers.py:431), and websocket_get_groupable_devices reads device.device.is_groupable on every device (HA zha/websocket_api.py:261), which BaseDevice doesn't have. Not reachable today since zigpy 2.3.0 can't commission a GPD yet, but once one is registered the device list / groupable / bindable websocket calls would raise AttributeError. Is there (or will there be) a companion HA Core change, and does it need to land before GPDs can appear?
  • Gateway.get_device typing (gateway.py:722). It is still annotated Device | None, but _devices now also holds GreenPowerDevice, so a caller doing .endpoints or async_get_cluster on the result type-checks and raises AttributeError for a GPD's IEEE. mypy doesn't catch it because zha.application.gateway has arg-type / return-value / assignment disabled in the pyproject overrides; with that block removed, mypy reports exactly these new errors: get_device, the calls into _async_device_joined / _async_device_rejoined (gateway.py:820, :839, which take Device), and coordinator_zha_device = self.get_or_create_device(...) (gateway.py:303). Suggest BaseDevice | None for get_device and BaseDevice for the two helpers (inline comment on devices). The zigpy.device.ZigbeeDevice annotations on device_removed, device_left, async_device_initialized etc. could be loosened to BaseDevice at the same time, since zigpy now calls device_removed with a GPD.
  • GPD availability after a re-announce. _async_device_rejoined ends with zha_device.available = False ("async_initialize will be called later"), while for a GPD _check_available is a no-op and handle_message / async_update_device only see Zigbee packets. If zigpy announces an already-known GPD through device_initialized (rather than device_reinterviewed, which the PR assumes), it would stay unavailable until restart. Worth confirming how zigpy's commissioning will announce a known GPD.

Smaller points

  • GP registry order: register does insert(0, entry) (quirks.py:256), so the most recently registered matching entry wins, same as the Zigbee registry. The docstrings ("matched in registration order", "first registered entry") read as the opposite; I confirmed with two overlapping entries that the later one wins. The existing Zigbee match_entry docstring has the same wording, so this is only worth fixing if you touch it.
  • helpers.py:551: the availability loop used to skip any device whose node descriptor says coordinator and now skips only the active coordinator. A stale former-coordinator entry (e.g. after a radio swap, if it is still in gateway.devices) would now go through _check_available and can be marked unavailable. Fine if intended; just confirming.
  • GreenPowerDevice.get_diagnostics_json writes last_seen as a float / None where the Zigbee version writes an ISO string, and omits active_coordinator; probably fine for a new device type, but the shapes differ.
  • The PR body still says it separates the base platform entity types from ZCL by adding an intermediate subclass; that landed in #919, and this diff touches no platforms. A refreshed description would make the release notes accurate.

Comment thread zha/application/gateway.py
Comment thread zha/quirks.py
Comment thread zha/application/helpers.py Outdated
@puddly

puddly commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

Regarding the Home Assistant comment: there is a corresponding branch to integrate this into HA Core. There's no way to make this compatible with ZHA Core.

@zigpy-review-bot zigpy-review-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Re-review at e2f1809: the points from my earlier review are addressed, and I have nothing blocking.

Verified (5 checks)
  • Full pytest tests on head e2f1809: 1488 passed (one new test). mypy zha/ and ruff check / ruff format --check: clean, with the gateway / group / platforms override entries shrunk accordingly.
  • get_device now returns BaseDevice | None, and device_removed / device_initialized / async_device_initialized / _async_device_joined / _async_device_rejoined take BaseDevice. The gateway typing gap from before is closed.
  • The availability loop is back to if not dev.is_coordinator: BaseDevice.is_coordinator returns False, ZigbeeDevice reads the node descriptor. test_check_available_skips_stale_coordinator covers a former coordinator staying available.
  • GreenPowerDevice.get_diagnostics_json now writes last_seen as an ISO string and includes active_coordinator, matching the Zigbee shape (asserted in the test).
  • The GP registry docstrings now say the most recently registered match wins, which agrees with insert(0, entry).

Remaining notes

  • GPD availability after a re-announce (carried over from the first review): _async_device_rejoined ends with available = False, and nothing re-marks a GPD available if zigpy announces a known GPD through device_initialized rather than device_reinterviewed. Not reachable until zigpy can commission a GPD, so only worth confirming once that lands.
  • PR description is still out of date: the second paragraph says this PR separates the base platform entity types from ZCL with an intermediate subclass, which landed in #919, and this diff touches no platform classes. Updating it would keep the release notes accurate.
  • Thanks for the note on the Home Assistant side; that answers my earlier question.

@zigpy-review-bot zigpy-review-bot left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Third pass, again at e2f1809, this time with the companion work read: puddly/zgp-backbone on your zigpy fork, zigpy/zha-device-handlers#5273, and puddly/zha-gp on your Core fork. That settles my remaining question from last round, and makes the earlier generic "what about Home Assistant?" note concrete. Nothing blocking; three things I think are worth fixing before this lands, and the first is the only place the split looks unfinished.

Verified (8 checks)
  • Full pytest tests at e2f1809: 1488 passed. mypy zha/: clean. ruff check and ruff format --check: clean.
  • The override debt shrinks rather than moving around: zha.zigbee.device goes from 48 ignored errors to 18, zha.application.gateway from 10 to 2, zha.quirks drops out, repo total 245 to 185, and no module grew. The new total-count header in tools/regenerate_mypy_ignores.py makes that checkable at a glance.
  • The re-announce question from the last two rounds is settled by the zigpy branch. GreenPowerManager._finalize_commissioning announces a new GPD with listener_event("device_joined", device) followed by device_initialized(device), and one that is already registered through gp_device_recommissioned, which ends in listener_event("device_reinterviewed", new_device). So a known GPD never comes back through device_initialized, _async_device_rejoined is unreachable for one, and the rebuild re-runs GreenPowerDevice._init_from_zigpy_device, which sets _available = True again. Dropping that point.
  • Same branch: ControllerApplication.remove pops a GreenPowerDevice and fires device_removed, which this PR's handler now accepts as a BaseDevice. I exercised the other GPD paths in gateway.py against a real device — new join, re-commission with and without a quirk change, device_removed, async_remove_device, async_reinterview_device — and they all behave, including the factory-change branch that nothing tests.
  • Quirks 5273 lines up with the registry API: every symbol it imports exists, GreenPowerQuirkFactory.__call__(zigpy_device, gateway) matches how BaseDevice.new calls zha_device_factory, and the factory living on a module-level registry entry keeps _device_factory's is comparison stable across a re-commission.
  • GreenPowerDeviceMatch.matches byte order checked empirically: synthetic_ieee(src_id) is src_id.to_bytes(4, "little") plus a four-zero marker, so bytes(reversed(ieee.serialize())) gives 00:00:00:00:<SrcID MSB-first> and a vendor prefix such as the 04:cd:15 in 5273's builder test can never collide with a SrcID-addressed GPD. The SrcID-or-IEEE semantics and the per-field is not None checks are right.
  • _first_in_cluster and the endpoint loop moving to non_zdo_endpoints is equivalent to the old ep_id == 0 filter, and all ten NotImplementedError stubs left on BaseDevice are overridden on GreenPowerDevice, so none is reachable.
  • Findings 1 and 2 below were reproduced in the worktree against a real GreenPowerDevice (mypy for the first, AttributeError for the second); finding 3 is an argued hazard from reading the registry and the zhaquirks load order, not something I ran.

PlatformEntity is still typed to Device, so a Green Power entity cannot be typed

zha/application/platforms/__init__.py:532 takes device: Device, :567 stores self._device: Device, and :610 returns Device — all still ZigbeeDevice after the alias. The device hierarchy and the gateway were re-typed; the entity base was not.

The consequence that matters: on a Green Power entity, reveal_type(entity.device) is ZigbeeDevice, so entity.device.async_get_cluster(...) and entity.device.endpoints pass mypy and raise AttributeError at runtime. Every GP quirk entity gets the ZCL API offered to it by the type checker.

The other direction is narrower than it first looks, and worth stating precisely: passing a GreenPowerDevice to PlatformEntity.__init__ is an arg-type error (incompatible type "GreenPowerDevice"; expected "ZigbeeDevice"), but 5273's entities do not trip it, because they derive from BaseEvent, whose __init__(self, *args: Any, **kwargs: Any) erases the argument type. I checked both shapes: the forwarding one is clean, a GP entity deriving from PlatformEntity directly errors. So today it is latent, and it lands on the first GP quirk that wants something other than an event entity.

Neither shows up as a red CI check either: zha's mypy hook is scoped files: ^zha/ so tests/ is never checked, and in zha-device-handlers mypy runs without zha in scope, so PlatformEntity collapses to Any there. It is a gap in the typing model rather than a build break — but it is the one place where the new hierarchy stops halfway. Making PlatformEntity generic over the device type, or widening it to BaseDevice and keeping ZclPlatformEntity.device -> Device, would finish it.

Two Home Assistant endpoints fan out over every device and the Core branch does not cover them

puddly/zha-gp (one WIP commit, 95a0d00f) narrows four call sites: the endpoint loop in ZHADeviceProxy.zha_device_info, async_cluster_exists, async_get_actions and websocket_device_clusters. I walked the rest of the integration for the places that touch every device rather than one the user picked. There are four such scans, and the branch covers two of them — the device list (via zha_device_info) and the alarm-options check in zha/configuration (via async_cluster_exists). The two it does not cover are:

  • zha/devices/groupable filters device_proxies.values() on device.device.is_groupable, which moved to ZigbeeDevice, so the "Add group" page raises AttributeError: 'GreenPowerDevice' object has no attribute 'is_groupable'.
  • zha/devices/bindable filters the same dict through async_is_bindable_target, which lives in this repo (zha/application/helpers.py:233) and reaches target_zha_device.async_get_std_clusters() for any target whose nwk is not 0x0000 — a GPD's nwk is derived from its SrcID, so it falls through and the binding page fails with AttributeError: 'GreenPowerDevice' object has no attribute 'async_get_std_clusters'.

One commissioned GPD breaks both of those pages for every device, not just for the GPD. That second one and get_matched_clusters (helpers.py:127) are still annotated Device while Core feeds them values straight out of gateway.devices, which this PR widened to BaseDevice — the same shape as the get_device typing you already fixed, one layer out. Narrowing inside the two helpers would fix the bindable list and the GPD-as-target case of async_binding_operation from the library side; a GPD as the source of a bind still needs a Core guard, because the handler goes on to read source_device.device.zdo and zigpy's GreenPowerDevice has no zdo. An is_groupable returning False on BaseDevice would cover the groupable list the same way. Whether those defaults belong here or as isinstance checks in Core is your call, but the fan-out pair is what needs an answer before a GPD can be commissioned.

The remaining Zigbee-only Core call sites only fire when a request is aimed at the GPD itself — zha/devices/clusters/attributes, .../commands, .../attributes/value, zha/groups/bind and unbind, and the set_zigbee_cluster_attribute / issue_zigbee_cluster_command services — so they are unhandled tracebacks rather than broken pages. ZHADeviceProxy.__init__ and async_rebind_device (Core helpers.py:314, :323) are annotated Device too; that one is annotation-only, since the proxy itself only uses BaseDevice-safe attributes.

The Green Power registry has no specificity tiers, so import order decides which quirk wins

match_entry just above it walks explicit tiers — (manufacturer, model), then (manufacturer, None), then (None, model), then wildcard — so an entry naming both manufacturer and model beats one naming only one of them, whatever order they registered in, and registration order only breaks ties inside a tier. match_green_power_entry (quirks.py:319) is a plain first-match over _gp_registry, and the only ordering input is insert(0, entry) at :257. zhaquirks imports via pkgutil.walk_packages, which yields modules in sorted order, so among overlapping GP quirks the alphabetically-last module wins.

Concretely: GreenPowerQuirkBuilder.applies_to invites a quirk that declares only device_id=..., and device IDs such as OnOffSwitch are shared across vendors. Such a generic quirk placed in a late-sorting module silently takes precedence over a vendor-specific one — 5273's EnOcean quirk matches on src_id_range(0x01550000, 0x0155FFFF), so any GPD in that block that also reports a matching device ID would be decided by filename. Nothing logs that a more specific entry was passed over. test_green_power_quirk_resolution happens to register generic-then-specific, which is the lucky order; nothing covers the other one.

Ranking candidates before returning — an entry with an identity criterion (src_id_ranges or ieee_prefixes) over one without, then by number of specified fields — would make built-in ordering independent of filenames. Note that it changes the custom-quirk story: today a custom quirk wins purely because it registers last, and under a ranking it would need an explicit precedence rule to keep beating a more specific built-in. Easier to settle now than once quirks depend on the current behaviour.

Smaller

  • Group.async_add_members / async_remove_members (group.py:298, :316) call get_zigbee_device, whose assert isinstance(device, Device) (gateway.py:752) is the only guard. Core's zha/group/members/add and zha/group/members/remove pass the request's members straight through without going via _is_groupable_member, so a GPD IEEE in that payload surfaces as a bare AssertionError with no message; under python -O it becomes AttributeError on the missing async_add_endpoint_to_group instead. A ZHAException, or skipping non-Zigbee members the way _is_groupable_member does, would be easier to act on. Group.members (group.py:195) has the same shape.
  • A GPD is available forever: _init_from_zigpy_device forces _available = True and _check_available is a no-op. That is a reasonable default for a device with no heartbeat, but zigpy does maintain last_seen for a GPD — it is set on every accepted GPDF and persisted next to the frame counter — so the data for a consider-unavailable heuristic exists and is deliberately unused. Worth a sentence in the PR either way, since it is the kind of thing the next person will try to "fix".
  • Related, and the reason I noticed: GreenPowerDevice.get_diagnostics_json (device.py:2060) is self.device._last_seen.isoformat(), but zigpy's BaseDevice._last_seen is datetime | None and the restore path only assigns it when the stored value is greater than zero, so None is inside the contract. On a GPD that has never been heard from, "Download diagnostics" raises AttributeError: 'NoneType' object has no attribute 'isoformat'; test_green_power_device_info only passes because it assigns last_seen first. Commissioning does always set it, so I could not produce a real path there — but the same None makes device_info report a last_seen of now (time.localtime(None) is the current time), which is a quieter wrong answer. Note the public last_seen is a float, so the fix is datetime.fromtimestamp(self.last_seen, UTC) with a None branch rather than self.last_seen.isoformat(). The Zigbee version at :1798 has the same expression, if you would rather do both at once.
  • With no endpoint and no cluster in it, a GP entity's base unique ID is just the IEEE, so two entities of one quirk on the same platform collide unless their unique_id_suffix values differ. 5273's PTM 216Z sets one per button, and GreenPowerQuirkBuilder.entity() does not require it; when it is missing the extra entity disappears into _add_pending_entities (device.py:918, branch at :935) with no log. Hard to warn on as the code stands — that branch is load-bearing on the normal path, since async_configure and async_initialize both call _discover_new_entities and only the second drains the queue, so every device reaches it with a duplicate. Not a regression, just newly easy to hit; requiring the suffix in the builder is probably the cheaper end to fix.
  • is_coordinator is a cached_property on BaseDevice but is not in BaseDevice._ZIGPY_CACHED_PROPERTIES; ZigbeeDevice had to add it to its own tuple precisely because it overrides the property. Harmless while the base returns a constant, but it is the one gap in an otherwise exhaustive scheme.
  • _device_factory's docstring (gateway.py:201) says it returns the class Device.new creates, but omits new's first branch, the CoordinatorDevice check. I walked the coordinator re-interview combinations and none produces a wrong object, since the IEEE is identical on both sides of the comparison — so just the docstring.

@zigpy-review-bot zigpy-review-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Delta review of e36ab67..7b29ead on top of my last pass at e2f1809. Nearly everything I raised is addressed; one new bug in the specificity ranking, nothing else.

Verified (6 checks)
  • Full pytest tests at 7b29ead: 1490 passed. mypy zha/ clean, ruff check clean.
  • PlatformEntity is now typed to BaseDevice, with ZclPlatformEntity.device and RSSISensor.device narrowed back to Device, which closes the typing gap I described.
  • BaseDevice.is_groupable (False) and async_get_std_clusters() ({}) cover the groupable and bindable fan-out pages from the library side. async_is_bindable_target returns False early for a source with no clusters, and get_matched_clusters / async_is_bindable_target are typed for BaseDevice. test_green_power_device_not_zigbee exercises each combination.
  • get_zigbee_device raises ZHAException("... is not a Zigbee device") instead of asserting.
  • is_coordinator is now in BaseDevice._ZIGPY_CACHED_PROPERTIES, and the _device_factory docstring is corrected.
  • GP quirk ranking: identity criterion first, then field count, and the stable sort keeps the newest entry ahead among equals. The new test covers both registration orders.

specificity counts a device_id of 0 as unspecified

specificity uses map(bool, (self.device_id, self.manufacturer_id, self.model_id)), but matches (just above) treats these fields as set whenever they are is not None. DeviceID.SimpleGenericOneStateSwitch is 0, a valid Green Power device ID, and a manufacturer or model ID of 0 is equally legal. So GreenPowerDeviceMatch(device_id=0) ranks (False, 0), the same as a wildcard GreenPowerDeviceMatch().

I checked it: GreenPowerDeviceMatch(device_id=0).specificity == GreenPowerDeviceMatch().specificity == (False, 0), and device_id=0, manufacturer_id=0 is also (False, 0). A quirk for a one-state switch therefore ties with a catch-all and falls back to registration order against it. That is the filename-order dependence this ranking was added to remove, just for the one device ID that is 0.

Counting is not None fields, as matches does, fixes it. A test with device_id=DeviceID.SimpleGenericOneStateSwitch registered before a wildcard entry would pin it, since the existing test only uses OnOffSwitch (2).

Comment thread zha/quirks.py Outdated

@zigpy-review-bot zigpy-review-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Delta review of 7b29ead..744d324: the specificity bug from my last review is fixed, and I have nothing further. Approving.

Verified (4 checks)
  • specificity now counts is not None fields, matching matches, and returns a single int (10 * has_identity + num_fields). The only consumer is the sort key in DeviceRegistry, which works the same with an int. Identity still outranks any field count, since at most 3 fields can be set.
  • The new assertion pins GreenPowerDeviceMatch(device_id=0).specificity > GreenPowerDeviceMatch().specificity.
  • tests/test_green_power_device.py: 14 passed at 744d324. mypy zha/quirks.py and ruff check are clean.
  • All five review threads are already resolved, so there was nothing to resolve or reply to.

@TheJulianJES

TheJulianJES commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

@zigpy-review-bot Review everything again, not just delta. After that, possibly also push extra/new branch that improves test coverage and link it in comment, so we can see whether to add here, or possibly in a future PR. Do everything with review rounds.

@zigpy-review-bot zigpy-review-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Fresh full re-review at 744d324, as asked. The refactor holds up, but there is one real bug in the new GPD code and a stale generated count. Nothing here blocks merging if you'd rather fix them in a follow-up.

Needs a fix

  • GreenPowerDevice.get_diagnostics_json raises AttributeError for a GPD that has no last_seen yet (inline comment). The pyproject override this PR regenerates hides it, and the header count # Total ignored errors: 182 across 20 modules is stale as a result: re-running python -m tools.regenerate_mypy_ignores after the fix should match it. Currently it gives 184 and union-attr 15 for zha.zigbee.device.

Worth knowing

  • async_is_bindable_target now returns False before reaching the target.nwk == 0x0000 branch when the source has no std clusters. A Zigbee device whose endpoints are all non-ZHA/ZLL profile is therefore no longer offered the coordinator as a bind target. That looks right, but it is a behaviour change outside the GP scope and the PR body doesn't mention it.
  • BaseDevice defaults async_get_std_clusters() and is_groupable, but async_get_clusters() stays on ZigbeeDevice only. HA Core's async_cluster_exists calls it for every non-coordinator device in gateway.devices, so a GPD would raise there unless the Core side guards it. A return {} default on BaseDevice would close that on the library side.

Coverage

codecov/patch is failing. I pushed a branch with extra tests so you can decide whether to take them here or in a later PR: https://github.com/zigpy/zha/tree/zigpy-bot/zgp-test-coverage (one commit on top of this PR's head, tests only, tests/test_green_power_device.py).

It covers the GPD gateway lifecycle (join, leave, removal, re-commissioning that swaps the resolved factory), BaseDevice defaults, bindability of cluster-less sources and the remaining quirk-registry branches (zha/quirks.py goes to 100%). It also adds a strict xfail for the last_seen bug above, so it will tell you to drop the marker once that is fixed. The full suite passes on the branch (1504 passed, 1 xfailed).

Verified
  • Full suite at 744d324: 1490 passed. mypy zha/ and ruff check / ruff format --check are clean.
  • zigpy 2.4.0 (pinned, lock unchanged) already ships GreenPowerDevice, non_zdo_endpoints and GreenPowerManager, so no bump is needed.
  • The device.py split into BaseDevice / ZigbeeDevice is behaviour-preserving: each moved method was compared against its old body, the cached-property invalidation split is correct, and the Device = ZigbeeDevice alias keeps HA Core and zhaquirks imports working.
  • Registry ordering is the newest entry among those with the highest specificity, and specificity counts is not None fields exactly like matches. purge_custom_quirks, remove and __iter__ cover the GP registry.
  • GPD join, leave, removal and re-commissioning paths were driven by hand and behave like the Zigbee ones.
  • The mypy override debt shrinks overall (device 48→18, gateway 10→2, helpers 16→7), and no module grew.

Comment thread zha/zigbee/device.py Outdated
@zigpy-review-bot

This comment was marked as resolved.

zigpy-review-bot and others added 2 commits October 26, 2026 22:17
Cover the GPD gateway lifecycle (join, leave, removal, re-commissioning), BaseDevice defaults, bindability of sources without clusters and the remaining quirk registry branches. Adds a strict xfail for GreenPowerDevice.get_diagnostics_json on a GPD that has never been seen.
@TheJulianJES

This comment was marked as resolved.

@zigpy-review-bot

This comment was marked as resolved.

Active-coordinator availability check, ZDO endpoint skip in default entity
discovery and GPD diagnostics entity serialization.
Comment thread zha/zigbee/device.py

@TheJulianJES TheJulianJES left a comment

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.

Looks awesome!

@puddly
puddly merged commit 04a4223 into zigpy:dev Oct 27, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new feature Adds new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Green Power (ZGP) support

4 participants