From f593f1d5e9d79c98cf3cad7b62ca27d27fb07f4d Mon Sep 17 00:00:00 2001 From: David Mulcahey Date: Sun, 15 Mar 2026 19:49:48 -0400 Subject: [PATCH] Initial attempt at device scanning --- .../2026-03-15-raw-device-scan-design.md | 814 ++++ docs/plans/2026-03-15-raw-device-scan.md | 542 +++ docs/plans/device_scan.log | 1050 +++++ docs/plans/device_scan_explanation.md | 420 ++ tests/test_appdb.py | 134 + tests/test_appdb_migration.py | 111 +- tests/test_application.py | 6 + tests/test_device.py | 192 + tests/test_device_scanner.py | 3911 +++++++++++++++++ zigpy/appdb.py | 1144 ++++- zigpy/appdb_schemas/schema_v15.sql | 295 ++ zigpy/application.py | 6 +- zigpy/device.py | 103 +- zigpy/device_scanner.py | 1962 +++++++++ zigpy/endpoint.py | 106 +- zigpy/ota/image.py | 24 +- zigpy/zcl/clusters/general.py | 22 +- zigpy/zcl/clusters/greenpower.py | 41 +- zigpy/zdo/types.py | 5 +- 19 files changed, 10789 insertions(+), 99 deletions(-) create mode 100644 docs/plans/2026-03-15-raw-device-scan-design.md create mode 100644 docs/plans/2026-03-15-raw-device-scan.md create mode 100644 docs/plans/device_scan.log create mode 100644 docs/plans/device_scan_explanation.md create mode 100644 tests/test_device_scanner.py create mode 100644 zigpy/appdb_schemas/schema_v15.sql create mode 100644 zigpy/device_scanner.py diff --git a/docs/plans/2026-03-15-raw-device-scan-design.md b/docs/plans/2026-03-15-raw-device-scan-design.md new file mode 100644 index 000000000..561021128 --- /dev/null +++ b/docs/plans/2026-03-15-raw-device-scan-design.md @@ -0,0 +1,814 @@ +# Raw On-Wire Device Scan Design + +**Date:** 2026-03-15 + +**Goal:** Add an on-demand zigpy device scan that performs a full raw on-wire inventory of a device, stores the results durably in zigpy's database, can resume from the last completed scan step, and never mutates live runtime cache or event state. + +## Requirements + +- On-demand only. This is not part of `Device.initialize()` or normal rejoin flow. +- Persist the full scan inventory durably in the database. +- Store the raw on-wire view, not the quirked view. +- Keep the first iteration non-invasive at runtime. +- Scan surface for v1: + - raw descriptors + - discovered attributes via `Discover_Attribute_Extended`, falling back to `Discover_Attributes` when needed + - readable attribute values + - discovered received/generated commands +- Preserve progress and resume from the last completed scan step. +- Default behavior is `resume`; `force_full` is explicit. +- Reject `resume=True` plus `force_full=True` as invalid input before queue classification. +- Use one fixed internal scan deadline in v1. It is not configurable. +- Use stable `error_code` values alongside human-readable error text. +- Store one canonical persisted attribute value representation plus datatype metadata. +- Keep manufacturer-specific scanning raw-only. If raw manufacturer code is absent, expose an explicit skipped reason instead of silently omitting that scope. + +## Research Summary + +### zha-toolkit behavior + +The reference implementation in `zha-toolkit` is an active diagnostic walker: + +- It walks all endpoints except endpoint `0`. +- It skips endpoint `242`. +- It scans input and output clusters. +- It discovers attributes with `discover_attributes_extended()` in pages of 16, falling back to `discover_attributes()` when the extended command is unsupported. +- It reads readable attributes in chunks of 4. +- It optionally does a second manufacturer-scoped attribute pass using `ep.manufacturer_id`. +- It discovers received/generated commands in pages of 16. +- It writes a JSON artifact, not durable normalized state. + +The useful ideas are the paging model, small read batches, and best-effort retry behavior. + +The parts zigpy should not copy directly are: + +- It uses `cluster.read_attributes()`, which updates cache/events and cannot read unknown discovered attribute IDs safely in current zigpy. +- It does not persist normalized, resumable state. +- It does not do manufacturer-scoped command discovery. +- It appears to pass `is_server=True` even for output clusters, which makes command direction handling unreliable. + +### zigpy behavior + +zigpy already has strong primitives, but not a complete device scanner: + +- `Device._initialize()` discovers node descriptor, active endpoints, simple descriptors, model/manufacturer, and an OTA attribute. +- `ControllerApplication.device_initialized()` persists the raw device view before quirks are applied. +- `PersistingListener` already stores: + - devices + - endpoints + - clusters + - node descriptors + - attribute cache + - unsupported attributes +- `Topology` is still a useful service-pattern reference for: + - task ownership + - retries + - request pacing + - explicit scan API + +The key gaps are: + +- No on-demand device scan orchestrator. +- No database model for discovered command inventory, attribute metadata, scan checkpoints, or partial completion. +- No non-invasive raw read path that persists scan results separately from `attributes_cache`. +- No DB-backed read model for scan snapshots. + +## Architectural Choice + +Use a dedicated on-demand raw scan service. + +Rejected alternatives: + +1. Extend normal init and reuse live clusters plus `attributes_cache`. + This violates the non-invasive and raw-view requirements. + +2. Scan through a detached raw `Device` clone. + This is incorrect in zigpy because request/response matching is attached to the live device instance used by the application. + +Chosen approach: + +- Add a `DeviceScanner` service owned by `ControllerApplication`. +- Use the live device only as the transport anchor for request/response matching. +- Extract a shared raw descriptor discovery helper from normal init and reuse it for scanner refresh instead of duplicating descriptor walk logic. +- Treat refreshed raw descriptor tables in the database as the canonical scan-plan source. +- Persist scan inventory and scan progress in dedicated database tables. +- Never write scan results into the live runtime attribute cache in v1. + +The shared descriptor helper is intentionally not local to `device_scanner.py`. This design expects the extraction to touch existing init code in `zigpy.device` and `zigpy.endpoint` so join-time descriptor behavior and scan-time descriptor behavior cannot drift apart. + +## Raw View Strategy + +The scan must operate on the raw on-wire topology, not the quirked topology. + +Important implication: + +- The live device object in `app.devices` may be quirked and may add/remove/replace endpoints and clusters. +- The scan plan must therefore be built from raw descriptor data, not the live quirked endpoint graph. + +Use this split: + +- **Canonical raw topology source:** refreshed raw descriptor tables in the database +- **Bootstrap fallback:** `device.original_signature` only before raw tables exist +- **Transport source:** the live application device instance + +This avoids the broken detached-device pattern while keeping the scan raw. + +## Public API + +### `ControllerApplication.device_scanner` + +A new explicit-use service similar in role to `Topology`. + +Responsibilities: + +- option validation +- task dedupe +- global scan concurrency +- resume and `force_full` +- progress events +- persistence coordination +- DB-backed snapshot assembly + +### `DeviceScanner.scan()` + +```python +await app.device_scanner.scan( + ieee, + resume=True, + force_full=False, +) +``` + +Behavior: + +- default `resume=True` +- every scan refreshes raw descriptors first +- `force_full=True` clears all scan state for the device first +- `resume=True` and `force_full=True` together raise `InvalidScanOptionsError` before queue classification +- the scan deadline is a fixed internal 120 seconds and non-configurable in v1 +- `scan()` returns a fixed `DeviceScanSummary` contract, not the full snapshot + +### `DeviceScanSummary` + +`scan()` returns a small fixed `DeviceScanSummary` type. + +Fields: + +- `ieee` +- `completed` +- `outcome` +- `used_resume` +- `force_full` +- `descriptor_refresh_performed` +- `last_finished` +- `error_code` +- `last_error` + +Meaning: + +- `completed=True` means this run reached a terminal state, not that every scope succeeded +- `outcome` is one of `success`, `partial`, or `failed` +- callers and tests must key off `error_code`, not `last_error` text + +### `DeviceScanner.get_snapshot()` + +```python +snapshot = await app.device_scanner.get_snapshot(ieee) +``` + +Behavior: + +- `get_snapshot()` is a pure DB-backed read API and does not require the live device to exist +- if no persisted raw descriptor rows exist for the IEEE, raise `DeviceScanSnapshotNotFoundError` +- if persisted raw descriptor rows exist but scan rows do not, return a valid empty snapshot with materialized scopes and empty `progress`, `attributes`, and `commands` +- partial scans return partial progress, attributes, and commands without consulting live runtime objects + +### `DeviceScanSnapshot` + +Top-level fields: + +- `ieee` +- `raw_node_descriptor` +- `last_snapshot_at` +- `endpoints` + +Hierarchy: + +```text +DeviceScanSnapshot + | + +--> ieee + +--> raw_node_descriptor + +--> last_snapshot_at + `--> endpoints[] + | + +--> endpoint_id + +--> profile_id + +--> device_type + `--> clusters[] + | + +--> cluster_id + +--> cluster_type + +--> standard + | +--> progress + | +--> attributes[] + | `--> commands[] + | + `--> manufacturer_specific + +--> manufacturer_code + +--> progress + +--> attributes[] + `--> commands[] +``` + +Materialization rules: + +- `standard` is always materialized +- `manufacturer_specific` is always materialized so manufacturer-scope visibility is explicit +- when the refreshed raw node descriptor has a manufacturer code, `manufacturer_specific` behaves like a normal scan scope +- when the refreshed raw node descriptor has no manufacturer code: + - `manufacturer_specific.manufacturer_code` is `None` + - `manufacturer_specific.progress.status` is `skipped` + - `manufacturer_specific.progress.error_code` is `missing_raw_manufacturer_code` + - `manufacturer_specific.attributes` and `manufacturer_specific.commands` are empty + - this skipped scope is synthesized during event and snapshot assembly from raw descriptor state; it does not require a persisted progress row + +Ordering: + +- endpoints ordered by `endpoint_id` ascending +- clusters ordered by `cluster_type`, then `cluster_id` ascending +- attributes ordered by `attr_id` ascending +- commands ordered by `direction`, then `command_id` ascending + +Assembly rule: + +- seed the endpoint and cluster hierarchy from raw descriptor rows in one pass +- attach progress, attributes, and commands from scan rows in one pass +- each snapshot attribute exposes `datatype`, the canonical stored raw value, and an optional decoded value +- decode the canonical stored attribute value into snapshot output during assembly +- resolve decode metadata once per cluster or type and reuse it while decoding attribute values in that single assembly pass +- never re-query or re-scan per cluster or scope during snapshot assembly + +### Public errors + +Public scanner errors are fixed named types, not message-based behavior. + +- `InvalidScanOptionsError` for invalid option combinations such as `resume=True` plus `force_full=True` +- `ScanInProgressError` for conflicting queued or running same-device requests +- `DeviceScanTargetMissingError` when a queued scan reaches execution after the target device was removed +- `DeviceScanSnapshotNotFoundError` when `get_snapshot()` has no persisted raw descriptor rows to read from + +## Progress Event Contract + +Progress events are intentionally small. + +Listeners attach to `app.device_scanner`, following the same `ListenableMixin` pattern used by `Topology`. + +Event names: + +- `scan_queued` +- `scan_started` +- `step_started` +- `step_finished` +- `scan_finished` + +Event payload fields: + +- `ieee` +- `status` +- optional `outcome` +- optional `step` +- optional scope: `endpoint_id`, `cluster_id`, `cluster_type`, `manufacturer_code_scope` +- optional `error_code` +- optional `error` + +Allowed statuses: + +- `queued` +- `started` +- `success` +- `failed` +- `skipped` + +Cardinality: + +- `scan_queued`, `scan_started`, and `scan_finished` are scan-level events +- `scan_finished` may include terminal `outcome=success|partial|failed` +- `step_started` and `step_finished` are used for both descriptor refresh and per-scope inventory work +- descriptor refresh uses `step="descriptor_refresh"` and does not include scope fields +- inventory step events are emitted per scope and include `endpoint_id`, `cluster_id`, `cluster_type`, and `manufacturer_code_scope` +- manufacturer-scope gating uses `status="skipped"` and `error_code="missing_raw_manufacturer_code"` + +## Error-Code Vocabulary + +The scanner layer defines one fixed error-code vocabulary used by summary, events, and persisted scan-state fields. + +V1 error codes: + +- `invalid_scan_options` +- `scan_in_progress` +- `device_scan_target_missing` +- `descriptor_refresh_failed` +- `scan_deadline_exceeded` +- `unsupported_discovery_command` +- `transport_failure` +- `attribute_unsupported` +- `missing_raw_manufacturer_code` + +`error_code` is the machine-readable contract. `last_error` and event `error` remain human-readable diagnostic text only. + +For attribute-read rows: + +- `read_status` is the protocol or result-category field +- `last_error_code` is the scanner error-contract field +- terminal protocol outcomes may have no `last_error_code` +- transport or scanner failures must set `last_error_code` +- `read_complete` is a derived query-optimization flag and must remain consistent with `read_status` + +## Scheduling And Task Ownership + +V1 scheduler rules: + +- only one device scan runs globally at a time +- different-device requests wait in FIFO order for the global slot +- a queued same-device request counts as in-progress for dedupe and conflict purposes +- exact duplicate same-device requests reuse the queued or running task +- conflicting same-device requests raise `ScanInProgressError` +- the shared scan task is owned by `DeviceScanner`, not any single caller +- caller cancellation detaches only that caller's wait and does not cancel shared queued or running scan work +- every shared scan task runs inside one fixed internal deadline wrapper +- deadline expiry releases the global slot and preserves already committed rows +- deadline expiry is `partial` only if the timed-out run committed new scan rows before expiry; otherwise it is `failed` + +```text +caller request + | + +--> validate options + | `--> invalid -> InvalidScanOptionsError + | + +--> classify request + | +--> same-device exact duplicate -> attach to shared task + | +--> same-device conflict -> ScanInProgressError + | `--> different device -> FIFO queue + | + `--> run shared task with deadline + | + +--> success -> release slot -> wake next queued scan + +--> failed -> release slot -> wake next queued scan + `--> timeout -> persist partial state -> release slot -> wake next queued scan +``` + +## Persistence Surface + +Keep database writes in `zigpy.appdb.PersistingListener`, but add explicit async methods for the device scan service and low-level DB-backed snapshot read helpers. + +This keeps schema ownership in one place while avoiding the normal event-driven attribute cache path. + +Callers read snapshots only through `DeviceScanner`; `appdb` returns low-level scan rows and `DeviceScanner` assembles the public `DeviceScanSnapshot`. + +The scanner layer also defines: + +- one shared internal step and status vocabulary used by progress events, summary assembly, and snapshot assembly +- one shared internal scope identity helper used by scan execution, event emission, and snapshot assembly +- one shared raw descriptor discovery helper reused by both normal init and scanner refresh + +## Database Model + +Do not duplicate existing raw descriptor tables. Reuse: + +- `devices_vN` +- `endpoints_vN` +- `clusters_vN` +- `node_descriptors_vN` + +Add new tables: + +### `device_scan_progress_v15` + +Keyed by: + +- `ieee` +- `endpoint_id` +- `cluster_type` +- `cluster_id` +- `manufacturer_code_scope` + +Stores: + +- `attr_discovery_complete` +- `attr_discovery_next_id` +- `attr_reads_complete` +- `cmd_rx_complete` +- `cmd_rx_next_id` +- `cmd_tx_complete` +- `cmd_tx_next_id` +- `last_started` +- `last_finished` +- `last_error_code` +- `last_error` +- `last_success` + +Purpose: + +- resumable paging +- resumable read progress +- stable machine-readable error reporting + +Secondary indexes: + +- `idx_device_scan_progress_v15_ieee` on `(ieee)` for per-device clears and snapshot reads + +### `device_scan_attributes_v15` + +Keyed by: + +- `ieee` +- `endpoint_id` +- `cluster_type` +- `cluster_id` +- `manufacturer_code_scope` +- `attr_id` + +Stores: + +- raw attribute identifier +- optional resolved attribute name +- datatype +- ACL bitmap +- discovery timestamp +- read completion flag +- read status +- one canonical raw persisted value field +- last read timestamp +- `last_error_code` +- `last_error` + +Notes: + +- Keep the scope manufacturer code in the key because standard and manufacturer-scoped discovery can both surface the same attribute ID. +- Use the same NULL-safe uniqueness approach zigpy already uses for `manufacturer_code`. +- `read_complete` is a query-oriented derived flag and must stay consistent with `read_status`. +- `read_status` captures protocol or result status; `last_error_code` captures scanner failure semantics when applicable. +- Do not store both raw bytes and a normalized text or JSON rendering. Snapshot assembly derives optional decoded output from the canonical stored value. + +Secondary indexes: + +- `idx_device_scan_attributes_v15_ieee` on `(ieee)` for per-device clears and snapshot reads +- `idx_device_scan_attributes_v15_pending_reads` on the existing NULL-safe manufacturer-scope index shape plus `(ieee, endpoint_id, cluster_type, cluster_id, read_complete, attr_id)` ordering for pending-readable-attribute selection per scope + +### `device_scan_commands_v15` + +Keyed by: + +- `ieee` +- `endpoint_id` +- `cluster_type` +- `cluster_id` +- `manufacturer_code_scope` +- `direction` +- `command_id` + +Stores: + +- raw command identifier +- direction (`received` or `generated`) +- optional resolved command name +- optional resolved schema text +- discovery timestamp + +Notes: + +- Name and schema are annotations from zigpy's known cluster definitions, not treated as raw on-wire truth. + +Secondary indexes: + +- `idx_device_scan_commands_v15_ieee` on `(ieee)` for per-device clears and snapshot reads + +## Scan Algorithm + +Every scan refreshes raw descriptors first, then inventories refreshed raw cluster scopes. + +```text +scan request + | + +--> validate options + | `--> invalid -> InvalidScanOptionsError + | + +--> classify request + | +--> same-device exact duplicate -> reuse task + | +--> same-device conflict -> ScanInProgressError + | `--> different device -> FIFO wait + | + +--> emit scan_queued + | + `--> run shared scan task inside deadline + | + +--> emit scan_started + | + +--> refresh raw descriptors + | +--> step_started(descriptor_refresh) + | +--> shared raw descriptor discovery helper + | +--> atomically replace raw descriptor rows + | +--> clear removed scan scopes + | `--> step_finished(descriptor_refresh, success|failed) + | + +--> for each refreshed raw endpoint/cluster + | +--> standard scope + | | +--> discover attrs + | | +--> read readable attrs + | | +--> discover rx commands + | | `--> discover tx commands + | | + | `--> manufacturer_specific scope + | +--> raw manufacturer code present -> scan normally + | `--> raw manufacturer code absent + | +--> mark scope skipped + | `--> emit step_finished(..., skipped, missing_raw_manufacturer_code) + | + `--> emit scan_finished(success|partial|failed) +``` + +### Step 0: refresh raw descriptors + +- Always request the node descriptor first. +- Request active endpoints with `Active_EP_req`. +- Request each endpoint's simple descriptor with `Simple_Desc_req`. +- Build the refreshed raw descriptor set in memory first. +- Replace the device's raw descriptor rows atomically only after the full refresh succeeds. +- Clear scan rows and progress rows for scopes that disappeared from the refreshed raw topology. +- If descriptor refresh fails, abort the scan before attribute or command inventory begins and leave the prior canonical raw rows untouched. + +### Step 1: choose scan scopes + +Only materialize scan scopes for refreshed raw endpoints other than endpoint `0` and endpoint `242`. + +For every refreshed raw cluster: + +1. Materialize the `standard` scope with `manufacturer_code_scope = NULL` +2. Materialize the `manufacturer_specific` scope + - if refreshed raw node descriptor has a non-`None` manufacturer code, scan it normally + - otherwise synthesize it as `skipped` with `error_code = missing_raw_manufacturer_code` and no persisted scan rows + +### Step 2: discover attributes + +- Use one shared private page runner for all page-driven discovery steps. +- Page in batches of 16, matching the reference behavior. +- Try `Discover_Attribute_Extended` first so ACL metadata is preserved when the device supports it. +- Discovery default responses may arrive either as the legacy `(GeneralCommand.Default_Response, status)` tuple or as a decoded `foundation.DefaultResponse` object; normalize both forms before making control-flow decisions. +- Some devices reply to output-cluster requests with the wrong ZCL direction bit; request/response matching must still treat those actual response frames as replies instead of letting them fall through to transport timeouts. +- Wrong-direction non-response traffic must not satisfy pending requests just because the TSN matches. +- If the device replies to `Discover_Attribute_Extended` with default-response `UNSUP_GENERAL_COMMAND`, log it and retry the same page with `Discover_Attributes`. +- If the manufacturer-scoped pass replies with default-response `UNSUP_MANUF_GENERAL_COMMAND`, treat it as the manufacturer-specific analogue of unsupported discovery: log it and retry the same page with `Discover_Attributes`. +- If `Discover_Attributes` also replies with default-response `UNSUP_GENERAL_COMMAND`, log it, persist an empty completed page for the scope, and continue the scan. +- If the manufacturer-scoped fallback also replies with default-response `UNSUP_MANUF_GENERAL_COMMAND`, log it, persist an empty completed page for the scope, and continue the scan. +- Any other discovery default-response status is terminal for that scope and produces the normal partial-scan failure path. +- Per-request transport timeouts still use zigpy's normal reply timeouts and surface as scope-local `transport_failure`, not `scan_deadline_exceeded`. +- Persist each successful page and its progress cursor update in the same transaction. +- Advance the stored cursor only after the page rows are committed. +- Mark the step complete only after the terminating page is processed. +- Persist ACL metadata only when extended discovery succeeds; standard discovery pages leave ACL as `NULL`. + +### Step 3: read readable attributes + +- Load the pending readable target set from stored attribute rows once per cluster scope after discovery completes. +- Use `read_attributes_raw()`, never `read_attributes()`. +- Default read batch size is fixed at 3 in v1. +- `read_attributes_raw()` default-response failures may arrive either as the legacy tuple form or as a decoded `foundation.DefaultResponse` object; normalize both forms before mapping terminal per-attribute statuses. +- If a `ReadAttributesResponse` omits one or more requested attribute IDs, log the missing IDs, retry only the missing IDs one-by-one, then persist retryable `transport_failure` rows only for any IDs that are still missing after the one-by-one retry. Continue the remaining read batches for the scope and surface the scope as a partial transport failure instead of aborting the scan. +- On whole-batch transport failure: + - retry with backoff + - split the batch into lower and upper halves + - continue binary splitting until single-attribute reads +- Drive split-and-retry from the in-memory target set for the current scope instead of re-querying the database for every fallback. +- Persist each successful read batch in its own transaction. +- Terminal device responses such as unsupported attribute are marked complete for that attribute and use the fixed `attribute_unsupported` error code where appropriate. +- Transport failures remain retryable and do not mark the attribute complete. + +### Request pacing + +- Apply a small fixed internal pacing delay between outbound scan requests in v1. +- Reuse the same pacing rule for descriptor refresh, discovery pages, and raw reads. +- Keep pacing internal and non-configurable in v1. +- Retry-time backoff is in addition to the baseline pacing rule. + +### Step 4 and 5: discover commands + +- Use the same shared private page runner used by attribute discovery. +- Page in batches of 16. +- Persist each successful page and its progress cursor update in the same transaction. +- Track separate cursors for received and generated directions. +- Use correct cluster role handling for output clusters. +- Output-cluster response frames with the wrong ZCL direction bit must still resolve the pending request so command discovery does not false-time out, but wrong-direction non-response traffic must not. +- Support manufacturer-scoped discovery only when the refreshed raw node descriptor from this scan provides a non-`None` manufacturer code. +- If manufacturer code is absent, represent the skipped scope explicitly rather than silently omitting it. +- If received or generated command discovery replies with default-response `UNSUP_GENERAL_COMMAND`, in either tuple or decoded `foundation.DefaultResponse` form, log it, persist an empty completed page for that direction, and continue the scan. +- If manufacturer-scoped command discovery replies with default-response `UNSUP_MANUF_GENERAL_COMMAND`, in either tuple or decoded `foundation.DefaultResponse` form, log it, persist an empty completed page for that direction, and continue the scan. +- Any other command-discovery default-response status is terminal for that scope and does not fall through to normal page parsing. +- If a manufacturer-scoped command-discovery page times out, retry that same page once with a short backoff before surfacing the normal scope-local `transport_failure`. + +## Resume, `force_full`, And Deadlines + +Default resume behavior: + +- if a cluster scope has completed a step, skip that step +- if attribute discovery completed but attribute reads did not, resume reads only +- if command discovery partially completed, continue from the stored cursor + +`force_full` behavior: + +- clear all scan rows and progress rows for the device first +- keep the current raw descriptor rows until descriptor refresh succeeds +- refresh descriptors and rescan from zero + +Deadline behavior: + +- the deadline wraps the whole shared scan task, not just individual requests +- the fixed deadline is 120 seconds in v1 +- underlying per-request reply timeouts are independent of the 120-second scan wrapper and do not imply deadline expiry +- deadline expiry preserves already committed rows +- deadline expiry releases the global queue slot +- deadline expiry returns `partial` only if the timed-out run committed new scan rows before expiry; otherwise it returns `failed` +- deadline expiry returns a terminal summary with `error_code=scan_deadline_exceeded` and human-readable `last_error` + +## Runtime Side Effects + +The first iteration is intentionally non-invasive. + +Must not: + +- update live `attributes_cache` +- emit normal attribute read or update events +- overwrite live model, manufacturer, or other runtime device state +- overwrite the live device endpoint or cluster dictionaries during scan execution + +Allowed: + +- sending active ZDO and ZCL requests for scanning +- emitting only the fixed scan-specific progress events described above + +Fast polling is not required for v1. It can be added later as an explicit scan option if needed. + +## Snapshot Materialization Diagram + +```text +raw cluster row + | + +--> standard scope + | +--> progress from progress rows + | +--> attributes from attribute rows + | `--> commands from command rows + | + `--> manufacturer_specific scope + | + +--> raw manufacturer code present + | +--> manufacturer_code= + | +--> progress from progress rows + | +--> attributes from attribute rows + | `--> commands from command rows + | + `--> raw manufacturer code absent + +--> manufacturer_code=None + +--> progress.status=skipped + +--> progress.error_code=missing_raw_manufacturer_code + +--> attributes=[] + `--> commands=[] +``` + +## Testing Strategy + +Minimum required tests: + +- progress listeners attach to `app.device_scanner`, not `ControllerApplication` +- invalid `resume=True` plus `force_full=True` fails fast before queueing +- scan performs descriptor refresh with `Node_Desc_req`, `Active_EP_req`, and `Simple_Desc_req` +- descriptor refresh uses the same raw descriptor helper as normal init +- descriptor refresh parity covers edge cases such as inactive endpoints and profile or device-type coercion +- descriptor refresh failure aborts before attribute or command inventory +- refreshed raw descriptor replacement clears removed endpoint and cluster scan scopes +- scan uses refreshed raw database topology, not quirked live topology +- output cluster command directions are correct +- raw scan reads do not touch live `attributes_cache` +- standard and manufacturer-scoped rows coexist for the same attribute ID +- manufacturer-scoped inventory is skipped when the refreshed raw node descriptor has `None`, even if live manufacturer values or overrides exist +- the manufacturer-scope skipped reason is visible in both progress events and snapshot output +- attribute discovery resume continues from the stored cursor +- partial attribute read resume skips completed rows +- raw attribute reads use deterministic binary split fallback after transport failure +- ordered pending-read selection uses the final pending-read index shape +- unsupported extended attribute discovery falls back to standard discovery +- if both attribute discovery commands are unsupported, the scope completes discovery with no attributes and the scan continues +- tuple and decoded `foundation.DefaultResponse` `UNSUP_GENERAL_COMMAND` replies take the same control-flow path +- command discovery `UNSUP_GENERAL_COMMAND` replies are logged and treated as completed empty pages, not terminal scope failures +- `force_full` resets all scan state for the device +- duplicate requests for the same device reuse the existing scan task +- conflicting same-device requests raise `ScanInProgressError` +- different-device requests wait in FIFO order for the single global slot +- queued same-device duplicate and conflict behavior matches running-task behavior +- queued scans whose device disappears before execution raise `DeviceScanTargetMissingError`, not raw `KeyError` +- caller cancellation does not cancel the shared queued or running scan task +- scan-level deadline returns `failed` when the timed-out run committed no new scan rows +- scan-level deadline returns `partial` when the timed-out run committed new scan rows before expiry +- progress events use the fixed event names, statuses, payload shape, and terminal `scan_finished.outcome` +- summary and lower-level scan-state outputs expose stable `error_code` values +- attribute-read rows keep `read_status`, `last_error_code`, and derived `read_complete` consistent for success, terminal unsupported, and retryable transport failure +- descriptor refresh `step_*` events have no scope fields while inventory `step_*` events are per-scope +- `get_snapshot()` raises `DeviceScanSnapshotNotFoundError` when no persisted raw descriptor rows exist +- `get_snapshot()` returns a valid empty snapshot when raw descriptor rows exist but scan rows do not +- snapshot reads are DB-backed and reflect partial persisted state, not live runtime objects +- snapshot reads use bulk per-table queries rather than per-scope N+1 queries +- snapshot assembly seeds the hierarchy from raw rows once, then attaches scan rows in one pass +- snapshot output ordering is deterministic for endpoints, clusters, attributes, and commands +- snapshot attributes expose `datatype`, canonical raw value, and decoded value when decoding is possible +- skipped manufacturer scope is synthesized with full shape: `manufacturer_code=None`, `progress.status=skipped`, `progress.error_code=missing_raw_manufacturer_code`, and empty `attributes` and `commands` +- snapshot decoding from the canonical stored attribute value representation is correct for standard and manufacturer-scoped rows +- scan applies the fixed internal pacing rule between outbound requests + +## Test Diagram + +```text +[NEW] app.device_scanner.scan(ieee, resume, force_full) + | + +--> validate request args + | `--> invalid resume+force_full -> InvalidScanOptionsError + | + +--> classify request + | +--> exact duplicate same-device -> reuse task + | +--> same-device conflict -> ScanInProgressError + | `--> different device -> FIFO queue + | + +--> scan-level deadline wrapper + | +--> timeout before new rows commit -> failed summary + slot release + | `--> timeout after new rows commit -> partial summary + slot release + | + +--> descriptor refresh via shared helper + | +--> Node_Desc_req / Active_EP_req / Simple_Desc_req + | +--> atomic raw-row replacement + | `--> failure aborts inventory + | + +--> build ephemeral raw targets from DB rows + | + +--> per refreshed cluster + | +--> standard scope -> discovery / reads / command discovery + | `--> manufacturer_specific scope + | +--> raw manufacturer code present -> normal scan + | `--> raw manufacturer code absent -> synthesized skipped scope + missing_raw_manufacturer_code + | + `--> summary emission + +--> success / partial / failed + `--> stable error_code + human-readable error + +[NEW] app.device_scanner.get_snapshot(ieee) + | + +--> no raw descriptor rows -> DeviceScanSnapshotNotFoundError + +--> raw rows but no scan rows -> empty materialized snapshot + `--> partial/full scan rows -> assembled snapshot + +--> deterministic ordering + +--> canonical-value decode + `--> explicit manufacturer-scope skip visibility +``` + +## Non-Goals + +- automatic scanning during init or rejoin +- endpoint-scoped public scan APIs +- caller override APIs for manufacturer-specific scanning +- using scan results as live runtime cache +- cancel-scan API +- historical snapshot retention or diffing +- quirk-aware scan persistence + +## Recommended File Shape + +- `zigpy/zigpy/device_scanner.py` +- `zigpy/zigpy/application.py` +- `zigpy/zigpy/device.py` +- `zigpy/zigpy/endpoint.py` +- `zigpy/zigpy/appdb.py` +- `zigpy/zigpy/appdb_schemas/schema_v15.sql` +- `zigpy/tests/test_device_scanner.py` +- `zigpy/tests/test_device.py` +- `zigpy/tests/test_appdb.py` +- `zigpy/tests/test_appdb_migration.py` + +## Inline ASCII Diagram Candidates + +- `zigpy/zigpy/device_scanner.py`: queue ownership, deadline release, and scan pipeline +- `zigpy/zigpy/appdb.py`: raw descriptor replacement transaction and removed-scope cleanup +- `zigpy/tests/test_device_scanner.py`: descriptor refresh fixture shape when the setup is non-obvious + +## Summary + +The tight design is: + +- on-demand only +- raw on-wire only +- non-invasive at runtime +- descriptor-refresh-first +- shared descriptor discovery logic with normal init +- normalized DB schema +- resumable per cluster scope and per attribute read +- transport via live device, topology via refreshed raw database tables +- explicit machine-readable error codes +- explicit skipped manufacturer-scope visibility + +That gives zigpy a proper core device scan instead of a JSON dump helper while keeping the v1 contract explicit and testable. diff --git a/docs/plans/2026-03-15-raw-device-scan.md b/docs/plans/2026-03-15-raw-device-scan.md new file mode 100644 index 000000000..703a7be11 --- /dev/null +++ b/docs/plans/2026-03-15-raw-device-scan.md @@ -0,0 +1,542 @@ +# Raw Device Scan Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add an on-demand raw on-wire device scan service to zigpy that persists resumable scan state and inventory in the database without mutating live runtime state. + +**Architecture:** Add a `DeviceScanner` service on `ControllerApplication` that refreshes raw descriptors first, then scans refreshed raw endpoint and cluster rows using ephemeral scan-only endpoint and cluster objects backed by the live device transport. Extract the raw descriptor walk into shared init-time and scan-time helper logic, persist scan inventory plus checkpoints in new appdb tables, use one canonical stored attribute value representation, and expose a DB-backed snapshot API with explicit error codes, deadline behavior, and manufacturer-scope skip visibility. + +**Tech Stack:** Python, asyncio, zigpy ZDO/ZCL APIs, SQLite via `aiosqlite`, pytest + +--- + +### Task 1: Add schema, migration, and runtime persistence wiring + +**Files:** +- Create: `zigpy/zigpy/appdb_schemas/schema_v15.sql` +- Modify: `zigpy/zigpy/appdb.py` +- Modify: `zigpy/tests/test_appdb.py` +- Modify: `zigpy/tests/test_appdb_migration.py` +- Test: `zigpy/tests/test_appdb.py` +- Test: `zigpy/tests/test_appdb_migration.py` + +**Step 1: Write the failing tests** + +Add runtime-persistence tests that assert: + +- progress rows preserve `last_error_code` separately from `last_error` +- attribute rows preserve standard and manufacturer-scoped duplicates +- attribute rows persist one canonical raw value field plus datatype metadata +- command rows persist direction separately +- the pending-read index includes `attr_id` in its ordered key shape + +Add migration tests that assert: + +- `PRAGMA user_version` is incremented +- all new scan tables, columns, the final NULL-safe manufacturer-scope index shape, and exact named secondary indexes exist + +**Step 2: Run the tests to verify they fail** + +Run: `pytest zigpy/tests/test_appdb.py zigpy/tests/test_appdb_migration.py -k "device_scan or raw_scan" -v` +Expected: FAIL because schema v15 and runtime helpers are not fully wired. + +**Step 3: Implement schema v15 and DB helpers** + +Add: + +- `device_scan_progress_v15` +- `device_scan_attributes_v15` +- `device_scan_commands_v15` + +Use the same NULL-safe manufacturer code uniqueness pattern already used by `attributes_cache_v14`; do not add a generated `manufacturer_code_scope_idx` column unless implementation proves the existing pattern is insufficient. + +Define these exact secondary indexes: + +- `idx_device_scan_progress_v15_ieee` +- `idx_device_scan_attributes_v15_ieee` +- `idx_device_scan_attributes_v15_pending_reads` using the final NULL-safe manufacturer-scope key shape with ordered `(ieee, endpoint_id, cluster_type, cluster_id, read_complete, attr_id)` lookup for pending-readable attributes +- `idx_device_scan_commands_v15_ieee` + +Update `zigpy.appdb`: + +- bump `DB_VERSION` +- add migration handling +- add explicit async helpers for writing and clearing scan rows +- add explicit helpers for DB-backed snapshot reads using bulk per-table queries for one device +- persist `last_error_code` alongside `last_error` +- store one canonical raw attribute value field instead of parallel raw and normalized columns + +**Step 4: Run the tests** + +Run: `pytest zigpy/tests/test_appdb.py zigpy/tests/test_appdb_migration.py -k "device_scan or raw_scan" -v` +Expected: PASS + +**Step 5: Commit** + +```bash +git -C zigpy add zigpy/zigpy/appdb.py zigpy/zigpy/appdb_schemas/schema_v15.sql zigpy/tests/test_appdb.py zigpy/tests/test_appdb_migration.py +git -C zigpy commit -m "feat: add raw device scan schema" +``` + +### Task 2: Add failing scanner API tests + +**Files:** +- Modify: `zigpy/tests/test_application.py` +- Create: `zigpy/tests/test_device_scanner.py` +- Modify: `zigpy/zigpy/application.py` +- Create: `zigpy/zigpy/device_scanner.py` + +**Step 1: Write the failing tests** + +Add tests that assert: + +- `ControllerApplication` exposes `device_scanner` +- scans are explicit and not started automatically +- `scan(ieee, resume=True, force_full=True)` fails fast before queue classification +- public contract types exist: `DeviceScanSummary`, `DeviceScanSnapshot`, and `DeviceScanProgressEvent` +- public error types exist: `InvalidScanOptionsError`, `ScanInProgressError`, `DeviceScanTargetMissingError`, and `DeviceScanSnapshotNotFoundError` +- scan progress events use the fixed event names and payload shape +- summary and event payloads include stable `error_code` +- scan progress listeners attach to `app.device_scanner` + +**Step 2: Run the tests to verify they fail** + +Run: `pytest zigpy/tests/test_application.py zigpy/tests/test_device_scanner.py -k "device_scanner" -v` +Expected: FAIL because `device_scanner` does not exist. + +**Step 3: Implement minimal service wiring** + +Add: + +- wire a `DeviceScanner` placeholder into `ControllerApplication.__init__` +- define `DeviceScanSummary`, `DeviceScanSnapshot`, and `DeviceScanProgressEvent` +- define `InvalidScanOptionsError`, `ScanInProgressError`, `DeviceScanTargetMissingError`, and `DeviceScanSnapshotNotFoundError` +- define one centralized vocabulary for scan event names, statuses, outcomes, steps, and error codes +- define one shared internal step-status vocabulary +- define one private `_emit_progress(...)` helper +- validate `resume` plus `force_full` before queue classification + +**Step 4: Run the tests** + +Run: `pytest zigpy/tests/test_application.py zigpy/tests/test_device_scanner.py -k "device_scanner" -v` +Expected: PASS for wiring tests, FAIL for actual scan behavior tests. + +**Step 5: Commit** + +```bash +git -C zigpy add zigpy/zigpy/application.py zigpy/zigpy/device_scanner.py zigpy/tests/test_application.py zigpy/tests/test_device_scanner.py +git -C zigpy commit -m "feat: add device scanner service wiring" +``` + +### Task 3: Extract shared raw descriptor helper logic + +**Files:** +- Modify: `zigpy/zigpy/device.py` +- Modify: `zigpy/zigpy/endpoint.py` +- Modify: `zigpy/zigpy/device_scanner.py` +- Modify: `zigpy/tests/test_device.py` +- Modify: `zigpy/tests/test_device_scanner.py` + +**Step 1: Write the failing tests** + +Cover: + +- scan refresh uses the same raw descriptor walk semantics as normal init +- inactive endpoints keep the same behavior under init and scan refresh +- profile and device-type coercion match between init and scan refresh +- scan refresh does not mutate live runtime endpoint or cluster dictionaries + +**Step 2: Run the tests** + +Run: `pytest zigpy/tests/test_device.py zigpy/tests/test_device_scanner.py -k "raw_descriptor or descriptor_parity" -v` +Expected: FAIL because the shared raw descriptor helper does not exist yet. + +**Step 3: Extract the shared helper** + +Implement: + +- one shared raw descriptor discovery helper and result shape reused by normal init and scanner refresh +- init-time application of the shared result to live endpoints and clusters +- scan-time reuse of the shared result without mutating live runtime dictionaries + +**Step 4: Run the tests** + +Run: `pytest zigpy/tests/test_device.py zigpy/tests/test_device_scanner.py -k "raw_descriptor or descriptor_parity" -v` +Expected: PASS + +**Step 5: Commit** + +```bash +git -C zigpy add zigpy/zigpy/device.py zigpy/zigpy/endpoint.py zigpy/zigpy/device_scanner.py zigpy/tests/test_device.py zigpy/tests/test_device_scanner.py +git -C zigpy commit -m "refactor: share raw descriptor discovery" +``` + +### Task 4: Add descriptor refresh, replacement, and raw target building + +**Files:** +- Modify: `zigpy/tests/test_device_scanner.py` +- Modify: `zigpy/zigpy/device_scanner.py` +- Modify: `zigpy/zigpy/appdb.py` + +**Step 1: Write the failing tests** + +Cover: + +- scan performs `Node_Desc_req`, `Active_EP_req`, and `Simple_Desc_req` before inventory +- descriptor refresh replaces raw descriptor rows atomically +- removed endpoint and cluster scan scopes are cleared when refresh removes them +- descriptor refresh failure aborts before attribute or command inventory +- descriptor refresh emits the expected step events with `started` then `success` or `failed` +- the shared internal pacing rule is applied between descriptor refresh requests +- raw scan targets are built from refreshed raw DB rows, not live quirked topology +- endpoint `242` is excluded from raw scan targets +- quirk-added endpoints and clusters are ignored +- ephemeral scan objects are not attached to live endpoint dictionaries +- output clusters use the proper cluster role when building scan objects + +**Step 2: Run the tests** + +Run: `pytest zigpy/tests/test_device_scanner.py -k "descriptor_refresh or raw_topology or scan_builder" -v` +Expected: FAIL because descriptor refresh and DB-backed target building are not implemented. + +**Step 3: Implement descriptor refresh and target building** + +Add: + +- atomic raw descriptor replacement helpers in `zigpy.appdb` +- removed-scope cleanup for scan rows and progress rows +- fail-closed behavior when descriptor refresh does not complete +- raw scan target extraction from refreshed raw descriptor rows +- exclusion of endpoint `242` from scan target materialization +- a private helper that creates ephemeral scan-only endpoint and cluster objects +- no named proxy classes +- inline ASCII diagrams in `device_scanner.py` and `appdb.py` when the queue and replacement code is non-obvious + +**Step 4: Run the tests** + +Run: `pytest zigpy/tests/test_device_scanner.py -k "descriptor_refresh or raw_topology or scan_builder" -v` +Expected: PASS + +**Step 5: Commit** + +```bash +git -C zigpy add zigpy/zigpy/device_scanner.py zigpy/zigpy/appdb.py zigpy/tests/test_device_scanner.py +git -C zigpy commit -m "feat: refresh raw descriptors and build scan targets" +``` + +### Task 5: Add failing attribute discovery and raw read tests + +**Files:** +- Modify: `zigpy/tests/test_device_scanner.py` +- Modify: `zigpy/tests/test_appdb.py` +- Modify: `zigpy/zigpy/device_scanner.py` +- Modify: `zigpy/zigpy/appdb.py` + +**Step 1: Write the failing tests** + +Cover: + +- `Discover_Attribute_Extended` pages persist immediately when supported +- tuple and decoded `foundation.DefaultResponse` unsupported replies are normalized the same way +- `Discover_Attribute_Extended` falls back to `Discover_Attributes` on default-response `UNSUP_GENERAL_COMMAND` +- manufacturer-scoped `Discover_Attribute_Extended` falls back to `Discover_Attributes` on default-response `UNSUP_MANUF_GENERAL_COMMAND` +- if both attribute discovery commands return default-response `UNSUP_GENERAL_COMMAND`, the scan continues with an empty completed discovery step +- if both manufacturer-scoped attribute discovery commands return default-response `UNSUP_MANUF_GENERAL_COMMAND`, the scan continues with an empty completed discovery step +- any other discovery default-response status becomes a terminal scope failure instead of falling through to page parsing +- output-cluster response frames with the wrong ZCL direction bit still resolve the pending request instead of becoming false transport timeouts +- wrong-direction non-response traffic does not resolve pending requests just because the TSN matches +- per-request reply timeouts become scope-local `transport_failure`, not `scan_deadline_exceeded` +- progress cursor advances only after a successful page write +- standard and manufacturer-scoped attribute rows can coexist +- manufacturer scope is skipped when the refreshed raw node descriptor has `None`, even if the live device has manufacturer values or overrides +- shared paging semantics match command discovery semantics +- readable attributes are read with `read_attributes_raw()` in fixed batches of 3 +- live attribute cache is unchanged after the scan +- batch transport failure falls back using deterministic binary split semantics +- tuple and decoded `foundation.DefaultResponse` raw-read replies are normalized the same way +- malformed `ReadAttributesResponse` payloads that omit requested attribute IDs are logged, retried one-by-one for only the missing attributes, then persisted as retryable `transport_failure` rows only for any attributes still missing after the one-by-one retry +- split-and-retry operates from an in-memory per-scope target set instead of re-querying every fallback +- per-attribute progress resumes correctly +- deadline expiry during raw reads preserves already committed rows +- attribute read failures report stable `error_code` values +- attribute-read rows keep `read_status`, `last_error_code`, and derived `read_complete` consistent for success, terminal unsupported, and retryable transport failure +- only one canonical raw value field is persisted for reads + +**Step 2: Run the tests** + +Run: `pytest zigpy/tests/test_device_scanner.py zigpy/tests/test_appdb.py -k "attribute_discovery or raw_read or device_scan_value" -v` +Expected: FAIL because discovery and raw read persistence are not implemented. + +**Step 3: Implement attribute discovery and raw reads** + +Implement: + +- paged attribute discovery through the shared private page-runner helper +- `Discover_Attribute_Extended` first, then `Discover_Attributes` fallback on default-response `UNSUP_GENERAL_COMMAND` +- manufacturer-scoped discovery uses the same fallback semantics for default-response `UNSUP_MANUF_GENERAL_COMMAND` +- empty completed attribute-discovery persistence when both discovery commands are unsupported +- tuple and decoded `foundation.DefaultResponse` unsupported replies share the same fallback/continue behavior +- non-`UNSUP_GENERAL_COMMAND` discovery default responses take the terminal scope-failure path +- output-cluster compatibility for wrong-direction response frames so request/reply matching does not false-time out +- wrong-direction non-response traffic cannot satisfy pending requests +- discovery request timeouts take the scope-local `transport_failure` path +- shared internal pacing reused for discovery pages +- progress row upserts committed in the same transaction as each successful page +- resolved attribute name only when safe +- read target selection from persisted ACL rows, with standard-discovery fallback rows treated as readable when ACL is unknown +- canonical raw value persistence plus datatype metadata +- tuple and decoded `foundation.DefaultResponse` raw-read replies share the same terminal-read mapping +- deterministic binary split fallback after whole-batch transport failure +- one-by-one retry for only the attribute IDs omitted from an otherwise valid read response +- per-batch transactions for successful persistence +- deadline-aware partial-persistence behavior +- stable `error_code` reporting for read failures +- explicit mapping between `read_status`, `last_error_code`, and derived `read_complete` + +**Step 4: Run the tests** + +Run: `pytest zigpy/tests/test_device_scanner.py zigpy/tests/test_appdb.py -k "attribute_discovery or raw_read or device_scan_value" -v` +Expected: PASS + +**Step 5: Commit** + +```bash +git -C zigpy add zigpy/zigpy/device_scanner.py zigpy/zigpy/appdb.py zigpy/tests/test_device_scanner.py zigpy/tests/test_appdb.py +git -C zigpy commit -m "feat: persist raw attribute discovery and reads" +``` + +### Task 6: Add failing command discovery tests + +**Files:** +- Modify: `zigpy/tests/test_device_scanner.py` +- Modify: `zigpy/zigpy/device_scanner.py` +- Modify: `zigpy/zigpy/appdb.py` + +**Step 1: Write the failing tests** + +Cover: + +- received and generated command discovery persists direction correctly +- output clusters use the proper client role +- manufacturer-scoped command discovery runs when a manufacturer code is available +- manufacturer-scoped command discovery is skipped when the refreshed raw node descriptor from this scan has `None` +- the manufacturer-scope skipped reason is visible in progress events +- default-response `UNSUP_GENERAL_COMMAND` during command discovery is logged, persisted as an empty completed page, and does not make the scan partial +- default-response `UNSUP_MANUF_GENERAL_COMMAND` during manufacturer-scoped command discovery is logged, persisted as an empty completed page, and does not make the scan partial +- tuple and decoded `foundation.DefaultResponse` unsupported replies take the same command-discovery path +- non-`UNSUP_GENERAL_COMMAND` command-discovery default responses are terminal scope failures +- manufacturer-scoped command-discovery timeouts retry the same page once with short backoff before surfacing `transport_failure` +- shared paging semantics match attribute discovery semantics + +**Step 2: Run the tests** + +Run: `pytest zigpy/tests/test_device_scanner.py -k "command_discovery or output_cluster" -v` +Expected: FAIL because command discovery is incomplete. + +**Step 3: Implement command discovery steps** + +Implement: + +- paged received command discovery through the shared private page-runner helper +- paged generated command discovery through the shared private page-runner helper +- shared internal pacing reused for command discovery pages +- direction-aware persistence +- per-page transactions for rows and progress updates +- default-response `UNSUP_GENERAL_COMMAND` handling that logs and completes the command-discovery direction with no commands +- tuple and decoded `foundation.DefaultResponse` unsupported replies share the same command-discovery skip path +- non-`UNSUP_GENERAL_COMMAND` command-discovery default responses share the same terminal failure path across tuple and decoded forms +- one short retry for manufacturer-scoped command-discovery timeouts before terminal transport failure +- skipped manufacturer-scope event emission with `error_code="missing_raw_manufacturer_code"` + +**Step 4: Run the tests** + +Run: `pytest zigpy/tests/test_device_scanner.py -k "command_discovery or output_cluster" -v` +Expected: PASS + +**Step 5: Commit** + +```bash +git -C zigpy add zigpy/zigpy/device_scanner.py zigpy/zigpy/appdb.py zigpy/tests/test_device_scanner.py +git -C zigpy commit -m "feat: persist raw command discovery" +``` + +### Task 7: Add failing resume and deadline tests + +**Files:** +- Modify: `zigpy/tests/test_device_scanner.py` +- Modify: `zigpy/zigpy/device_scanner.py` +- Modify: `zigpy/zigpy/appdb.py` + +**Step 1: Write the failing tests** + +Cover: + +- completed steps are skipped on resume +- incomplete steps resume from stored cursors +- `force_full` clears all scan rows for the device +- duplicate same-device requests reuse the existing task +- conflicting same-device requests raise `ScanInProgressError` +- different-device requests wait in FIFO order for the single global slot +- queued same-device duplicate and conflict behavior matches running-task behavior +- queued scans whose device disappears before execution raise `DeviceScanTargetMissingError` +- caller cancellation detaches the caller wait without cancelling shared queued or running scan work +- deadline expiry before any new scan rows are committed returns `failed` +- deadline expiry after new scan rows are committed returns `partial` +- queue, start, and finish progress events are emitted on `app.device_scanner` in the expected order +- `scan_finished` carries terminal `outcome=success|partial|failed` matching the summary result +- descriptor refresh step events have no scope fields and inventory step events are per-scope +- resumed, partial, or failed scans still return the fixed summary type with correct `outcome` and `error_code` + +**Step 2: Run the tests** + +Run: `pytest zigpy/tests/test_device_scanner.py -k "resume or force_full or deadline or device_scanner" -v` +Expected: FAIL because resume and deadline semantics are incomplete. + +**Step 3: Implement resume and deadline behavior** + +Add: + +- device-wide reset helpers +- progress row evaluation +- per-device task dedupe +- conflicting request rejection +- global FIFO queue handling +- missing-device-at-execution failure handling via `DeviceScanTargetMissingError` +- shared task ownership with caller-cancellation detachment +- scan-level deadline wrapper +- fixed internal scan deadline set to 120 seconds +- underlying zigpy per-request reply timeouts are separate from the scan deadline wrapper +- `scan_deadline_exceeded` summary and event reporting +- timeout outcome selection based on whether the timed-out run committed new scan rows + +**Step 4: Run the tests** + +Run: `pytest zigpy/tests/test_device_scanner.py -k "resume or force_full or deadline or device_scanner" -v` +Expected: PASS + +**Step 5: Commit** + +```bash +git -C zigpy add zigpy/zigpy/device_scanner.py zigpy/zigpy/appdb.py zigpy/tests/test_device_scanner.py +git -C zigpy commit -m "feat: add resumable raw device scans and deadline handling" +``` + +### Task 8: Add snapshot retrieval API tests + +**Files:** +- Modify: `zigpy/tests/test_device_scanner.py` +- Modify: `zigpy/zigpy/device_scanner.py` +- Modify: `zigpy/zigpy/appdb.py` + +**Step 1: Write the failing tests** + +Cover: + +- `app.device_scanner.get_snapshot(...)` returns a fixed `DeviceScanSnapshot` +- `app.device_scanner.get_snapshot(...)` raises `DeviceScanSnapshotNotFoundError` when no persisted raw descriptor rows exist +- `app.device_scanner.get_snapshot(...)` returns a valid empty snapshot when raw descriptor rows exist but scan rows do not +- the snapshot has stable top-level fields: `ieee`, `raw_node_descriptor`, `last_snapshot_at`, and `endpoints` +- the snapshot hierarchy is `endpoints -> clusters -> standard / manufacturer_specific` +- each scope embeds `progress`, `attributes`, and `commands` +- `standard` is always materialized and `manufacturer_specific` is also materialized when raw manufacturer scope is explicitly skipped +- `last_snapshot_at` is the max persisted timestamp among included rows +- snapshots do not require loading scan tables into live runtime objects +- partial snapshot reads reflect persisted incomplete DB state, not live runtime objects +- snapshot retrieval is a public `DeviceScanner` API rather than a caller-facing `appdb` surface +- progress event scope metadata maps to the same hierarchical snapshot location +- snapshot output ordering is deterministic for endpoints, clusters, attributes, and commands +- snapshot attributes expose `datatype`, canonical raw value, and decoded value when decoding is possible +- skipped manufacturer scope includes `manufacturer_code=None`, `progress.status="skipped"`, `progress.error_code="missing_raw_manufacturer_code"`, and empty `attributes` and `commands` + +**Step 2: Run the tests** + +Run: `pytest zigpy/tests/test_device_scanner.py -k "snapshot" -v` +Expected: FAIL because snapshot retrieval is missing. + +**Step 3: Implement snapshot getter** + +Add a public `DeviceScanner` snapshot API backed by a DB read path that assembles a fixed hierarchical `DeviceScanSnapshot` from: + +- raw device identifiers +- raw endpoints and clusters +- discovered attributes +- discovered commands +- per-scope progress state + +Use: + +- one bulk query per relevant table for the target IEEE +- low-level per-table scan rows from `appdb` +- seed the endpoint and cluster hierarchy from raw rows in one pass +- then attach progress, attributes, and commands from scan rows in one pass +- the shared internal scope identity helper to place rows under the correct endpoint, cluster, and scope +- in-memory assembly of the merged hierarchical snapshot in `DeviceScanner` +- canonical ordering for endpoints, clusters, attributes, and commands +- single-pass decode of canonical stored attribute values during assembly with reused metadata per cluster or type +- synthesized skipped manufacturer scope materialization from raw descriptor state instead of silent omission or persisted skipped progress rows + +**Step 4: Run the tests** + +Run: `pytest zigpy/tests/test_device_scanner.py -k "snapshot" -v` +Expected: PASS + +**Step 5: Commit** + +```bash +git -C zigpy add zigpy/zigpy/device_scanner.py zigpy/zigpy/appdb.py zigpy/tests/test_device_scanner.py +git -C zigpy commit -m "feat: add raw device scan snapshot retrieval" +``` + +### Task 9: Run focused verification and polish + +**Files:** +- Modify: `zigpy/zigpy/device_scanner.py` +- Modify: `zigpy/zigpy/appdb.py` +- Modify: `zigpy/zigpy/device.py` +- Modify: `zigpy/zigpy/endpoint.py` +- Modify: `zigpy/tests/test_device_scanner.py` +- Modify: `zigpy/tests/test_appdb.py` +- Modify: `zigpy/tests/test_appdb_migration.py` +- Modify: `zigpy/tests/test_application.py` +- Modify: `zigpy/tests/test_device.py` + +**Step 1: Run the focused suites** + +Run: + +```bash +pytest zigpy/tests/test_device_scanner.py -v +pytest zigpy/tests/test_appdb.py -k "device_scan or raw_scan" -v +pytest zigpy/tests/test_appdb_migration.py -k "device_scan or raw_scan" -v +pytest zigpy/tests/test_application.py -k "device_scanner" -v +pytest zigpy/tests/test_device.py -k "raw_descriptor or device_scanner" -v +``` + +Expected: PASS + +**Step 2: Fix any failures with minimal changes** + +Prefer: + +- small test corrections only when behavior is clearly correct +- minimal production changes +- no extra options or abstractions not required by the design +- tests that assert `error_code` rather than human-readable message text +- preserving the single canonical value-storage rule + +**Step 3: Run a broader regression pass** + +Run: + +```bash +pytest zigpy/tests/test_appdb.py zigpy/tests/test_appdb_migration.py zigpy/tests/test_application.py zigpy/tests/test_device.py zigpy/tests/test_endpoint.py zigpy/tests/test_device_scanner.py -v +``` + +Expected: PASS + +**Step 4: Commit** + +```bash +git -C zigpy add zigpy/zigpy/device_scanner.py zigpy/zigpy/application.py zigpy/zigpy/device.py zigpy/zigpy/endpoint.py zigpy/zigpy/appdb.py zigpy/zigpy/appdb_schemas/schema_v15.sql zigpy/tests/test_device_scanner.py zigpy/tests/test_appdb.py zigpy/tests/test_appdb_migration.py zigpy/tests/test_application.py zigpy/tests/test_device.py +git -C zigpy commit -m "feat: add resumable raw device scan service" +``` diff --git a/docs/plans/device_scan.log b/docs/plans/device_scan.log new file mode 100644 index 000000000..5bf5ac39c --- /dev/null +++ b/docs/plans/device_scan.log @@ -0,0 +1,1050 @@ +2026-03-15 19:04:33.046 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan scan_queued for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='queued', outcome=None, step=None, endpoint_id=None, cluster_id=None, cluster_type=None, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:33.049 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan scan_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step=None, endpoint_id=None, cluster_id=None, cluster_type=None, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:33.050 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='descriptor_refresh', endpoint_id=None, cluster_id=None, cluster_type=None, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:33.050 INFO (MainThread) [zigpy.device] [0x40d1] Requesting 'Node Descriptor' +2026-03-15 19:04:33.050 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 33, 50855, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=0, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=0, source_route=[], extended_timeout=False, tsn=6, profile_id=0, cluster_id=, data=Serialized[b'\x06\xd1@'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:33.177 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 33, 177474, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x40D1), src_ep=0, dst=AddrModeAddress(addr_mode=, address=0x0000), dst_ep=0, source_route=None, extended_timeout=False, tsn=183, profile_id=0, cluster_id=32770, data=Serialized[b'\x06\x00\xd1@\x01@\x8e\x0b\x10R\x80\x00\x00,\x80\x00\x00'], tx_options=, radius=0, non_member_radius=0, lqi=88, rssi=-78) +2026-03-15 19:04:33.349 DEBUG (MainThread) [zigpy.application] Failed to send packet, attempt 1 of 3 +2026-03-15 19:04:33.351 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 33, 351273, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=0, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=0, source_route=[], extended_timeout=False, tsn=6, profile_id=0, cluster_id=, data=Serialized[b'\x06\xd1@'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:33.378 INFO (MainThread) [zigpy.device] [0x40d1] Got Node Descriptor: NodeDescriptor(logical_type=, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=, mac_capability_flags=, manufacturer_code=4107, maximum_buffer_size=82, maximum_incoming_transfer_size=128, server_mask=11264, maximum_outgoing_transfer_size=128, descriptor_capability_field=, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=False, *is_full_function_device=True, *is_mains_powered=True, *is_receiver_on_when_idle=True, *is_router=True, *is_security_capable=False) +2026-03-15 19:04:33.389 INFO (MainThread) [zigpy.device] [0x40d1] Discovering endpoints +2026-03-15 19:04:33.389 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 33, 389147, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=0, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=0, source_route=[], extended_timeout=False, tsn=7, profile_id=0, cluster_id=, data=Serialized[b'\x07\xd1@'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:33.390 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 33, 390538, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x40D1), src_ep=0, dst=AddrModeAddress(addr_mode=, address=0x0000), dst_ep=0, source_route=None, extended_timeout=False, tsn=184, profile_id=0, cluster_id=32770, data=Serialized[b'\x06\x00\xd1@\x01@\x8e\x0b\x10R\x80\x00\x00,\x80\x00\x00'], tx_options=, radius=0, non_member_radius=0, lqi=88, rssi=-78) +2026-03-15 19:04:33.390 DEBUG (MainThread) [zigpy.device] [0x40d1] Filtering duplicate packet +2026-03-15 19:04:33.586 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 33, 586544, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x40D1), src_ep=0, dst=AddrModeAddress(addr_mode=, address=0x0000), dst_ep=0, source_route=None, extended_timeout=False, tsn=185, profile_id=0, cluster_id=32773, data=Serialized[b'\x07\x00\xd1@\x02\x0b\xf2'], tx_options=, radius=0, non_member_radius=0, lqi=88, rssi=-78) +2026-03-15 19:04:33.587 INFO (MainThread) [zigpy.device] [0x40d1] Discovered endpoints: [11, 242] +2026-03-15 19:04:33.598 INFO (MainThread) [zigpy.endpoint] [0x40d1:11] Discovering endpoint information +2026-03-15 19:04:33.598 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 33, 598546, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=0, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=0, source_route=[], extended_timeout=False, tsn=8, profile_id=0, cluster_id=, data=Serialized[b'\x08\xd1@\x0b'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:33.715 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 33, 715991, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x40D1), src_ep=0, dst=AddrModeAddress(addr_mode=, address=0x0000), dst_ep=0, source_route=None, extended_timeout=False, tsn=186, profile_id=0, cluster_id=32772, data=Serialized[b'\x08\x00\xd1@\x1e\x0b\x04\x01\r\x01\x01\n\x00\x00\x03\x00\x04\x00\x05\x00\x06\x00\x08\x00\x00\x10\x03\xfc\x00\x03\x01\xfc\x01\x19\x00'], tx_options=, radius=0, non_member_radius=0, lqi=88, rssi=-78) +2026-03-15 19:04:33.716 INFO (MainThread) [zigpy.endpoint] [0x40d1:11] Discovered endpoint information: SizePrefixedSimpleDescriptor(endpoint=11, profile=260, device_type=269, device_version=1, input_clusters=[0, 3, 4, 5, 6, 8, 4096, 64515, 768, 64513], output_clusters=[25]) +2026-03-15 19:04:33.727 INFO (MainThread) [zigpy.endpoint] [0x40d1:242] Discovering endpoint information +2026-03-15 19:04:33.727 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 33, 727442, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=0, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=0, source_route=[], extended_timeout=False, tsn=9, profile_id=0, cluster_id=, data=Serialized[b'\t\xd1@\xf2'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:33.998 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 33, 998151, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x40D1), src_ep=0, dst=AddrModeAddress(addr_mode=, address=0x0000), dst_ep=0, source_route=None, extended_timeout=False, tsn=187, profile_id=0, cluster_id=32772, data=Serialized[b'\t\x00\xd1@\n\xf2\xe0\xa1a\x00\x00\x00\x01!\x00'], tx_options=, radius=0, non_member_radius=0, lqi=88, rssi=-78) +2026-03-15 19:04:33.998 INFO (MainThread) [zigpy.endpoint] [0x40d1:242] Discovered endpoint information: SizePrefixedSimpleDescriptor(endpoint=242, profile=41440, device_type=97, device_version=0, input_clusters=[], output_clusters=[33]) +2026-03-15 19:04:34.009 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='descriptor_refresh', endpoint_id=None, cluster_id=None, cluster_type=None, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:34.010 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=0, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:34.011 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=10, command_id=, *direction=) +2026-03-15 19:04:34.011 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:34.011 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 34, 11296, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=10, profile_id=260, cluster_id=0, data=Serialized[b'\x00\n\x15\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:34.239 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=10, command_id=11, *direction=) +2026-03-15 19:04:34.239 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame: Basic:DefaultResponse(command_id=21, status=) +2026-03-15 19:04:34.239 WARNING (MainThread) [zigpy.device_scanner] Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0000 (server): UNSUP_GENERAL_COMMAND; falling back to discover_attributes +2026-03-15 19:04:34.239 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=11, command_id=, *direction=) +2026-03-15 19:04:34.240 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Discover_Attributes(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:34.240 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 34, 240297, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=11, profile_id=260, cluster_id=0, data=Serialized[b'\x00\x0b\x0c\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:34.305 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=11, command_id=13, *direction=) +2026-03-15 19:04:34.305 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame: Basic:Discover_Attributes_rsp(discovery_complete=, attribute_info=[DiscoverAttributesResponseRecord(attrid=0, datatype=32), DiscoverAttributesResponseRecord(attrid=1, datatype=32), DiscoverAttributesResponseRecord(attrid=2, datatype=32), DiscoverAttributesResponseRecord(attrid=3, datatype=32), DiscoverAttributesResponseRecord(attrid=4, datatype=66), DiscoverAttributesResponseRecord(attrid=5, datatype=66), DiscoverAttributesResponseRecord(attrid=6, datatype=66), DiscoverAttributesResponseRecord(attrid=7, datatype=48), DiscoverAttributesResponseRecord(attrid=8, datatype=48), DiscoverAttributesResponseRecord(attrid=9, datatype=48), DiscoverAttributesResponseRecord(attrid=10, datatype=65), DiscoverAttributesResponseRecord(attrid=11, datatype=66), DiscoverAttributesResponseRecord(attrid=16384, datatype=66), DiscoverAttributesResponseRecord(attrid=65533, datatype=33)]) +2026-03-15 19:04:34.319 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=0, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:34.319 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=0, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:34.320 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=12, command_id=, *direction=) +2026-03-15 19:04:34.320 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Read_Attributes(attribute_ids=[0, 1, 2]) +2026-03-15 19:04:34.320 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 34, 320707, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=12, profile_id=260, cluster_id=0, data=Serialized[b'\x00\x0c\x00\x00\x00\x01\x00\x02\x00'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:34.608 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=12, command_id=1, *direction=) +2026-03-15 19:04:34.608 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame: Basic:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=0, status=, value=TypeValue(type=uint8_t, value=2)), ReadAttributeRecord(attrid=1, status=, value=TypeValue(type=uint8_t, value=2)), ReadAttributeRecord(attrid=2, status=, value=TypeValue(type=uint8_t, value=1))]) +2026-03-15 19:04:34.621 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=13, command_id=, *direction=) +2026-03-15 19:04:34.621 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Read_Attributes(attribute_ids=[3, 4, 5]) +2026-03-15 19:04:34.621 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 34, 621501, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=13, profile_id=260, cluster_id=0, data=Serialized[b'\x00\r\x00\x03\x00\x04\x00\x05\x00'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:34.899 DEBUG (MainThread) [zigpy.application] Failed to send packet, attempt 1 of 3 +2026-03-15 19:04:34.900 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 34, 900201, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=13, profile_id=260, cluster_id=0, data=Serialized[b'\x00\r\x00\x03\x00\x04\x00\x05\x00'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:35.213 DEBUG (MainThread) [zigpy.application] Failed to send packet, attempt 2 of 3 +2026-03-15 19:04:35.215 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 35, 215133, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=13, profile_id=260, cluster_id=0, data=Serialized[b'\x00\r\x00\x03\x00\x04\x00\x05\x00'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:35.304 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 35, 304663, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x40D1), src_ep=0, dst=AddrModeAddress(addr_mode=, address=0x0000), dst_ep=0, source_route=None, extended_timeout=False, tsn=191, profile_id=0, cluster_id=32824, data=Serialized[b'\x00\x00\x00\xf8\xff\x07\x14\x00\x06\x00\x10\xc0\xc1\xe0\xea\xe1\xd5\xc4\xc9\xc5\xd2\xde\xe6\xd9\xcf\xd3\xdb'], tx_options=, radius=0, non_member_radius=0, lqi=88, rssi=-78) +2026-03-15 19:04:35.304 DEBUG (MainThread) [zigpy.zdo] [0x40d1:zdo] ZDO request ZDOCmd.Mgmt_NWK_Update_rsp: [, , 20, 6, [192, 193, 224, 234, 225, 213, 196, 201, 197, 210, 222, 230, 217, 207, 211, 219]] +2026-03-15 19:04:35.305 DEBUG (MainThread) [zigpy.zdo] [0x40d1:zdo] No handler for ZDO request:ZDOCmd.Mgmt_NWK_Update_rsp([, , 20, 6, [192, 193, 224, 234, 225, 213, 196, 201, 197, 210, 222, 230, 217, 207, 211, 219]]) +2026-03-15 19:04:35.430 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=13, command_id=1, *direction=) +2026-03-15 19:04:35.430 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame: Basic:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=3, status=, value=TypeValue(type=uint8_t, value=0)), ReadAttributeRecord(attrid=4, status=, value=TypeValue(type=CharacterString, value='Philips')), ReadAttributeRecord(attrid=5, status=, value=TypeValue(type=CharacterString, value='7602031U7'))]) +2026-03-15 19:04:35.441 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=14, command_id=, *direction=) +2026-03-15 19:04:35.442 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Read_Attributes(attribute_ids=[6, 7, 8]) +2026-03-15 19:04:35.442 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 35, 442285, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=14, profile_id=260, cluster_id=0, data=Serialized[b'\x00\x0e\x00\x06\x00\x07\x00\x08\x00'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:40.525 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=15, command_id=, *direction=) +2026-03-15 19:04:40.525 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Read_Attributes(attribute_ids=[6, 7, 8]) +2026-03-15 19:04:40.526 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 40, 526217, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=15, profile_id=260, cluster_id=0, data=Serialized[b'\x00\x0f\x00\x06\x00\x07\x00\x08\x00'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:40.597 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=15, command_id=1, *direction=) +2026-03-15 19:04:40.597 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame: Basic:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=6, status=, value=TypeValue(type=CharacterString, value='20211210')), ReadAttributeRecord(attrid=7, status=, value=TypeValue(type=enum8, value=)), ReadAttributeRecord(attrid=8, status=, value=TypeValue(type=enum8, value=))]) +2026-03-15 19:04:40.610 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=16, command_id=, *direction=) +2026-03-15 19:04:40.610 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Read_Attributes(attribute_ids=[9, 10, 11]) +2026-03-15 19:04:40.610 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 40, 610924, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=16, profile_id=260, cluster_id=0, data=Serialized[b'\x00\x10\x00\t\x00\n\x00\x0b\x00'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:40.653 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=16, command_id=1, *direction=) +2026-03-15 19:04:40.653 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame: Basic:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=9, status=, value=TypeValue(type=enum8, value=)), ReadAttributeRecord(attrid=10, status=, value=TypeValue(type=LVBytes, value=b'')), ReadAttributeRecord(attrid=11, status=, value=TypeValue(type=CharacterString, value='www.meethue.com'))]) +2026-03-15 19:04:40.665 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=17, command_id=, *direction=) +2026-03-15 19:04:40.665 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Read_Attributes(attribute_ids=[16384, 65533]) +2026-03-15 19:04:40.665 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 40, 665804, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=17, profile_id=260, cluster_id=0, data=Serialized[b'\x00\x11\x00\x00@\xfd\xff'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:40.710 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=17, command_id=1, *direction=) +2026-03-15 19:04:40.710 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame: Basic:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=16384, status=, value=TypeValue(type=CharacterString, value='1.93.7')), ReadAttributeRecord(attrid=65533, status=, value=TypeValue(type=uint16_t, value=1))]) +2026-03-15 19:04:40.723 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=0, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:40.724 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=0, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:40.724 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=18, command_id=, *direction=) +2026-03-15 19:04:40.724 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Discover_Commands_Received(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:40.724 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 40, 724534, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=18, profile_id=260, cluster_id=0, data=Serialized[b'\x00\x12\x11\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:40.759 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=18, command_id=11, *direction=) +2026-03-15 19:04:40.759 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame: Basic:DefaultResponse(command_id=17, status=) +2026-03-15 19:04:40.759 WARNING (MainThread) [zigpy.device_scanner] Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0000 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND +2026-03-15 19:04:40.771 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=0, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:40.771 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=0, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:40.771 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=19, command_id=, *direction=) +2026-03-15 19:04:40.771 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Discover_Commands_Generated(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:40.772 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 40, 772104, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=19, profile_id=260, cluster_id=0, data=Serialized[b'\x00\x13\x13\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:40.817 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=19, command_id=11, *direction=) +2026-03-15 19:04:40.817 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame: Basic:DefaultResponse(command_id=19, status=) +2026-03-15 19:04:40.817 WARNING (MainThread) [zigpy.device_scanner] Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0000 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND +2026-03-15 19:04:40.829 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=0, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:40.829 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=3, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:40.829 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=20, command_id=, *direction=) +2026-03-15 19:04:40.829 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Sending request: Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:40.829 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 40, 829808, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=20, profile_id=260, cluster_id=3, data=Serialized[b'\x00\x14\x15\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:40.880 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=20, command_id=11, *direction=) +2026-03-15 19:04:40.881 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Decoded ZCL frame: Identify:DefaultResponse(command_id=21, status=) +2026-03-15 19:04:40.881 WARNING (MainThread) [zigpy.device_scanner] Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0003 (server): UNSUP_GENERAL_COMMAND; falling back to discover_attributes +2026-03-15 19:04:40.881 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=21, command_id=, *direction=) +2026-03-15 19:04:40.881 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Sending request: Discover_Attributes(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:40.881 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 40, 881545, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=21, profile_id=260, cluster_id=3, data=Serialized[b'\x00\x15\x0c\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:40.932 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=21, command_id=13, *direction=) +2026-03-15 19:04:40.932 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Decoded ZCL frame: Identify:Discover_Attributes_rsp(discovery_complete=, attribute_info=[DiscoverAttributesResponseRecord(attrid=0, datatype=33), DiscoverAttributesResponseRecord(attrid=65533, datatype=33)]) +2026-03-15 19:04:40.944 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=3, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:40.945 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=3, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:40.945 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=22, command_id=, *direction=) +2026-03-15 19:04:40.946 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Sending request: Read_Attributes(attribute_ids=[0, 65533]) +2026-03-15 19:04:40.946 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 40, 946146, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=22, profile_id=260, cluster_id=3, data=Serialized[b'\x00\x16\x00\x00\x00\xfd\xff'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:41.005 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=22, command_id=1, *direction=) +2026-03-15 19:04:41.005 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Decoded ZCL frame: Identify:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=0, status=, value=TypeValue(type=uint16_t, value=0)), ReadAttributeRecord(attrid=65533, status=, value=TypeValue(type=uint16_t, value=1))]) +2026-03-15 19:04:41.017 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=3, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:41.017 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=3, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:41.018 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=23, command_id=, *direction=) +2026-03-15 19:04:41.018 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Sending request: Discover_Commands_Received(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:41.018 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 41, 18284, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=23, profile_id=260, cluster_id=3, data=Serialized[b'\x00\x17\x11\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:41.052 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=23, command_id=11, *direction=) +2026-03-15 19:04:41.052 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Decoded ZCL frame: Identify:DefaultResponse(command_id=17, status=) +2026-03-15 19:04:41.052 WARNING (MainThread) [zigpy.device_scanner] Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0003 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND +2026-03-15 19:04:41.063 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=3, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:41.064 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=3, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:41.064 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=24, command_id=, *direction=) +2026-03-15 19:04:41.064 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Sending request: Discover_Commands_Generated(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:41.064 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 41, 64544, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=24, profile_id=260, cluster_id=3, data=Serialized[b'\x00\x18\x13\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:41.111 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=24, command_id=11, *direction=) +2026-03-15 19:04:41.111 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Decoded ZCL frame: Identify:DefaultResponse(command_id=19, status=) +2026-03-15 19:04:41.111 WARNING (MainThread) [zigpy.device_scanner] Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0003 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND +2026-03-15 19:04:41.122 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=3, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:41.122 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=4, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:41.122 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=25, command_id=, *direction=) +2026-03-15 19:04:41.122 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Sending request: Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:41.123 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 41, 123073, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=25, profile_id=260, cluster_id=4, data=Serialized[b'\x00\x19\x15\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:41.160 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=25, command_id=11, *direction=) +2026-03-15 19:04:41.160 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Decoded ZCL frame: Groups:DefaultResponse(command_id=21, status=) +2026-03-15 19:04:41.160 WARNING (MainThread) [zigpy.device_scanner] Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0004 (server): UNSUP_GENERAL_COMMAND; falling back to discover_attributes +2026-03-15 19:04:41.161 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=26, command_id=, *direction=) +2026-03-15 19:04:41.161 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Sending request: Discover_Attributes(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:41.161 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 41, 161291, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=26, profile_id=260, cluster_id=4, data=Serialized[b'\x00\x1a\x0c\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:41.211 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=26, command_id=13, *direction=) +2026-03-15 19:04:41.211 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Decoded ZCL frame: Groups:Discover_Attributes_rsp(discovery_complete=, attribute_info=[DiscoverAttributesResponseRecord(attrid=0, datatype=24), DiscoverAttributesResponseRecord(attrid=65533, datatype=33)]) +2026-03-15 19:04:41.223 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=4, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:41.223 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=4, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:41.223 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=27, command_id=, *direction=) +2026-03-15 19:04:41.223 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Sending request: Read_Attributes(attribute_ids=[0, 65533]) +2026-03-15 19:04:41.224 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 41, 224125, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=27, profile_id=260, cluster_id=4, data=Serialized[b'\x00\x1b\x00\x00\x00\xfd\xff'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:41.261 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=27, command_id=1, *direction=) +2026-03-15 19:04:41.261 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Decoded ZCL frame: Groups:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=0, status=, value=TypeValue(type=bitmap8, value=)), ReadAttributeRecord(attrid=65533, status=, value=TypeValue(type=uint16_t, value=1))]) +2026-03-15 19:04:41.273 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=4, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:41.274 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=4, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:41.274 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=28, command_id=, *direction=) +2026-03-15 19:04:41.274 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Sending request: Discover_Commands_Received(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:41.274 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 41, 274495, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=28, profile_id=260, cluster_id=4, data=Serialized[b'\x00\x1c\x11\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:41.324 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=28, command_id=11, *direction=) +2026-03-15 19:04:41.324 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Decoded ZCL frame: Groups:DefaultResponse(command_id=17, status=) +2026-03-15 19:04:41.324 WARNING (MainThread) [zigpy.device_scanner] Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0004 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND +2026-03-15 19:04:41.336 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=4, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:41.336 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=4, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:41.336 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=29, command_id=, *direction=) +2026-03-15 19:04:41.336 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Sending request: Discover_Commands_Generated(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:41.337 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 41, 337059, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=29, profile_id=260, cluster_id=4, data=Serialized[b'\x00\x1d\x13\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:41.370 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=29, command_id=11, *direction=) +2026-03-15 19:04:41.370 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Decoded ZCL frame: Groups:DefaultResponse(command_id=19, status=) +2026-03-15 19:04:41.371 WARNING (MainThread) [zigpy.device_scanner] Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0004 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND +2026-03-15 19:04:41.382 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=4, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:41.382 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=5, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:41.382 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=30, command_id=, *direction=) +2026-03-15 19:04:41.382 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Sending request: Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:41.382 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 41, 382848, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=30, profile_id=260, cluster_id=5, data=Serialized[b'\x00\x1e\x15\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:41.415 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=30, command_id=11, *direction=) +2026-03-15 19:04:41.415 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Decoded ZCL frame: Scenes:DefaultResponse(command_id=21, status=) +2026-03-15 19:04:41.415 WARNING (MainThread) [zigpy.device_scanner] Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0005 (server): UNSUP_GENERAL_COMMAND; falling back to discover_attributes +2026-03-15 19:04:41.415 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=31, command_id=, *direction=) +2026-03-15 19:04:41.416 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Sending request: Discover_Attributes(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:41.416 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 41, 416124, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=31, profile_id=260, cluster_id=5, data=Serialized[b'\x00\x1f\x0c\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:41.455 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=31, command_id=13, *direction=) +2026-03-15 19:04:41.455 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Decoded ZCL frame: Scenes:Discover_Attributes_rsp(discovery_complete=, attribute_info=[DiscoverAttributesResponseRecord(attrid=0, datatype=32), DiscoverAttributesResponseRecord(attrid=1, datatype=32), DiscoverAttributesResponseRecord(attrid=2, datatype=33), DiscoverAttributesResponseRecord(attrid=3, datatype=16), DiscoverAttributesResponseRecord(attrid=4, datatype=24), DiscoverAttributesResponseRecord(attrid=65533, datatype=33)]) +2026-03-15 19:04:41.467 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=5, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:41.467 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=5, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:41.468 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=32, command_id=, *direction=) +2026-03-15 19:04:41.468 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Sending request: Read_Attributes(attribute_ids=[0, 1, 2]) +2026-03-15 19:04:41.468 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 41, 468579, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=32, profile_id=260, cluster_id=5, data=Serialized[b'\x00 \x00\x00\x00\x01\x00\x02\x00'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:41.563 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=32, command_id=1, *direction=) +2026-03-15 19:04:41.563 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Decoded ZCL frame: Scenes:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=0, status=, value=TypeValue(type=uint8_t, value=1)), ReadAttributeRecord(attrid=1, status=, value=TypeValue(type=uint8_t, value=0)), ReadAttributeRecord(attrid=2, status=, value=TypeValue(type=uint16_t, value=0))]) +2026-03-15 19:04:41.575 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=33, command_id=, *direction=) +2026-03-15 19:04:41.575 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Sending request: Read_Attributes(attribute_ids=[3, 4, 65533]) +2026-03-15 19:04:41.575 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 41, 575503, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=33, profile_id=260, cluster_id=5, data=Serialized[b'\x00!\x00\x03\x00\x04\x00\xfd\xff'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:41.627 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=33, command_id=1, *direction=) +2026-03-15 19:04:41.627 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Decoded ZCL frame: Scenes:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=3, status=, value=TypeValue(type=Bool, value=)), ReadAttributeRecord(attrid=4, status=, value=TypeValue(type=bitmap8, value=)), ReadAttributeRecord(attrid=65533, status=, value=TypeValue(type=uint16_t, value=1))]) +2026-03-15 19:04:41.639 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=5, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:41.640 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=5, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:41.640 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=34, command_id=, *direction=) +2026-03-15 19:04:41.640 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Sending request: Discover_Commands_Received(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:41.640 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 41, 640630, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=34, profile_id=260, cluster_id=5, data=Serialized[b'\x00"\x11\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:41.683 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=34, command_id=11, *direction=) +2026-03-15 19:04:41.683 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Decoded ZCL frame: Scenes:DefaultResponse(command_id=17, status=) +2026-03-15 19:04:41.683 WARNING (MainThread) [zigpy.device_scanner] Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0005 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND +2026-03-15 19:04:41.695 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=5, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:41.695 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=5, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:41.695 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=35, command_id=, *direction=) +2026-03-15 19:04:41.695 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Sending request: Discover_Commands_Generated(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:41.696 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 41, 696068, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=35, profile_id=260, cluster_id=5, data=Serialized[b'\x00#\x13\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:41.735 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=35, command_id=11, *direction=) +2026-03-15 19:04:41.735 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Decoded ZCL frame: Scenes:DefaultResponse(command_id=19, status=) +2026-03-15 19:04:41.735 WARNING (MainThread) [zigpy.device_scanner] Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0005 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND +2026-03-15 19:04:41.747 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=5, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:41.747 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=6, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:41.747 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=36, command_id=, *direction=) +2026-03-15 19:04:41.747 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Sending request: Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:41.747 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 41, 747532, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=36, profile_id=260, cluster_id=6, data=Serialized[b'\x00$\x15\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:41.783 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=36, command_id=11, *direction=) +2026-03-15 19:04:41.783 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Decoded ZCL frame: OnOff:DefaultResponse(command_id=21, status=) +2026-03-15 19:04:41.784 WARNING (MainThread) [zigpy.device_scanner] Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0006 (server): UNSUP_GENERAL_COMMAND; falling back to discover_attributes +2026-03-15 19:04:41.784 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=37, command_id=, *direction=) +2026-03-15 19:04:41.784 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Sending request: Discover_Attributes(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:41.784 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 41, 784311, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=37, profile_id=260, cluster_id=6, data=Serialized[b'\x00%\x0c\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:41.829 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=37, command_id=13, *direction=) +2026-03-15 19:04:41.829 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Decoded ZCL frame: OnOff:Discover_Attributes_rsp(discovery_complete=, attribute_info=[DiscoverAttributesResponseRecord(attrid=0, datatype=16), DiscoverAttributesResponseRecord(attrid=16384, datatype=16), DiscoverAttributesResponseRecord(attrid=16385, datatype=33), DiscoverAttributesResponseRecord(attrid=16386, datatype=33), DiscoverAttributesResponseRecord(attrid=16387, datatype=48), DiscoverAttributesResponseRecord(attrid=65533, datatype=33)]) +2026-03-15 19:04:41.841 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=6, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:41.842 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=6, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:41.842 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=38, command_id=, *direction=) +2026-03-15 19:04:41.842 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Sending request: Read_Attributes(attribute_ids=[0, 16384, 16385]) +2026-03-15 19:04:41.843 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 41, 843075, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=38, profile_id=260, cluster_id=6, data=Serialized[b'\x00&\x00\x00\x00\x00@\x01@'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:41.887 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=38, command_id=1, *direction=) +2026-03-15 19:04:41.887 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Decoded ZCL frame: OnOff:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=0, status=, value=TypeValue(type=Bool, value=)), ReadAttributeRecord(attrid=16384, status=, value=TypeValue(type=Bool, value=)), ReadAttributeRecord(attrid=16385, status=, value=TypeValue(type=uint16_t, value=0))]) +2026-03-15 19:04:41.898 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=39, command_id=, *direction=) +2026-03-15 19:04:41.898 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Sending request: Read_Attributes(attribute_ids=[16386, 16387, 65533]) +2026-03-15 19:04:41.898 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 41, 898810, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=39, profile_id=260, cluster_id=6, data=Serialized[b"\x00'\x00\x02@\x03@\xfd\xff"], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:41.936 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=39, command_id=1, *direction=) +2026-03-15 19:04:41.936 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Decoded ZCL frame: OnOff:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=16386, status=, value=TypeValue(type=uint16_t, value=0)), ReadAttributeRecord(attrid=16387, status=, value=TypeValue(type=enum8, value=)), ReadAttributeRecord(attrid=65533, status=, value=TypeValue(type=uint16_t, value=1))]) +2026-03-15 19:04:41.948 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=6, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:41.948 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=6, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:41.948 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=40, command_id=, *direction=) +2026-03-15 19:04:41.949 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Sending request: Discover_Commands_Received(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:41.949 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 41, 949129, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=40, profile_id=260, cluster_id=6, data=Serialized[b'\x00(\x11\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:41.987 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=40, command_id=11, *direction=) +2026-03-15 19:04:41.987 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Decoded ZCL frame: OnOff:DefaultResponse(command_id=17, status=) +2026-03-15 19:04:41.987 WARNING (MainThread) [zigpy.device_scanner] Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0006 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND +2026-03-15 19:04:41.998 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=6, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:41.999 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=6, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:41.999 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=41, command_id=, *direction=) +2026-03-15 19:04:41.999 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Sending request: Discover_Commands_Generated(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:41.999 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 41, 999616, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=41, profile_id=260, cluster_id=6, data=Serialized[b'\x00)\x13\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:42.032 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=41, command_id=11, *direction=) +2026-03-15 19:04:42.032 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Decoded ZCL frame: OnOff:DefaultResponse(command_id=19, status=) +2026-03-15 19:04:42.032 WARNING (MainThread) [zigpy.device_scanner] Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0006 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND +2026-03-15 19:04:42.042 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=6, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:42.043 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=8, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:42.043 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=42, command_id=, *direction=) +2026-03-15 19:04:42.043 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Sending request: Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:42.043 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 42, 43323, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=42, profile_id=260, cluster_id=8, data=Serialized[b'\x00*\x15\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:42.082 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=42, command_id=11, *direction=) +2026-03-15 19:04:42.082 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Decoded ZCL frame: LevelControl:DefaultResponse(command_id=21, status=) +2026-03-15 19:04:42.082 WARNING (MainThread) [zigpy.device_scanner] Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0008 (server): UNSUP_GENERAL_COMMAND; falling back to discover_attributes +2026-03-15 19:04:42.082 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=43, command_id=, *direction=) +2026-03-15 19:04:42.082 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Sending request: Discover_Attributes(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:42.082 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 42, 82853, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=43, profile_id=260, cluster_id=8, data=Serialized[b'\x00+\x0c\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:42.122 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=43, command_id=13, *direction=) +2026-03-15 19:04:42.122 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Decoded ZCL frame: LevelControl:Discover_Attributes_rsp(discovery_complete=, attribute_info=[DiscoverAttributesResponseRecord(attrid=0, datatype=32), DiscoverAttributesResponseRecord(attrid=1, datatype=33), DiscoverAttributesResponseRecord(attrid=15, datatype=24), DiscoverAttributesResponseRecord(attrid=16384, datatype=32), DiscoverAttributesResponseRecord(attrid=65533, datatype=33)]) +2026-03-15 19:04:42.134 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=8, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:42.135 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=8, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:42.135 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=44, command_id=, *direction=) +2026-03-15 19:04:42.135 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Sending request: Read_Attributes(attribute_ids=[0, 1, 15]) +2026-03-15 19:04:42.135 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 42, 135692, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=44, profile_id=260, cluster_id=8, data=Serialized[b'\x00,\x00\x00\x00\x01\x00\x0f\x00'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:42.177 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=44, command_id=1, *direction=) +2026-03-15 19:04:42.177 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Decoded ZCL frame: LevelControl:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=0, status=, value=TypeValue(type=uint8_t, value=144)), ReadAttributeRecord(attrid=1, status=, value=TypeValue(type=uint16_t, value=0)), ReadAttributeRecord(attrid=15, status=, value=TypeValue(type=bitmap8, value=))]) +2026-03-15 19:04:42.271 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=45, command_id=, *direction=) +2026-03-15 19:04:42.271 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Sending request: Read_Attributes(attribute_ids=[16384, 65533]) +2026-03-15 19:04:42.271 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 42, 271918, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=45, profile_id=260, cluster_id=8, data=Serialized[b'\x00-\x00\x00@\xfd\xff'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:42.341 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=45, command_id=1, *direction=) +2026-03-15 19:04:42.341 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Decoded ZCL frame: LevelControl:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=16384, status=, value=TypeValue(type=uint8_t, value=255)), ReadAttributeRecord(attrid=65533, status=, value=TypeValue(type=uint16_t, value=1))]) +2026-03-15 19:04:42.353 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=8, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:42.353 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=8, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:42.354 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=46, command_id=, *direction=) +2026-03-15 19:04:42.354 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Sending request: Discover_Commands_Received(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:42.354 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 42, 354249, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=46, profile_id=260, cluster_id=8, data=Serialized[b'\x00.\x11\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:42.387 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=46, command_id=11, *direction=) +2026-03-15 19:04:42.387 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Decoded ZCL frame: LevelControl:DefaultResponse(command_id=17, status=) +2026-03-15 19:04:42.387 WARNING (MainThread) [zigpy.device_scanner] Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0008 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND +2026-03-15 19:04:42.398 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=8, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:42.399 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=8, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:42.399 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=47, command_id=, *direction=) +2026-03-15 19:04:42.399 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Sending request: Discover_Commands_Generated(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:42.399 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 42, 399885, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=47, profile_id=260, cluster_id=8, data=Serialized[b'\x00/\x13\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:42.446 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=47, command_id=11, *direction=) +2026-03-15 19:04:42.446 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Decoded ZCL frame: LevelControl:DefaultResponse(command_id=19, status=) +2026-03-15 19:04:42.446 WARNING (MainThread) [zigpy.device_scanner] Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0008 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND +2026-03-15 19:04:42.457 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=8, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:42.457 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=768, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:42.457 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=48, command_id=, *direction=) +2026-03-15 19:04:42.458 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request: Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:42.458 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 42, 458139, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=48, profile_id=260, cluster_id=768, data=Serialized[b'\x000\x15\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:42.491 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=48, command_id=11, *direction=) +2026-03-15 19:04:42.491 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame: Color:DefaultResponse(command_id=21, status=) +2026-03-15 19:04:42.492 WARNING (MainThread) [zigpy.device_scanner] Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0300 (server): UNSUP_GENERAL_COMMAND; falling back to discover_attributes +2026-03-15 19:04:42.492 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=49, command_id=, *direction=) +2026-03-15 19:04:42.492 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request: Discover_Attributes(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:42.492 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 42, 492344, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=49, profile_id=260, cluster_id=768, data=Serialized[b'\x001\x0c\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:42.555 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=49, command_id=13, *direction=) +2026-03-15 19:04:42.555 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame: Color:Discover_Attributes_rsp(discovery_complete=, attribute_info=[DiscoverAttributesResponseRecord(attrid=0, datatype=32), DiscoverAttributesResponseRecord(attrid=1, datatype=32), DiscoverAttributesResponseRecord(attrid=2, datatype=33), DiscoverAttributesResponseRecord(attrid=3, datatype=33), DiscoverAttributesResponseRecord(attrid=4, datatype=33), DiscoverAttributesResponseRecord(attrid=7, datatype=33), DiscoverAttributesResponseRecord(attrid=8, datatype=48), DiscoverAttributesResponseRecord(attrid=15, datatype=24), DiscoverAttributesResponseRecord(attrid=16, datatype=32), DiscoverAttributesResponseRecord(attrid=17, datatype=33), DiscoverAttributesResponseRecord(attrid=18, datatype=33), DiscoverAttributesResponseRecord(attrid=19, datatype=32), DiscoverAttributesResponseRecord(attrid=21, datatype=33), DiscoverAttributesResponseRecord(attrid=22, datatype=33), DiscoverAttributesResponseRecord(attrid=23, datatype=32), DiscoverAttributesResponseRecord(attrid=25, datatype=33)]) +2026-03-15 19:04:42.567 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=50, command_id=, *direction=) +2026-03-15 19:04:42.567 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request: Discover_Attributes(start_attribute_id=26, max_attribute_ids=16) +2026-03-15 19:04:42.567 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 42, 567281, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=50, profile_id=260, cluster_id=768, data=Serialized[b'\x002\x0c\x1a\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:42.621 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=50, command_id=13, *direction=) +2026-03-15 19:04:42.621 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame: Color:Discover_Attributes_rsp(discovery_complete=, attribute_info=[DiscoverAttributesResponseRecord(attrid=26, datatype=33), DiscoverAttributesResponseRecord(attrid=27, datatype=32), DiscoverAttributesResponseRecord(attrid=48, datatype=33), DiscoverAttributesResponseRecord(attrid=49, datatype=33), DiscoverAttributesResponseRecord(attrid=50, datatype=33), DiscoverAttributesResponseRecord(attrid=51, datatype=33), DiscoverAttributesResponseRecord(attrid=52, datatype=32), DiscoverAttributesResponseRecord(attrid=54, datatype=33), DiscoverAttributesResponseRecord(attrid=55, datatype=33), DiscoverAttributesResponseRecord(attrid=56, datatype=32), DiscoverAttributesResponseRecord(attrid=58, datatype=33), DiscoverAttributesResponseRecord(attrid=59, datatype=33), DiscoverAttributesResponseRecord(attrid=60, datatype=32), DiscoverAttributesResponseRecord(attrid=16384, datatype=33), DiscoverAttributesResponseRecord(attrid=16385, datatype=48), DiscoverAttributesResponseRecord(attrid=16386, datatype=32)]) +2026-03-15 19:04:42.632 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=51, command_id=, *direction=) +2026-03-15 19:04:42.633 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request: Discover_Attributes(start_attribute_id=16387, max_attribute_ids=16) +2026-03-15 19:04:42.633 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 42, 633182, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=51, profile_id=260, cluster_id=768, data=Serialized[b'\x003\x0c\x03@\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:42.676 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=51, command_id=13, *direction=) +2026-03-15 19:04:42.676 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame: Color:Discover_Attributes_rsp(discovery_complete=, attribute_info=[DiscoverAttributesResponseRecord(attrid=16387, datatype=32), DiscoverAttributesResponseRecord(attrid=16388, datatype=33), DiscoverAttributesResponseRecord(attrid=16389, datatype=33), DiscoverAttributesResponseRecord(attrid=16390, datatype=33), DiscoverAttributesResponseRecord(attrid=16394, datatype=25), DiscoverAttributesResponseRecord(attrid=16395, datatype=33), DiscoverAttributesResponseRecord(attrid=16396, datatype=33), DiscoverAttributesResponseRecord(attrid=16397, datatype=33), DiscoverAttributesResponseRecord(attrid=16400, datatype=33), DiscoverAttributesResponseRecord(attrid=65533, datatype=33)]) +2026-03-15 19:04:42.688 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=768, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:42.689 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=768, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:42.689 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=52, command_id=, *direction=) +2026-03-15 19:04:42.689 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request: Read_Attributes(attribute_ids=[0, 1, 2]) +2026-03-15 19:04:42.689 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 42, 689714, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=52, profile_id=260, cluster_id=768, data=Serialized[b'\x004\x00\x00\x00\x01\x00\x02\x00'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:42.782 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=52, command_id=1, *direction=) +2026-03-15 19:04:42.782 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame: Color:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=0, status=, value=TypeValue(type=uint8_t, value=30)), ReadAttributeRecord(attrid=1, status=, value=TypeValue(type=uint8_t, value=199)), ReadAttributeRecord(attrid=2, status=, value=TypeValue(type=uint16_t, value=0))]) +2026-03-15 19:04:42.794 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=53, command_id=, *direction=) +2026-03-15 19:04:42.794 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request: Read_Attributes(attribute_ids=[3, 4, 7]) +2026-03-15 19:04:42.795 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 42, 795061, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=53, profile_id=260, cluster_id=768, data=Serialized[b'\x005\x00\x03\x00\x04\x00\x07\x00'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:42.843 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=53, command_id=1, *direction=) +2026-03-15 19:04:42.843 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame: Color:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=3, status=, value=TypeValue(type=uint16_t, value=32862)), ReadAttributeRecord(attrid=4, status=, value=TypeValue(type=uint16_t, value=27215)), ReadAttributeRecord(attrid=7, status=, value=TypeValue(type=uint16_t, value=443))]) +2026-03-15 19:04:42.855 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=54, command_id=, *direction=) +2026-03-15 19:04:42.855 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request: Read_Attributes(attribute_ids=[8, 15, 16]) +2026-03-15 19:04:42.855 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 42, 855560, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=54, profile_id=260, cluster_id=768, data=Serialized[b'\x006\x00\x08\x00\x0f\x00\x10\x00'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:42.900 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=54, command_id=1, *direction=) +2026-03-15 19:04:42.900 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame: Color:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=8, status=, value=TypeValue(type=enum8, value=)), ReadAttributeRecord(attrid=15, status=, value=TypeValue(type=bitmap8, value=)), ReadAttributeRecord(attrid=16, status=, value=TypeValue(type=uint8_t, value=3))]) +2026-03-15 19:04:42.911 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=55, command_id=, *direction=) +2026-03-15 19:04:42.911 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request: Read_Attributes(attribute_ids=[17, 18, 19]) +2026-03-15 19:04:42.911 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 42, 911855, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=55, profile_id=260, cluster_id=768, data=Serialized[b'\x007\x00\x11\x00\x12\x00\x13\x00'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:43.005 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=55, command_id=1, *direction=) +2026-03-15 19:04:43.005 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame: Color:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=17, status=, value=TypeValue(type=uint16_t, value=45317)), ReadAttributeRecord(attrid=18, status=, value=TypeValue(type=uint16_t, value=20204)), ReadAttributeRecord(attrid=19, status=, value=TypeValue(type=uint8_t, value=64))]) +2026-03-15 19:04:43.017 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=56, command_id=, *direction=) +2026-03-15 19:04:43.017 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request: Read_Attributes(attribute_ids=[21, 22, 23]) +2026-03-15 19:04:43.017 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 43, 17594, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=56, profile_id=260, cluster_id=768, data=Serialized[b'\x008\x00\x15\x00\x16\x00\x17\x00'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:43.062 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=56, command_id=1, *direction=) +2026-03-15 19:04:43.062 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame: Color:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=21, status=, value=TypeValue(type=uint16_t, value=11141)), ReadAttributeRecord(attrid=22, status=, value=TypeValue(type=uint16_t, value=45875)), ReadAttributeRecord(attrid=23, status=, value=TypeValue(type=uint8_t, value=254))]) +2026-03-15 19:04:43.074 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=57, command_id=, *direction=) +2026-03-15 19:04:43.074 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request: Read_Attributes(attribute_ids=[25, 26, 27]) +2026-03-15 19:04:43.074 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 43, 74634, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=57, profile_id=260, cluster_id=768, data=Serialized[b'\x009\x00\x19\x00\x1a\x00\x1b\x00'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:43.252 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=57, command_id=1, *direction=) +2026-03-15 19:04:43.252 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame: Color:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=25, status=, value=TypeValue(type=uint16_t, value=10040)), ReadAttributeRecord(attrid=26, status=, value=TypeValue(type=uint16_t, value=3116)), ReadAttributeRecord(attrid=27, status=, value=TypeValue(type=uint8_t, value=11))]) +2026-03-15 19:04:43.264 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=58, command_id=, *direction=) +2026-03-15 19:04:43.264 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request: Read_Attributes(attribute_ids=[48, 49, 50]) +2026-03-15 19:04:43.264 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 43, 264560, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=58, profile_id=260, cluster_id=768, data=Serialized[b'\x00:\x000\x001\x002\x00'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:43.363 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=58, command_id=1, *direction=) +2026-03-15 19:04:43.363 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame: Color:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=48, status=, value=TypeValue(type=uint16_t, value=24929)), ReadAttributeRecord(attrid=49, status=, value=TypeValue(type=uint16_t, value=24693)), ReadAttributeRecord(attrid=50, status=, value=TypeValue(type=uint16_t, value=45317))]) +2026-03-15 19:04:43.375 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=59, command_id=, *direction=) +2026-03-15 19:04:43.375 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request: Read_Attributes(attribute_ids=[51, 52, 54]) +2026-03-15 19:04:43.375 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 43, 375513, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=59, profile_id=260, cluster_id=768, data=Serialized[b'\x00;\x003\x004\x006\x00'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:43.413 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=59, command_id=1, *direction=) +2026-03-15 19:04:43.413 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame: Color:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=51, status=, value=TypeValue(type=uint16_t, value=20204)), ReadAttributeRecord(attrid=52, status=, value=TypeValue(type=uint8_t, value=64)), ReadAttributeRecord(attrid=54, status=, value=TypeValue(type=uint16_t, value=11141))]) +2026-03-15 19:04:43.425 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=60, command_id=, *direction=) +2026-03-15 19:04:43.425 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request: Read_Attributes(attribute_ids=[55, 56, 58]) +2026-03-15 19:04:43.425 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 43, 425524, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=60, profile_id=260, cluster_id=768, data=Serialized[b'\x00<\x007\x008\x00:\x00'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:43.608 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=60, command_id=1, *direction=) +2026-03-15 19:04:43.608 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame: Color:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=55, status=, value=TypeValue(type=uint16_t, value=45875)), ReadAttributeRecord(attrid=56, status=, value=TypeValue(type=uint8_t, value=254)), ReadAttributeRecord(attrid=58, status=, value=TypeValue(type=uint16_t, value=10040))]) +2026-03-15 19:04:43.619 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=61, command_id=, *direction=) +2026-03-15 19:04:43.619 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request: Read_Attributes(attribute_ids=[59, 60, 16384]) +2026-03-15 19:04:43.620 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 43, 620098, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=61, profile_id=260, cluster_id=768, data=Serialized[b'\x00=\x00;\x00<\x00\x00@'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:43.703 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=61, command_id=1, *direction=) +2026-03-15 19:04:43.703 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame: Color:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=59, status=, value=TypeValue(type=uint16_t, value=3116)), ReadAttributeRecord(attrid=60, status=, value=TypeValue(type=uint8_t, value=11)), ReadAttributeRecord(attrid=16384, status=, value=TypeValue(type=uint16_t, value=7687))]) +2026-03-15 19:04:43.715 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=62, command_id=, *direction=) +2026-03-15 19:04:43.715 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request: Read_Attributes(attribute_ids=[16385, 16386, 16387]) +2026-03-15 19:04:43.715 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 43, 715583, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=62, profile_id=260, cluster_id=768, data=Serialized[b'\x00>\x00\x01@\x02@\x03@'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:43.766 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=62, command_id=1, *direction=) +2026-03-15 19:04:43.766 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame: Color:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=16385, status=, value=TypeValue(type=enum8, value=)), ReadAttributeRecord(attrid=16386, status=, value=TypeValue(type=uint8_t, value=0)), ReadAttributeRecord(attrid=16387, status=, value=TypeValue(type=uint8_t, value=0))]) +2026-03-15 19:04:43.777 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=63, command_id=, *direction=) +2026-03-15 19:04:43.777 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request: Read_Attributes(attribute_ids=[16388, 16389, 16390]) +2026-03-15 19:04:43.777 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 43, 777958, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=63, profile_id=260, cluster_id=768, data=Serialized[b'\x00?\x00\x04@\x05@\x06@'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:43.815 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=63, command_id=1, *direction=) +2026-03-15 19:04:43.815 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame: Color:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=16388, status=, value=TypeValue(type=uint16_t, value=25)), ReadAttributeRecord(attrid=16389, status=, value=TypeValue(type=uint16_t, value=8960)), ReadAttributeRecord(attrid=16390, status=, value=TypeValue(type=uint16_t, value=7687))]) +2026-03-15 19:04:43.826 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=64, command_id=, *direction=) +2026-03-15 19:04:43.826 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request: Read_Attributes(attribute_ids=[16394, 16395, 16396]) +2026-03-15 19:04:43.827 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 43, 827024, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=64, profile_id=260, cluster_id=768, data=Serialized[b'\x00@\x00\n@\x0b@\x0c@'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:43.937 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=64, command_id=1, *direction=) +2026-03-15 19:04:43.937 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame: Color:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=16394, status=, value=TypeValue(type=bitmap16, value=)), ReadAttributeRecord(attrid=16395, status=, value=TypeValue(type=uint16_t, value=153)), ReadAttributeRecord(attrid=16396, status=, value=TypeValue(type=uint16_t, value=500))]) +2026-03-15 19:04:43.948 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=65, command_id=, *direction=) +2026-03-15 19:04:43.948 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request: Read_Attributes(attribute_ids=[16397, 16400, 65533]) +2026-03-15 19:04:43.948 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 43, 948789, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=65, profile_id=260, cluster_id=768, data=Serialized[b'\x00A\x00\r@\x10@\xfd\xff'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:44.072 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=65, command_id=1, *direction=) +2026-03-15 19:04:44.072 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame: Color:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=16397, status=, value=TypeValue(type=uint16_t, value=250)), ReadAttributeRecord(attrid=16400, status=, value=TypeValue(type=uint16_t, value=366)), ReadAttributeRecord(attrid=65533, status=, value=TypeValue(type=uint16_t, value=1))]) +2026-03-15 19:04:44.083 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=768, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:44.084 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=768, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:44.084 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=66, command_id=, *direction=) +2026-03-15 19:04:44.084 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request: Discover_Commands_Received(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:44.084 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 44, 84356, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=66, profile_id=260, cluster_id=768, data=Serialized[b'\x00B\x11\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:44.131 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=66, command_id=11, *direction=) +2026-03-15 19:04:44.131 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame: Color:DefaultResponse(command_id=17, status=) +2026-03-15 19:04:44.132 WARNING (MainThread) [zigpy.device_scanner] Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0300 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND +2026-03-15 19:04:44.143 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=768, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:44.143 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=768, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:44.144 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=67, command_id=, *direction=) +2026-03-15 19:04:44.144 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request: Discover_Commands_Generated(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:44.144 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 44, 144254, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=67, profile_id=260, cluster_id=768, data=Serialized[b'\x00C\x13\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:44.190 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=67, command_id=11, *direction=) +2026-03-15 19:04:44.190 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame: Color:DefaultResponse(command_id=19, status=) +2026-03-15 19:04:44.191 WARNING (MainThread) [zigpy.device_scanner] Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0300 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND +2026-03-15 19:04:44.202 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=768, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:44.202 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=4096, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:44.202 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=68, command_id=, *direction=) +2026-03-15 19:04:44.202 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Sending request: Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:44.202 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 44, 202770, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=68, profile_id=260, cluster_id=4096, data=Serialized[b'\x00D\x15\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:44.433 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=68, command_id=11, *direction=) +2026-03-15 19:04:44.433 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Decoded ZCL frame: LightLink:DefaultResponse(command_id=21, status=) +2026-03-15 19:04:44.434 WARNING (MainThread) [zigpy.device_scanner] Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x1000 (server): UNSUP_GENERAL_COMMAND; falling back to discover_attributes +2026-03-15 19:04:44.434 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=69, command_id=, *direction=) +2026-03-15 19:04:44.434 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Sending request: Discover_Attributes(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:44.434 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 44, 434892, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=69, profile_id=260, cluster_id=4096, data=Serialized[b'\x00E\x0c\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:44.646 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=69, command_id=13, *direction=) +2026-03-15 19:04:44.646 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Decoded ZCL frame: LightLink:Discover_Attributes_rsp(discovery_complete=, attribute_info=[DiscoverAttributesResponseRecord(attrid=65533, datatype=33)]) +2026-03-15 19:04:44.658 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=4096, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:44.659 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=4096, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:44.659 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=70, command_id=, *direction=) +2026-03-15 19:04:44.659 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Sending request: Read_Attributes(attribute_ids=[65533]) +2026-03-15 19:04:44.659 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 44, 659928, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=70, profile_id=260, cluster_id=4096, data=Serialized[b'\x00F\x00\xfd\xff'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:44.834 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=70, command_id=1, *direction=) +2026-03-15 19:04:44.834 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Decoded ZCL frame: LightLink:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=65533, status=, value=TypeValue(type=uint16_t, value=1))]) +2026-03-15 19:04:44.846 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=4096, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:44.847 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=4096, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:44.847 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=71, command_id=, *direction=) +2026-03-15 19:04:44.847 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Sending request: Discover_Commands_Received(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:44.847 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 44, 847940, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=71, profile_id=260, cluster_id=4096, data=Serialized[b'\x00G\x11\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:44.943 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=71, command_id=11, *direction=) +2026-03-15 19:04:44.943 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Decoded ZCL frame: LightLink:DefaultResponse(command_id=17, status=) +2026-03-15 19:04:44.943 WARNING (MainThread) [zigpy.device_scanner] Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x1000 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND +2026-03-15 19:04:44.954 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=4096, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:44.955 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=4096, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:44.955 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=72, command_id=, *direction=) +2026-03-15 19:04:44.955 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Sending request: Discover_Commands_Generated(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:44.955 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 44, 955899, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=72, profile_id=260, cluster_id=4096, data=Serialized[b'\x00H\x13\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:45.158 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=72, command_id=11, *direction=) +2026-03-15 19:04:45.158 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Decoded ZCL frame: LightLink:DefaultResponse(command_id=19, status=) +2026-03-15 19:04:45.158 WARNING (MainThread) [zigpy.device_scanner] Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x1000 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND +2026-03-15 19:04:45.170 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=4096, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:45.170 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=64513, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:45.170 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=73, command_id=, *direction=) +2026-03-15 19:04:45.170 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Sending request: Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:45.170 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 45, 170772, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=73, profile_id=260, cluster_id=64513, data=Serialized[b'\x00I\x15\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:45.233 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=73, command_id=11, *direction=) +2026-03-15 19:04:45.234 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Decoded ZCL frame: ManufacturerSpecificCluster:DefaultResponse(command_id=21, status=) +2026-03-15 19:04:45.234 WARNING (MainThread) [zigpy.device_scanner] Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0xfc01 (server): UNSUP_GENERAL_COMMAND; falling back to discover_attributes +2026-03-15 19:04:45.234 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=74, command_id=, *direction=) +2026-03-15 19:04:45.234 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Sending request: Discover_Attributes(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:45.234 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 45, 234538, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=74, profile_id=260, cluster_id=64513, data=Serialized[b'\x00J\x0c\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:45.458 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=74, command_id=13, *direction=) +2026-03-15 19:04:45.458 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Decoded ZCL frame: ManufacturerSpecificCluster:Discover_Attributes_rsp(discovery_complete=, attribute_info=[]) +2026-03-15 19:04:45.470 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=64513, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:45.471 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=64513, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:45.472 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=64513, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:45.473 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=64513, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:45.473 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=75, command_id=, *direction=) +2026-03-15 19:04:45.473 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Sending request: Discover_Commands_Received(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:45.473 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 45, 473949, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=75, profile_id=260, cluster_id=64513, data=Serialized[b'\x00K\x11\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:45.517 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=75, command_id=11, *direction=) +2026-03-15 19:04:45.517 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Decoded ZCL frame: ManufacturerSpecificCluster:DefaultResponse(command_id=17, status=) +2026-03-15 19:04:45.517 WARNING (MainThread) [zigpy.device_scanner] Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0xfc01 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND +2026-03-15 19:04:45.529 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=64513, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:45.530 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=64513, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:45.530 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=76, command_id=, *direction=) +2026-03-15 19:04:45.530 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Sending request: Discover_Commands_Generated(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:45.530 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 45, 530556, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=76, profile_id=260, cluster_id=64513, data=Serialized[b'\x00L\x13\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:45.572 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=76, command_id=11, *direction=) +2026-03-15 19:04:45.572 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Decoded ZCL frame: ManufacturerSpecificCluster:DefaultResponse(command_id=19, status=) +2026-03-15 19:04:45.572 WARNING (MainThread) [zigpy.device_scanner] Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0xfc01 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND +2026-03-15 19:04:45.584 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=64513, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:45.584 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=64515, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:45.584 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=77, command_id=, *direction=) +2026-03-15 19:04:45.584 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Sending request: Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:45.584 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 45, 584671, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=77, profile_id=260, cluster_id=64515, data=Serialized[b'\x00M\x15\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:45.669 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=77, command_id=11, *direction=) +2026-03-15 19:04:45.669 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Decoded ZCL frame: PhilipsHueLightCluster:DefaultResponse(command_id=21, status=) +2026-03-15 19:04:45.669 WARNING (MainThread) [zigpy.device_scanner] Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0xfc03 (server): UNSUP_GENERAL_COMMAND; falling back to discover_attributes +2026-03-15 19:04:45.669 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=78, command_id=, *direction=) +2026-03-15 19:04:45.669 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Sending request: Discover_Attributes(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:45.669 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 45, 669875, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=78, profile_id=260, cluster_id=64515, data=Serialized[b'\x00N\x0c\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:45.740 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=78, command_id=13, *direction=) +2026-03-15 19:04:45.740 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Decoded ZCL frame: PhilipsHueLightCluster:Discover_Attributes_rsp(discovery_complete=, attribute_info=[]) +2026-03-15 19:04:45.751 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=64515, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:45.752 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=64515, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:45.752 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=64515, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:45.753 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=64515, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:45.753 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=79, command_id=, *direction=) +2026-03-15 19:04:45.753 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Sending request: Discover_Commands_Received(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:45.753 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 45, 753624, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=79, profile_id=260, cluster_id=64515, data=Serialized[b'\x00O\x11\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:45.857 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=79, command_id=11, *direction=) +2026-03-15 19:04:45.857 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Decoded ZCL frame: PhilipsHueLightCluster:DefaultResponse(command_id=17, status=) +2026-03-15 19:04:45.857 WARNING (MainThread) [zigpy.device_scanner] Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0xfc03 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND +2026-03-15 19:04:45.869 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=64515, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:45.870 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=64515, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:45.870 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Sending request header: ZCLHeader(frame_control=FrameControl<0x00>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=80, command_id=, *direction=) +2026-03-15 19:04:45.870 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Sending request: Discover_Commands_Generated(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:45.870 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 45, 870594, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=80, profile_id=260, cluster_id=64515, data=Serialized[b'\x00P\x13\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:45.923 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=80, command_id=11, *direction=) +2026-03-15 19:04:45.923 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Decoded ZCL frame: PhilipsHueLightCluster:DefaultResponse(command_id=19, status=) +2026-03-15 19:04:45.923 WARNING (MainThread) [zigpy.device_scanner] Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0xfc03 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND +2026-03-15 19:04:45.934 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=64515, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:45.934 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=25, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:45.934 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Sending request header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=81, command_id=, *direction=) +2026-03-15 19:04:45.934 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Sending request: Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:45.934 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 45, 934892, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=81, profile_id=260, cluster_id=25, data=Serialized[b'\x18Q\x15\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:45.968 DEBUG (MainThread) [zigpy.device] Cluster 0x0019 on has incorrect direction (got for cluster). Please report this here: https://github.com/zigpy/zigpy/issues/1640 +2026-03-15 19:04:45.968 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=81, command_id=11, *direction=) +2026-03-15 19:04:45.968 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Decoded ZCL frame: Ota:DefaultResponse(command_id=21, status=) +2026-03-15 19:04:45.968 WARNING (MainThread) [zigpy.device_scanner] Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0019 (client): UNSUP_GENERAL_COMMAND; falling back to discover_attributes +2026-03-15 19:04:45.968 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Sending request header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=82, command_id=, *direction=) +2026-03-15 19:04:45.968 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Sending request: Discover_Attributes(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:45.968 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 45, 968708, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=82, profile_id=260, cluster_id=25, data=Serialized[b'\x18R\x0c\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:46.026 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x10>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=82, command_id=13, *direction=) +2026-03-15 19:04:46.026 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Decoded ZCL frame: Ota:Discover_Attributes_rsp(discovery_complete=, attribute_info=[DiscoverAttributesResponseRecord(attrid=0, datatype=240), DiscoverAttributesResponseRecord(attrid=1, datatype=35), DiscoverAttributesResponseRecord(attrid=2, datatype=35), DiscoverAttributesResponseRecord(attrid=3, datatype=33), DiscoverAttributesResponseRecord(attrid=4, datatype=35), DiscoverAttributesResponseRecord(attrid=6, datatype=48), DiscoverAttributesResponseRecord(attrid=7, datatype=33), DiscoverAttributesResponseRecord(attrid=8, datatype=33), DiscoverAttributesResponseRecord(attrid=9, datatype=33), DiscoverAttributesResponseRecord(attrid=65533, datatype=33)]) +2026-03-15 19:04:46.037 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=25, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:46.038 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=25, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:46.038 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Sending request header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=83, command_id=, *direction=) +2026-03-15 19:04:46.038 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Sending request: Read_Attributes(attribute_ids=[0, 1, 2]) +2026-03-15 19:04:46.038 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 46, 38668, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=83, profile_id=260, cluster_id=25, data=Serialized[b'\x18S\x00\x00\x00\x01\x00\x02\x00'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:46.080 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x10>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=83, command_id=1, *direction=) +2026-03-15 19:04:46.080 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Decoded ZCL frame: Ota:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=0, status=, value=TypeValue(type=EUI64, value=00:0d:6f:00:0a:ff:74:55)), ReadAttributeRecord(attrid=1, status=, value=TypeValue(type=uint32_t, value=4294967295)), ReadAttributeRecord(attrid=2, status=, value=TypeValue(type=uint32_t, value=16784640))]) +2026-03-15 19:04:46.091 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Sending request header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=84, command_id=, *direction=) +2026-03-15 19:04:46.091 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Sending request: Read_Attributes(attribute_ids=[3, 4, 6]) +2026-03-15 19:04:46.092 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 46, 92014, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=84, profile_id=260, cluster_id=25, data=Serialized[b'\x18T\x00\x03\x00\x04\x00\x06\x00'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:46.134 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x10>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=84, command_id=1, *direction=) +2026-03-15 19:04:46.135 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Decoded ZCL frame: Ota:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=3, status=, value=TypeValue(type=uint16_t, value=2)), ReadAttributeRecord(attrid=4, status=, value=TypeValue(type=uint32_t, value=4294967295)), ReadAttributeRecord(attrid=6, status=, value=TypeValue(type=enum8, value=))]) +2026-03-15 19:04:46.146 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Sending request header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=85, command_id=, *direction=) +2026-03-15 19:04:46.146 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Sending request: Read_Attributes(attribute_ids=[7, 8, 9]) +2026-03-15 19:04:46.146 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 46, 146638, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=85, profile_id=260, cluster_id=25, data=Serialized[b'\x18U\x00\x07\x00\x08\x00\t\x00'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:46.184 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x10>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=85, command_id=1, *direction=) +2026-03-15 19:04:46.184 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Decoded ZCL frame: Ota:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=7, status=, value=TypeValue(type=uint16_t, value=4107)), ReadAttributeRecord(attrid=8, status=, value=TypeValue(type=uint16_t, value=65535)), ReadAttributeRecord(attrid=9, status=, value=TypeValue(type=uint16_t, value=0))]) +2026-03-15 19:04:46.196 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Sending request header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=86, command_id=, *direction=) +2026-03-15 19:04:46.196 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Sending request: Read_Attributes(attribute_ids=[65533]) +2026-03-15 19:04:46.196 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 46, 196425, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=86, profile_id=260, cluster_id=25, data=Serialized[b'\x18V\x00\xfd\xff'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:46.230 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x10>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=86, command_id=1, *direction=) +2026-03-15 19:04:46.230 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Decoded ZCL frame: Ota:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=65533, status=, value=TypeValue(type=uint16_t, value=1))]) +2026-03-15 19:04:46.242 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=25, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:46.243 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=25, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:46.243 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Sending request header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=87, command_id=, *direction=) +2026-03-15 19:04:46.243 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Sending request: Discover_Commands_Received(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:46.243 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 46, 243676, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=87, profile_id=260, cluster_id=25, data=Serialized[b'\x18W\x11\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:46.285 DEBUG (MainThread) [zigpy.device] Cluster 0x0019 on has incorrect direction (got for cluster). Please report this here: https://github.com/zigpy/zigpy/issues/1640 +2026-03-15 19:04:46.285 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=87, command_id=11, *direction=) +2026-03-15 19:04:46.285 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Decoded ZCL frame: Ota:DefaultResponse(command_id=17, status=) +2026-03-15 19:04:46.285 WARNING (MainThread) [zigpy.device_scanner] Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0019 (client): unsupported command discovery response UNSUP_GENERAL_COMMAND +2026-03-15 19:04:46.296 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=25, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:46.297 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=25, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:46.297 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Sending request header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=88, command_id=, *direction=) +2026-03-15 19:04:46.298 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Sending request: Discover_Commands_Generated(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:46.298 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 46, 298151, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=88, profile_id=260, cluster_id=25, data=Serialized[b'\x18X\x13\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:46.349 DEBUG (MainThread) [zigpy.device] Cluster 0x0019 on has incorrect direction (got for cluster). Please report this here: https://github.com/zigpy/zigpy/issues/1640 +2026-03-15 19:04:46.349 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x18>(frame_type=, is_manufacturer_specific=0, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=88, command_id=11, *direction=) +2026-03-15 19:04:46.349 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Decoded ZCL frame: Ota:DefaultResponse(command_id=19, status=) +2026-03-15 19:04:46.349 WARNING (MainThread) [zigpy.device_scanner] Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0019 (client): unsupported command discovery response UNSUP_GENERAL_COMMAND +2026-03-15 19:04:46.360 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=25, cluster_type=, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:46.360 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=0, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:46.360 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=89, command_id=, *direction=) +2026-03-15 19:04:46.360 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:46.360 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 46, 360566, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=89, profile_id=260, cluster_id=0, data=Serialized[b'\x04\x0b\x10Y\x15\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:46.403 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=89, command_id=11, *direction=) +2026-03-15 19:04:46.404 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame: Basic:DefaultResponse(command_id=21, status=) +2026-03-15 19:04:46.404 WARNING (MainThread) [zigpy.device_scanner] Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0000 (server): UNSUP_MANUF_GENERAL_COMMAND; falling back to discover_attributes +2026-03-15 19:04:46.404 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=90, command_id=, *direction=) +2026-03-15 19:04:46.404 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Discover_Attributes(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:46.404 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 46, 404501, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=90, profile_id=260, cluster_id=0, data=Serialized[b'\x04\x0b\x10Z\x0c\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:46.450 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=90, command_id=13, *direction=) +2026-03-15 19:04:46.451 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame: Basic:Discover_Attributes_rsp(discovery_complete=, attribute_info=[DiscoverAttributesResponseRecord(attrid=1, datatype=35), DiscoverAttributesResponseRecord(attrid=32, datatype=66), DiscoverAttributesResponseRecord(attrid=33, datatype=35), DiscoverAttributesResponseRecord(attrid=64, datatype=66), DiscoverAttributesResponseRecord(attrid=65, datatype=35), DiscoverAttributesResponseRecord(attrid=80, datatype=27), DiscoverAttributesResponseRecord(attrid=81, datatype=48), DiscoverAttributesResponseRecord(attrid=82, datatype=48), DiscoverAttributesResponseRecord(attrid=61697, datatype=66), DiscoverAttributesResponseRecord(attrid=61698, datatype=33), DiscoverAttributesResponseRecord(attrid=61700, datatype=32), DiscoverAttributesResponseRecord(attrid=61702, datatype=35), DiscoverAttributesResponseRecord(attrid=61703, datatype=65), DiscoverAttributesResponseRecord(attrid=61704, datatype=48), DiscoverAttributesResponseRecord(attrid=61705, datatype=16), DiscoverAttributesResponseRecord(attrid=61706, datatype=35)]) +2026-03-15 19:04:46.463 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=91, command_id=, *direction=) +2026-03-15 19:04:46.463 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Discover_Attributes(start_attribute_id=61707, max_attribute_ids=16) +2026-03-15 19:04:46.463 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 46, 463381, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=91, profile_id=260, cluster_id=0, data=Serialized[b'\x04\x0b\x10[\x0c\x0b\xf1\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:46.514 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=91, command_id=13, *direction=) +2026-03-15 19:04:46.514 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame: Basic:Discover_Attributes_rsp(discovery_complete=, attribute_info=[DiscoverAttributesResponseRecord(attrid=61707, datatype=32), DiscoverAttributesResponseRecord(attrid=61708, datatype=48), DiscoverAttributesResponseRecord(attrid=61716, datatype=66), DiscoverAttributesResponseRecord(attrid=61717, datatype=65), DiscoverAttributesResponseRecord(attrid=61718, datatype=35), DiscoverAttributesResponseRecord(attrid=61719, datatype=48), DiscoverAttributesResponseRecord(attrid=61720, datatype=48), DiscoverAttributesResponseRecord(attrid=61721, datatype=65), DiscoverAttributesResponseRecord(attrid=61722, datatype=65), DiscoverAttributesResponseRecord(attrid=61724, datatype=35), DiscoverAttributesResponseRecord(attrid=61725, datatype=35), DiscoverAttributesResponseRecord(attrid=61726, datatype=35), DiscoverAttributesResponseRecord(attrid=61727, datatype=35), DiscoverAttributesResponseRecord(attrid=61728, datatype=35), DiscoverAttributesResponseRecord(attrid=61729, datatype=35), DiscoverAttributesResponseRecord(attrid=61730, datatype=27)]) +2026-03-15 19:04:46.526 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=92, command_id=, *direction=) +2026-03-15 19:04:46.526 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Discover_Attributes(start_attribute_id=61731, max_attribute_ids=16) +2026-03-15 19:04:46.526 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 46, 526713, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=92, profile_id=260, cluster_id=0, data=Serialized[b'\x04\x0b\x10\\\x0c#\xf1\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:46.571 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=92, command_id=13, *direction=) +2026-03-15 19:04:46.571 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame: Basic:Discover_Attributes_rsp(discovery_complete=, attribute_info=[DiscoverAttributesResponseRecord(attrid=61731, datatype=27), DiscoverAttributesResponseRecord(attrid=61733, datatype=16), DiscoverAttributesResponseRecord(attrid=61734, datatype=16), DiscoverAttributesResponseRecord(attrid=61735, datatype=35)]) +2026-03-15 19:04:46.583 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=0, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:46.584 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=0, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:46.584 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=93, command_id=, *direction=) +2026-03-15 19:04:46.584 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Read_Attributes(attribute_ids=[1, 32, 33]) +2026-03-15 19:04:46.584 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 46, 584922, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=93, profile_id=260, cluster_id=0, data=Serialized[b'\x04\x0b\x10]\x00\x01\x00 \x00!\x00'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:46.633 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=93, command_id=1, *direction=) +2026-03-15 19:04:46.633 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame: Basic:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=1, status=, value=TypeValue(type=uint32_t, value=0)), ReadAttributeRecord(attrid=32, status=, value=TypeValue(type=CharacterString, value='0:PWRON@0')), ReadAttributeRecord(attrid=33, status=, value=TypeValue(type=uint32_t, value=10424039))]) +2026-03-15 19:04:46.644 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=94, command_id=, *direction=) +2026-03-15 19:04:46.644 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Read_Attributes(attribute_ids=[64, 65, 80]) +2026-03-15 19:04:46.644 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 46, 644251, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=94, profile_id=260, cluster_id=0, data=Serialized[b'\x04\x0b\x10^\x00@\x00A\x00P\x00'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:46.689 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=94, command_id=1, *direction=) +2026-03-15 19:04:46.689 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame: Basic:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=64, status=, value=TypeValue(type=CharacterString, value='Philips-LCT030-1-HueGoECLv2')), ReadAttributeRecord(attrid=65, status=, value=TypeValue(type=uint32_t, value=1181842861)), ReadAttributeRecord(attrid=80, status=, value=TypeValue(type=bitmap32, value=))]) +2026-03-15 19:04:46.701 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=95, command_id=, *direction=) +2026-03-15 19:04:46.701 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Read_Attributes(attribute_ids=[81, 82, 61697]) +2026-03-15 19:04:46.701 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 46, 701755, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=95, profile_id=260, cluster_id=0, data=Serialized[b'\x04\x0b\x10_\x00Q\x00R\x00\x01\xf1'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:46.787 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=95, command_id=1, *direction=) +2026-03-15 19:04:46.787 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame: Basic:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=81, status=, value=TypeValue(type=enum8, value=)), ReadAttributeRecord(attrid=82, status=, value=None), ReadAttributeRecord(attrid=61697, status=, value=TypeValue(type=CharacterString, value='7602031U7'))]) +2026-03-15 19:04:46.798 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=96, command_id=, *direction=) +2026-03-15 19:04:46.798 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Read_Attributes(attribute_ids=[61698, 61700, 61702]) +2026-03-15 19:04:46.799 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 46, 799136, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=96, profile_id=260, cluster_id=0, data=Serialized[b'\x04\x0b\x10`\x00\x02\xf1\x04\xf1\x06\xf1'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:47.076 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=96, command_id=1, *direction=) +2026-03-15 19:04:47.076 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame: Basic:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=61698, status=, value=None), ReadAttributeRecord(attrid=61700, status=, value=TypeValue(type=uint8_t, value=1)), ReadAttributeRecord(attrid=61702, status=, value=TypeValue(type=uint32_t, value=12000))]) +2026-03-15 19:04:47.088 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=97, command_id=, *direction=) +2026-03-15 19:04:47.088 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Read_Attributes(attribute_ids=[61703, 61704, 61705]) +2026-03-15 19:04:47.088 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 47, 88707, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=97, profile_id=260, cluster_id=0, data=Serialized[b'\x04\x0b\x10a\x00\x07\xf1\x08\xf1\t\xf1'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:47.391 DEBUG (MainThread) [zigpy.application] Failed to send packet, attempt 1 of 3 +2026-03-15 19:04:47.392 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 47, 392611, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=97, profile_id=260, cluster_id=0, data=Serialized[b'\x04\x0b\x10a\x00\x07\xf1\x08\xf1\t\xf1'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:47.479 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=97, command_id=1, *direction=) +2026-03-15 19:04:47.480 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame: Basic:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=61703, status=, value=None), ReadAttributeRecord(attrid=61704, status=, value=TypeValue(type=enum8, value=)), ReadAttributeRecord(attrid=61705, status=, value=TypeValue(type=Bool, value=))]) +2026-03-15 19:04:47.491 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=98, command_id=, *direction=) +2026-03-15 19:04:47.491 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Read_Attributes(attribute_ids=[61706, 61707, 61708]) +2026-03-15 19:04:47.492 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 47, 492122, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=98, profile_id=260, cluster_id=0, data=Serialized[b'\x04\x0b\x10b\x00\n\xf1\x0b\xf1\x0c\xf1'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:47.610 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=98, command_id=1, *direction=) +2026-03-15 19:04:47.610 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame: Basic:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=61706, status=, value=TypeValue(type=uint32_t, value=6000)), ReadAttributeRecord(attrid=61707, status=, value=TypeValue(type=uint8_t, value=4)), ReadAttributeRecord(attrid=61708, status=, value=TypeValue(type=enum8, value=))]) +2026-03-15 19:04:47.622 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=99, command_id=, *direction=) +2026-03-15 19:04:47.622 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Read_Attributes(attribute_ids=[61716, 61717, 61718]) +2026-03-15 19:04:47.622 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 47, 622333, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=99, profile_id=260, cluster_id=0, data=Serialized[b'\x04\x0b\x10c\x00\x14\xf1\x15\xf1\x16\xf1'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:47.707 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=99, command_id=1, *direction=) +2026-03-15 19:04:47.707 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame: Basic:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=61716, status=, value=TypeValue(type=CharacterString, value='')), ReadAttributeRecord(attrid=61717, status=, value=None), ReadAttributeRecord(attrid=61718, status=, value=TypeValue(type=uint32_t, value=492824930))]) +2026-03-15 19:04:47.720 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=100, command_id=, *direction=) +2026-03-15 19:04:47.720 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Read_Attributes(attribute_ids=[61719, 61720, 61721]) +2026-03-15 19:04:47.720 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 47, 720597, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=100, profile_id=260, cluster_id=0, data=Serialized[b'\x04\x0b\x10d\x00\x17\xf1\x18\xf1\x19\xf1'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:47.815 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=100, command_id=1, *direction=) +2026-03-15 19:04:47.815 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame: Basic:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=61719, status=, value=TypeValue(type=enum8, value=)), ReadAttributeRecord(attrid=61720, status=, value=TypeValue(type=enum8, value=))]) +2026-03-15 19:04:47.826 WARNING (MainThread) [zigpy.device_scanner] Read_Attributes response missing status records for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0000 (server) manufacturer 4107: missing 0xf119; response contained 0xf117, 0xf118 +2026-03-15 19:04:47.826 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=101, command_id=, *direction=) +2026-03-15 19:04:47.826 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Read_Attributes(attribute_ids=[61721]) +2026-03-15 19:04:47.826 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 47, 826964, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=101, profile_id=260, cluster_id=0, data=Serialized[b'\x04\x0b\x10e\x00\x19\xf1'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:47.960 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=101, command_id=1, *direction=) +2026-03-15 19:04:47.960 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame: Basic:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=61721, status=, value=TypeValue(type=LVBytes, value=b'\n3\x08\x01\x1a/\n\x05K1911\x12\x06\x08\xdc\xf7\x85\x8b\x06\x18\x01 \x01(\xf8\x060\xfe\xf0\xf2\xe6\x84\x1b:\x08\xf0.\xab\x08\xc8e\x89\x12B\x06\x00\x00\xe3N\xff\x1f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'))]) +2026-03-15 19:04:47.972 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=102, command_id=, *direction=) +2026-03-15 19:04:47.972 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Read_Attributes(attribute_ids=[61722, 61724, 61725]) +2026-03-15 19:04:47.972 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 47, 972882, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=102, profile_id=260, cluster_id=0, data=Serialized[b'\x04\x0b\x10f\x00\x1a\xf1\x1c\xf1\x1d\xf1'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:48.032 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=102, command_id=1, *direction=) +2026-03-15 19:04:48.032 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame: Basic:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=61722, status=, value=TypeValue(type=LVBytes, value=b'\n0\x08\x01",\n\x03A2M\x12\x06\x08\xba\xb8\xab\x8f\x06\x18\x11 \x02(\x012\x0c9150058221018\x8a\xa4\x81\x08H\xe2\xd2\xff\xea\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'))]) +2026-03-15 19:04:48.043 WARNING (MainThread) [zigpy.device_scanner] Read_Attributes response missing status records for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0000 (server) manufacturer 4107: missing 0xf11c, 0xf11d; response contained 0xf11a +2026-03-15 19:04:48.044 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=103, command_id=, *direction=) +2026-03-15 19:04:48.044 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Read_Attributes(attribute_ids=[61724]) +2026-03-15 19:04:48.044 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 48, 44856, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=103, profile_id=260, cluster_id=0, data=Serialized[b'\x04\x0b\x10g\x00\x1c\xf1'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:48.233 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=103, command_id=1, *direction=) +2026-03-15 19:04:48.233 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame: Basic:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=61724, status=, value=TypeValue(type=uint32_t, value=386))]) +2026-03-15 19:04:48.244 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=104, command_id=, *direction=) +2026-03-15 19:04:48.244 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Read_Attributes(attribute_ids=[61725]) +2026-03-15 19:04:48.245 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 48, 245044, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=104, profile_id=260, cluster_id=0, data=Serialized[b'\x04\x0b\x10h\x00\x1d\xf1'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:48.287 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=104, command_id=1, *direction=) +2026-03-15 19:04:48.287 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame: Basic:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=61725, status=, value=TypeValue(type=uint32_t, value=18949))]) +2026-03-15 19:04:48.299 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=105, command_id=, *direction=) +2026-03-15 19:04:48.299 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Read_Attributes(attribute_ids=[61726, 61727, 61728]) +2026-03-15 19:04:48.299 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 48, 299244, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=105, profile_id=260, cluster_id=0, data=Serialized[b'\x04\x0b\x10i\x00\x1e\xf1\x1f\xf1 \xf1'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:48.406 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=105, command_id=1, *direction=) +2026-03-15 19:04:48.406 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame: Basic:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=61726, status=, value=TypeValue(type=uint32_t, value=15424)), ReadAttributeRecord(attrid=61727, status=, value=TypeValue(type=uint32_t, value=16611)), ReadAttributeRecord(attrid=61728, status=, value=TypeValue(type=uint32_t, value=8563))]) +2026-03-15 19:04:48.418 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=106, command_id=, *direction=) +2026-03-15 19:04:48.418 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Read_Attributes(attribute_ids=[61729, 61730, 61731]) +2026-03-15 19:04:48.419 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 48, 419114, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=106, profile_id=260, cluster_id=0, data=Serialized[b'\x04\x0b\x10j\x00!\xf1"\xf1#\xf1'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:48.501 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=106, command_id=1, *direction=) +2026-03-15 19:04:48.501 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame: Basic:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=61729, status=, value=TypeValue(type=uint32_t, value=7188)), ReadAttributeRecord(attrid=61730, status=, value=TypeValue(type=bitmap32, value=)), ReadAttributeRecord(attrid=61731, status=, value=TypeValue(type=bitmap32, value=))]) +2026-03-15 19:04:48.512 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=107, command_id=, *direction=) +2026-03-15 19:04:48.512 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Read_Attributes(attribute_ids=[61733, 61734, 61735]) +2026-03-15 19:04:48.513 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 48, 513037, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=107, profile_id=260, cluster_id=0, data=Serialized[b"\x04\x0b\x10k\x00%\xf1&\xf1'\xf1"], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:48.584 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=107, command_id=1, *direction=) +2026-03-15 19:04:48.584 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame: Basic:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=61733, status=, value=TypeValue(type=Bool, value=)), ReadAttributeRecord(attrid=61734, status=, value=TypeValue(type=Bool, value=)), ReadAttributeRecord(attrid=61735, status=, value=TypeValue(type=uint32_t, value=1000))]) +2026-03-15 19:04:48.596 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=0, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:48.597 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=0, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:48.597 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=108, command_id=, *direction=) +2026-03-15 19:04:48.597 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Discover_Commands_Received(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:48.597 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 48, 597746, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=108, profile_id=260, cluster_id=0, data=Serialized[b'\x04\x0b\x10l\x11\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:48.646 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=108, command_id=11, *direction=) +2026-03-15 19:04:48.646 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame: Basic:DefaultResponse(command_id=17, status=) +2026-03-15 19:04:48.646 WARNING (MainThread) [zigpy.device_scanner] Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0000 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND +2026-03-15 19:04:48.658 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=0, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:48.659 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=0, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:48.659 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=109, command_id=, *direction=) +2026-03-15 19:04:48.659 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Sending request: Discover_Commands_Generated(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:48.659 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 48, 659527, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=109, profile_id=260, cluster_id=0, data=Serialized[b'\x04\x0b\x10m\x13\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:48.698 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=109, command_id=11, *direction=) +2026-03-15 19:04:48.698 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0000] Decoded ZCL frame: Basic:DefaultResponse(command_id=19, status=) +2026-03-15 19:04:48.698 WARNING (MainThread) [zigpy.device_scanner] Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0000 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND +2026-03-15 19:04:48.709 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=0, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:48.709 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=3, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:48.709 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=110, command_id=, *direction=) +2026-03-15 19:04:48.709 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Sending request: Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:48.710 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 48, 710113, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=110, profile_id=260, cluster_id=3, data=Serialized[b'\x04\x0b\x10n\x15\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:48.743 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=110, command_id=11, *direction=) +2026-03-15 19:04:48.744 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Decoded ZCL frame: Identify:DefaultResponse(command_id=21, status=) +2026-03-15 19:04:48.744 WARNING (MainThread) [zigpy.device_scanner] Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0003 (server): UNSUP_MANUF_GENERAL_COMMAND; falling back to discover_attributes +2026-03-15 19:04:48.744 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=111, command_id=, *direction=) +2026-03-15 19:04:48.744 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Sending request: Discover_Attributes(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:48.744 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 48, 744491, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=111, profile_id=260, cluster_id=3, data=Serialized[b'\x04\x0b\x10o\x0c\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:48.830 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=111, command_id=13, *direction=) +2026-03-15 19:04:48.830 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Decoded ZCL frame: Identify:Discover_Attributes_rsp(discovery_complete=, attribute_info=[]) +2026-03-15 19:04:48.842 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=3, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:48.843 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=3, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:48.843 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=3, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:48.845 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=3, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:48.845 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=112, command_id=, *direction=) +2026-03-15 19:04:48.845 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Sending request: Discover_Commands_Received(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:48.845 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 48, 845392, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=112, profile_id=260, cluster_id=3, data=Serialized[b'\x04\x0b\x10p\x11\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:48.941 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=112, command_id=11, *direction=) +2026-03-15 19:04:48.941 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Decoded ZCL frame: Identify:DefaultResponse(command_id=17, status=) +2026-03-15 19:04:48.941 WARNING (MainThread) [zigpy.device_scanner] Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0003 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND +2026-03-15 19:04:48.953 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=3, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:48.954 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=3, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:48.954 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=113, command_id=, *direction=) +2026-03-15 19:04:48.954 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Sending request: Discover_Commands_Generated(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:48.954 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 48, 954590, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=113, profile_id=260, cluster_id=3, data=Serialized[b'\x04\x0b\x10q\x13\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:49.034 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=113, command_id=11, *direction=) +2026-03-15 19:04:49.034 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0003] Decoded ZCL frame: Identify:DefaultResponse(command_id=19, status=) +2026-03-15 19:04:49.034 WARNING (MainThread) [zigpy.device_scanner] Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0003 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND +2026-03-15 19:04:49.045 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=3, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:49.045 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=4, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:49.045 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=114, command_id=, *direction=) +2026-03-15 19:04:49.045 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Sending request: Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:49.045 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 49, 45720, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=114, profile_id=260, cluster_id=4, data=Serialized[b'\x04\x0b\x10r\x15\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:49.093 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=114, command_id=11, *direction=) +2026-03-15 19:04:49.093 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Decoded ZCL frame: Groups:DefaultResponse(command_id=21, status=) +2026-03-15 19:04:49.093 WARNING (MainThread) [zigpy.device_scanner] Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0004 (server): UNSUP_MANUF_GENERAL_COMMAND; falling back to discover_attributes +2026-03-15 19:04:49.093 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=115, command_id=, *direction=) +2026-03-15 19:04:49.093 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Sending request: Discover_Attributes(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:49.093 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 49, 93976, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=115, profile_id=260, cluster_id=4, data=Serialized[b'\x04\x0b\x10s\x0c\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:49.128 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=115, command_id=13, *direction=) +2026-03-15 19:04:49.128 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Decoded ZCL frame: Groups:Discover_Attributes_rsp(discovery_complete=, attribute_info=[]) +2026-03-15 19:04:49.140 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=4, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:49.141 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=4, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:49.141 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=4, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:49.142 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=4, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:49.142 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=116, command_id=, *direction=) +2026-03-15 19:04:49.142 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Sending request: Discover_Commands_Received(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:49.143 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 49, 143066, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=116, profile_id=260, cluster_id=4, data=Serialized[b'\x04\x0b\x10t\x11\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:49.196 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=116, command_id=11, *direction=) +2026-03-15 19:04:49.196 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Decoded ZCL frame: Groups:DefaultResponse(command_id=17, status=) +2026-03-15 19:04:49.196 WARNING (MainThread) [zigpy.device_scanner] Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0004 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND +2026-03-15 19:04:49.207 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=4, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:49.208 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=4, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:49.208 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=117, command_id=, *direction=) +2026-03-15 19:04:49.208 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Sending request: Discover_Commands_Generated(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:49.209 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 49, 209071, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=117, profile_id=260, cluster_id=4, data=Serialized[b'\x04\x0b\x10u\x13\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:49.247 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=117, command_id=11, *direction=) +2026-03-15 19:04:49.247 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0004] Decoded ZCL frame: Groups:DefaultResponse(command_id=19, status=) +2026-03-15 19:04:49.247 WARNING (MainThread) [zigpy.device_scanner] Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0004 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND +2026-03-15 19:04:49.259 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=4, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:49.259 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=5, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:49.259 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=118, command_id=, *direction=) +2026-03-15 19:04:49.259 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Sending request: Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:49.259 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 49, 259567, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=118, profile_id=260, cluster_id=5, data=Serialized[b'\x04\x0b\x10v\x15\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:49.304 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=118, command_id=11, *direction=) +2026-03-15 19:04:49.304 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Decoded ZCL frame: Scenes:DefaultResponse(command_id=21, status=) +2026-03-15 19:04:49.304 WARNING (MainThread) [zigpy.device_scanner] Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0005 (server): UNSUP_MANUF_GENERAL_COMMAND; falling back to discover_attributes +2026-03-15 19:04:49.304 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=119, command_id=, *direction=) +2026-03-15 19:04:49.304 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Sending request: Discover_Attributes(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:49.305 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 49, 305012, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=119, profile_id=260, cluster_id=5, data=Serialized[b'\x04\x0b\x10w\x0c\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:49.341 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=119, command_id=13, *direction=) +2026-03-15 19:04:49.341 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Decoded ZCL frame: Scenes:Discover_Attributes_rsp(discovery_complete=, attribute_info=[DiscoverAttributesResponseRecord(attrid=1, datatype=27)]) +2026-03-15 19:04:49.352 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=5, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:49.354 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=5, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:49.354 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=120, command_id=, *direction=) +2026-03-15 19:04:49.354 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Sending request: Read_Attributes(attribute_ids=[1]) +2026-03-15 19:04:49.355 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 49, 354997, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=120, profile_id=260, cluster_id=5, data=Serialized[b'\x04\x0b\x10x\x00\x01\x00'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:49.427 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=120, command_id=1, *direction=) +2026-03-15 19:04:49.427 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Decoded ZCL frame: Scenes:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=1, status=, value=TypeValue(type=bitmap32, value=))]) +2026-03-15 19:04:49.439 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=5, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:49.441 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=5, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:49.441 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=121, command_id=, *direction=) +2026-03-15 19:04:49.441 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Sending request: Discover_Commands_Received(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:49.441 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 49, 441350, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=121, profile_id=260, cluster_id=5, data=Serialized[b'\x04\x0b\x10y\x11\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:49.499 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=121, command_id=11, *direction=) +2026-03-15 19:04:49.499 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Decoded ZCL frame: Scenes:DefaultResponse(command_id=17, status=) +2026-03-15 19:04:49.499 WARNING (MainThread) [zigpy.device_scanner] Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0005 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND +2026-03-15 19:04:49.511 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=5, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:49.512 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=5, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:49.512 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=122, command_id=, *direction=) +2026-03-15 19:04:49.512 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Sending request: Discover_Commands_Generated(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:49.512 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 49, 512631, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=122, profile_id=260, cluster_id=5, data=Serialized[b'\x04\x0b\x10z\x13\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:49.547 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=122, command_id=11, *direction=) +2026-03-15 19:04:49.547 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0005] Decoded ZCL frame: Scenes:DefaultResponse(command_id=19, status=) +2026-03-15 19:04:49.547 WARNING (MainThread) [zigpy.device_scanner] Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0005 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND +2026-03-15 19:04:49.558 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=5, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:49.558 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=6, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:49.558 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=123, command_id=, *direction=) +2026-03-15 19:04:49.558 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Sending request: Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:49.558 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 49, 558557, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=123, profile_id=260, cluster_id=6, data=Serialized[b'\x04\x0b\x10{\x15\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:49.605 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=123, command_id=11, *direction=) +2026-03-15 19:04:49.605 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Decoded ZCL frame: OnOff:DefaultResponse(command_id=21, status=) +2026-03-15 19:04:49.606 WARNING (MainThread) [zigpy.device_scanner] Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0006 (server): UNSUP_MANUF_GENERAL_COMMAND; falling back to discover_attributes +2026-03-15 19:04:49.606 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=124, command_id=, *direction=) +2026-03-15 19:04:49.606 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Sending request: Discover_Attributes(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:49.606 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 49, 606469, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=124, profile_id=260, cluster_id=6, data=Serialized[b'\x04\x0b\x10|\x0c\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:49.642 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=124, command_id=13, *direction=) +2026-03-15 19:04:49.642 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Decoded ZCL frame: OnOff:Discover_Attributes_rsp(discovery_complete=, attribute_info=[]) +2026-03-15 19:04:49.653 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=6, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:49.654 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=6, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:49.654 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=6, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:49.655 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=6, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:49.655 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=125, command_id=, *direction=) +2026-03-15 19:04:49.655 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Sending request: Discover_Commands_Received(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:49.656 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 49, 655998, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=125, profile_id=260, cluster_id=6, data=Serialized[b'\x04\x0b\x10}\x11\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:49.738 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=125, command_id=11, *direction=) +2026-03-15 19:04:49.738 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Decoded ZCL frame: OnOff:DefaultResponse(command_id=17, status=) +2026-03-15 19:04:49.739 WARNING (MainThread) [zigpy.device_scanner] Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0006 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND +2026-03-15 19:04:49.750 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=6, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:49.751 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=6, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:49.752 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=126, command_id=, *direction=) +2026-03-15 19:04:49.752 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Sending request: Discover_Commands_Generated(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:49.752 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 49, 752326, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=126, profile_id=260, cluster_id=6, data=Serialized[b'\x04\x0b\x10~\x13\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:49.851 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=126, command_id=11, *direction=) +2026-03-15 19:04:49.852 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0006] Decoded ZCL frame: OnOff:DefaultResponse(command_id=19, status=) +2026-03-15 19:04:49.852 WARNING (MainThread) [zigpy.device_scanner] Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0006 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND +2026-03-15 19:04:49.863 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=6, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:49.863 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=8, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:49.863 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=127, command_id=, *direction=) +2026-03-15 19:04:49.863 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Sending request: Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:49.864 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 49, 864114, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=127, profile_id=260, cluster_id=8, data=Serialized[b'\x04\x0b\x10\x7f\x15\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:49.912 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=127, command_id=11, *direction=) +2026-03-15 19:04:49.912 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Decoded ZCL frame: LevelControl:DefaultResponse(command_id=21, status=) +2026-03-15 19:04:49.912 WARNING (MainThread) [zigpy.device_scanner] Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0008 (server): UNSUP_MANUF_GENERAL_COMMAND; falling back to discover_attributes +2026-03-15 19:04:49.912 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=128, command_id=, *direction=) +2026-03-15 19:04:49.912 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Sending request: Discover_Attributes(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:49.912 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 49, 912955, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=128, profile_id=260, cluster_id=8, data=Serialized[b'\x04\x0b\x10\x80\x0c\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:49.948 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=128, command_id=13, *direction=) +2026-03-15 19:04:49.948 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Decoded ZCL frame: LevelControl:Discover_Attributes_rsp(discovery_complete=, attribute_info=[DiscoverAttributesResponseRecord(attrid=3, datatype=33)]) +2026-03-15 19:04:49.959 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=8, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:49.960 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=8, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:49.960 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=129, command_id=, *direction=) +2026-03-15 19:04:49.960 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Sending request: Read_Attributes(attribute_ids=[3]) +2026-03-15 19:04:49.961 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 49, 961005, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=129, profile_id=260, cluster_id=8, data=Serialized[b'\x04\x0b\x10\x81\x00\x03\x00'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:50.068 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=129, command_id=1, *direction=) +2026-03-15 19:04:50.068 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Decoded ZCL frame: LevelControl:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=3, status=, value=TypeValue(type=uint16_t, value=70))]) +2026-03-15 19:04:50.080 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=8, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:50.081 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=8, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:50.081 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=130, command_id=, *direction=) +2026-03-15 19:04:50.081 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Sending request: Discover_Commands_Received(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:50.081 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 50, 81689, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=130, profile_id=260, cluster_id=8, data=Serialized[b'\x04\x0b\x10\x82\x11\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:50.121 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=130, command_id=11, *direction=) +2026-03-15 19:04:50.121 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Decoded ZCL frame: LevelControl:DefaultResponse(command_id=17, status=) +2026-03-15 19:04:50.121 WARNING (MainThread) [zigpy.device_scanner] Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0008 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND +2026-03-15 19:04:50.133 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=8, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:50.134 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=8, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:50.134 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=131, command_id=, *direction=) +2026-03-15 19:04:50.134 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Sending request: Discover_Commands_Generated(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:50.134 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 50, 134519, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=131, profile_id=260, cluster_id=8, data=Serialized[b'\x04\x0b\x10\x83\x13\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:50.166 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=131, command_id=11, *direction=) +2026-03-15 19:04:50.166 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0008] Decoded ZCL frame: LevelControl:DefaultResponse(command_id=19, status=) +2026-03-15 19:04:50.167 WARNING (MainThread) [zigpy.device_scanner] Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0008 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND +2026-03-15 19:04:50.177 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=8, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:50.177 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=768, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:50.178 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=132, command_id=, *direction=) +2026-03-15 19:04:50.178 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request: Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:50.178 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 50, 178303, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=132, profile_id=260, cluster_id=768, data=Serialized[b'\x04\x0b\x10\x84\x15\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:50.225 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=132, command_id=11, *direction=) +2026-03-15 19:04:50.225 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame: Color:DefaultResponse(command_id=21, status=) +2026-03-15 19:04:50.225 WARNING (MainThread) [zigpy.device_scanner] Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0300 (server): UNSUP_MANUF_GENERAL_COMMAND; falling back to discover_attributes +2026-03-15 19:04:50.226 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=133, command_id=, *direction=) +2026-03-15 19:04:50.226 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request: Discover_Attributes(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:50.226 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 50, 226245, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=133, profile_id=260, cluster_id=768, data=Serialized[b'\x04\x0b\x10\x85\x0c\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:50.275 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=133, command_id=13, *direction=) +2026-03-15 19:04:50.276 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame: Color:Discover_Attributes_rsp(discovery_complete=, attribute_info=[DiscoverAttributesResponseRecord(attrid=3, datatype=33), DiscoverAttributesResponseRecord(attrid=4, datatype=33), DiscoverAttributesResponseRecord(attrid=61440, datatype=48), DiscoverAttributesResponseRecord(attrid=61445, datatype=48), DiscoverAttributesResponseRecord(attrid=61446, datatype=48), DiscoverAttributesResponseRecord(attrid=61447, datatype=35), DiscoverAttributesResponseRecord(attrid=61448, datatype=35), DiscoverAttributesResponseRecord(attrid=61697, datatype=33), DiscoverAttributesResponseRecord(attrid=61953, datatype=33), DiscoverAttributesResponseRecord(attrid=62209, datatype=33), DiscoverAttributesResponseRecord(attrid=62465, datatype=33), DiscoverAttributesResponseRecord(attrid=62721, datatype=33), DiscoverAttributesResponseRecord(attrid=64000, datatype=33)]) +2026-03-15 19:04:50.287 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=768, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:50.288 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=768, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:50.289 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=134, command_id=, *direction=) +2026-03-15 19:04:50.289 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request: Read_Attributes(attribute_ids=[3, 4, 61440]) +2026-03-15 19:04:50.289 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 50, 289593, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=134, profile_id=260, cluster_id=768, data=Serialized[b'\x04\x0b\x10\x86\x00\x03\x00\x04\x00\x00\xf0'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:50.331 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=134, command_id=1, *direction=) +2026-03-15 19:04:50.331 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame: Color:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=3, status=, value=TypeValue(type=uint16_t, value=65535)), ReadAttributeRecord(attrid=4, status=, value=TypeValue(type=uint16_t, value=65535)), ReadAttributeRecord(attrid=61440, status=, value=TypeValue(type=enum8, value=))]) +2026-03-15 19:04:50.343 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=135, command_id=, *direction=) +2026-03-15 19:04:50.343 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request: Read_Attributes(attribute_ids=[61445, 61446, 61447]) +2026-03-15 19:04:50.344 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 50, 344132, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=135, profile_id=260, cluster_id=768, data=Serialized[b'\x04\x0b\x10\x87\x00\x05\xf0\x06\xf0\x07\xf0'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:50.389 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=135, command_id=1, *direction=) +2026-03-15 19:04:50.389 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame: Color:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=61445, status=, value=None), ReadAttributeRecord(attrid=61446, status=, value=TypeValue(type=enum8, value=)), ReadAttributeRecord(attrid=61447, status=, value=TypeValue(type=uint32_t, value=0))]) +2026-03-15 19:04:50.401 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=136, command_id=, *direction=) +2026-03-15 19:04:50.401 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request: Read_Attributes(attribute_ids=[61448, 61697, 61953]) +2026-03-15 19:04:50.401 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 50, 401837, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=136, profile_id=260, cluster_id=768, data=Serialized[b'\x04\x0b\x10\x88\x00\x08\xf0\x01\xf1\x01\xf2'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:50.441 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=136, command_id=1, *direction=) +2026-03-15 19:04:50.442 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame: Color:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=61448, status=, value=TypeValue(type=uint32_t, value=0)), ReadAttributeRecord(attrid=61697, status=, value=TypeValue(type=uint16_t, value=0)), ReadAttributeRecord(attrid=61953, status=, value=TypeValue(type=uint16_t, value=0))]) +2026-03-15 19:04:50.453 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=137, command_id=, *direction=) +2026-03-15 19:04:50.453 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request: Read_Attributes(attribute_ids=[62209, 62465, 62721]) +2026-03-15 19:04:50.453 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 50, 453591, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=137, profile_id=260, cluster_id=768, data=Serialized[b'\x04\x0b\x10\x89\x00\x01\xf3\x01\xf4\x01\xf5'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:50.501 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=137, command_id=1, *direction=) +2026-03-15 19:04:50.501 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame: Color:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=62209, status=, value=TypeValue(type=uint16_t, value=0)), ReadAttributeRecord(attrid=62465, status=, value=TypeValue(type=uint16_t, value=0)), ReadAttributeRecord(attrid=62721, status=, value=TypeValue(type=uint16_t, value=0))]) +2026-03-15 19:04:50.513 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=138, command_id=, *direction=) +2026-03-15 19:04:50.513 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request: Read_Attributes(attribute_ids=[64000]) +2026-03-15 19:04:50.513 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 50, 513757, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=138, profile_id=260, cluster_id=768, data=Serialized[b'\x04\x0b\x10\x8a\x00\x00\xfa'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:50.559 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=138, command_id=1, *direction=) +2026-03-15 19:04:50.559 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame: Color:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=64000, status=, value=TypeValue(type=uint16_t, value=366))]) +2026-03-15 19:04:50.571 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=768, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:50.572 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=768, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:50.572 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=139, command_id=, *direction=) +2026-03-15 19:04:50.572 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request: Discover_Commands_Received(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:50.572 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 50, 572856, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=139, profile_id=260, cluster_id=768, data=Serialized[b'\x04\x0b\x10\x8b\x11\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:50.687 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=139, command_id=11, *direction=) +2026-03-15 19:04:50.687 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame: Color:DefaultResponse(command_id=17, status=) +2026-03-15 19:04:50.687 WARNING (MainThread) [zigpy.device_scanner] Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0300 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND +2026-03-15 19:04:50.699 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=768, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:50.700 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=768, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:50.700 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=140, command_id=, *direction=) +2026-03-15 19:04:50.700 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Sending request: Discover_Commands_Generated(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:50.700 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 50, 700896, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=140, profile_id=260, cluster_id=768, data=Serialized[b'\x04\x0b\x10\x8c\x13\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:50.742 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=140, command_id=11, *direction=) +2026-03-15 19:04:50.742 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0300] Decoded ZCL frame: Color:DefaultResponse(command_id=19, status=) +2026-03-15 19:04:50.742 WARNING (MainThread) [zigpy.device_scanner] Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0300 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND +2026-03-15 19:04:50.753 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=768, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:50.753 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=4096, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:50.754 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=141, command_id=, *direction=) +2026-03-15 19:04:50.754 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Sending request: Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:50.754 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 50, 754254, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=141, profile_id=260, cluster_id=4096, data=Serialized[b'\x04\x0b\x10\x8d\x15\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:50.792 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=141, command_id=11, *direction=) +2026-03-15 19:04:50.792 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Decoded ZCL frame: LightLink:DefaultResponse(command_id=21, status=) +2026-03-15 19:04:50.792 WARNING (MainThread) [zigpy.device_scanner] Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x1000 (server): UNSUP_MANUF_GENERAL_COMMAND; falling back to discover_attributes +2026-03-15 19:04:50.792 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=142, command_id=, *direction=) +2026-03-15 19:04:50.792 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Sending request: Discover_Attributes(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:50.792 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 50, 792686, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=142, profile_id=260, cluster_id=4096, data=Serialized[b'\x04\x0b\x10\x8e\x0c\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:50.882 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=142, command_id=13, *direction=) +2026-03-15 19:04:50.882 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Decoded ZCL frame: LightLink:Discover_Attributes_rsp(discovery_complete=, attribute_info=[DiscoverAttributesResponseRecord(attrid=61440, datatype=40), DiscoverAttributesResponseRecord(attrid=61441, datatype=32), DiscoverAttributesResponseRecord(attrid=61442, datatype=32), DiscoverAttributesResponseRecord(attrid=61443, datatype=40), DiscoverAttributesResponseRecord(attrid=61696, datatype=35), DiscoverAttributesResponseRecord(attrid=61697, datatype=35)]) +2026-03-15 19:04:50.893 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=4096, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:50.895 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=4096, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:50.895 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=143, command_id=, *direction=) +2026-03-15 19:04:50.895 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Sending request: Read_Attributes(attribute_ids=[61440, 61441, 61442]) +2026-03-15 19:04:50.895 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 50, 895643, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=143, profile_id=260, cluster_id=4096, data=Serialized[b'\x04\x0b\x10\x8f\x00\x00\xf0\x01\xf0\x02\xf0'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:50.962 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=143, command_id=1, *direction=) +2026-03-15 19:04:50.962 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Decoded ZCL frame: LightLink:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=61440, status=, value=TypeValue(type=int8s, value=-50)), ReadAttributeRecord(attrid=61441, status=, value=TypeValue(type=uint8_t, value=10)), ReadAttributeRecord(attrid=61442, status=, value=TypeValue(type=uint8_t, value=8))]) +2026-03-15 19:04:50.974 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=144, command_id=, *direction=) +2026-03-15 19:04:50.974 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Sending request: Read_Attributes(attribute_ids=[61443, 61696, 61697]) +2026-03-15 19:04:50.974 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 50, 974487, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=144, profile_id=260, cluster_id=4096, data=Serialized[b'\x04\x0b\x10\x90\x00\x03\xf0\x00\xf1\x01\xf1'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:51.074 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=144, command_id=1, *direction=) +2026-03-15 19:04:51.074 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Decoded ZCL frame: LightLink:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=61443, status=, value=TypeValue(type=int8s, value=-45)), ReadAttributeRecord(attrid=61696, status=, value=TypeValue(type=uint32_t, value=13662022)), ReadAttributeRecord(attrid=61697, status=, value=TypeValue(type=uint32_t, value=0))]) +2026-03-15 19:04:51.086 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=4096, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:51.087 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=4096, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:51.087 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=145, command_id=, *direction=) +2026-03-15 19:04:51.087 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Sending request: Discover_Commands_Received(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:51.087 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 51, 87770, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=145, profile_id=260, cluster_id=4096, data=Serialized[b'\x04\x0b\x10\x91\x11\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:51.120 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=145, command_id=11, *direction=) +2026-03-15 19:04:51.120 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Decoded ZCL frame: LightLink:DefaultResponse(command_id=17, status=) +2026-03-15 19:04:51.120 WARNING (MainThread) [zigpy.device_scanner] Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x1000 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND +2026-03-15 19:04:51.130 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=4096, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:51.131 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=4096, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:51.132 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=146, command_id=, *direction=) +2026-03-15 19:04:51.132 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Sending request: Discover_Commands_Generated(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:51.132 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 51, 132276, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=146, profile_id=260, cluster_id=4096, data=Serialized[b'\x04\x0b\x10\x92\x13\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:51.169 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=146, command_id=11, *direction=) +2026-03-15 19:04:51.169 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x1000] Decoded ZCL frame: LightLink:DefaultResponse(command_id=19, status=) +2026-03-15 19:04:51.170 WARNING (MainThread) [zigpy.device_scanner] Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x1000 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND +2026-03-15 19:04:51.181 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=4096, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:51.181 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=64513, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:51.182 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=147, command_id=, *direction=) +2026-03-15 19:04:51.182 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Sending request: Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:51.182 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 51, 182308, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=147, profile_id=260, cluster_id=64513, data=Serialized[b'\x04\x0b\x10\x93\x15\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:51.226 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=147, command_id=11, *direction=) +2026-03-15 19:04:51.226 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Decoded ZCL frame: ManufacturerSpecificCluster:DefaultResponse(command_id=21, status=) +2026-03-15 19:04:51.226 WARNING (MainThread) [zigpy.device_scanner] Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0xfc01 (server): UNSUP_MANUF_GENERAL_COMMAND; falling back to discover_attributes +2026-03-15 19:04:51.226 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=148, command_id=, *direction=) +2026-03-15 19:04:51.226 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Sending request: Discover_Attributes(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:51.227 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 51, 227016, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=148, profile_id=260, cluster_id=64513, data=Serialized[b'\x04\x0b\x10\x94\x0c\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:51.265 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=148, command_id=13, *direction=) +2026-03-15 19:04:51.265 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Decoded ZCL frame: ManufacturerSpecificCluster:Discover_Attributes_rsp(discovery_complete=, attribute_info=[DiscoverAttributesResponseRecord(attrid=0, datatype=24), DiscoverAttributesResponseRecord(attrid=1, datatype=48), DiscoverAttributesResponseRecord(attrid=5, datatype=32), DiscoverAttributesResponseRecord(attrid=53249, datatype=35)]) +2026-03-15 19:04:51.276 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=64513, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:51.278 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=64513, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:51.278 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=149, command_id=, *direction=) +2026-03-15 19:04:51.278 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Sending request: Read_Attributes(attribute_ids=[0, 1, 5]) +2026-03-15 19:04:51.278 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 51, 278686, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=149, profile_id=260, cluster_id=64513, data=Serialized[b'\x04\x0b\x10\x95\x00\x00\x00\x01\x00\x05\x00'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:51.327 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=149, command_id=1, *direction=) +2026-03-15 19:04:51.328 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Decoded ZCL frame: ManufacturerSpecificCluster:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=0, status=, value=TypeValue(type=bitmap8, value=)), ReadAttributeRecord(attrid=1, status=, value=TypeValue(type=enum8, value=)), ReadAttributeRecord(attrid=5, status=, value=TypeValue(type=uint8_t, value=254))]) +2026-03-15 19:04:51.339 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=150, command_id=, *direction=) +2026-03-15 19:04:51.339 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Sending request: Read_Attributes(attribute_ids=[53249]) +2026-03-15 19:04:51.340 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 51, 340004, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=150, profile_id=260, cluster_id=64513, data=Serialized[b'\x04\x0b\x10\x96\x00\x01\xd0'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:51.375 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=150, command_id=1, *direction=) +2026-03-15 19:04:51.375 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Decoded ZCL frame: ManufacturerSpecificCluster:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=53249, status=, value=TypeValue(type=uint32_t, value=0))]) +2026-03-15 19:04:51.387 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=64513, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:51.388 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=64513, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:51.389 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=151, command_id=, *direction=) +2026-03-15 19:04:51.389 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Sending request: Discover_Commands_Received(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:51.389 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 51, 389281, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=151, profile_id=260, cluster_id=64513, data=Serialized[b'\x04\x0b\x10\x97\x11\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:51.546 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=151, command_id=11, *direction=) +2026-03-15 19:04:51.546 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Decoded ZCL frame: ManufacturerSpecificCluster:DefaultResponse(command_id=17, status=) +2026-03-15 19:04:51.546 WARNING (MainThread) [zigpy.device_scanner] Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0xfc01 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND +2026-03-15 19:04:51.557 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=64513, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:51.559 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=64513, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:51.559 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=152, command_id=, *direction=) +2026-03-15 19:04:51.559 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Sending request: Discover_Commands_Generated(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:51.559 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 51, 559694, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=152, profile_id=260, cluster_id=64513, data=Serialized[b'\x04\x0b\x10\x98\x13\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:51.837 DEBUG (MainThread) [zigpy.application] Failed to send packet, attempt 1 of 3 +2026-03-15 19:04:51.838 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 51, 838364, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=152, profile_id=260, cluster_id=64513, data=Serialized[b'\x04\x0b\x10\x98\x13\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:51.879 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=152, command_id=11, *direction=) +2026-03-15 19:04:51.879 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc01] Decoded ZCL frame: ManufacturerSpecificCluster:DefaultResponse(command_id=19, status=) +2026-03-15 19:04:51.879 WARNING (MainThread) [zigpy.device_scanner] Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0xfc01 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND +2026-03-15 19:04:51.891 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=64513, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:51.891 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=64515, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:51.891 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=153, command_id=, *direction=) +2026-03-15 19:04:51.891 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Sending request: Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:51.891 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 51, 891805, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=153, profile_id=260, cluster_id=64515, data=Serialized[b'\x04\x0b\x10\x99\x15\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:51.943 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=153, command_id=11, *direction=) +2026-03-15 19:04:51.943 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Decoded ZCL frame: PhilipsHueLightCluster:DefaultResponse(command_id=21, status=) +2026-03-15 19:04:51.943 WARNING (MainThread) [zigpy.device_scanner] Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0xfc03 (server): UNSUP_MANUF_GENERAL_COMMAND; falling back to discover_attributes +2026-03-15 19:04:51.944 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=154, command_id=, *direction=) +2026-03-15 19:04:51.944 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Sending request: Discover_Attributes(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:51.944 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 51, 944337, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=154, profile_id=260, cluster_id=64515, data=Serialized[b'\x04\x0b\x10\x9a\x0c\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:52.151 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=154, command_id=13, *direction=) +2026-03-15 19:04:52.151 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Decoded ZCL frame: PhilipsHueLightCluster:Discover_Attributes_rsp(discovery_complete=, attribute_info=[DiscoverAttributesResponseRecord(attrid=1, datatype=27), DiscoverAttributesResponseRecord(attrid=2, datatype=65), DiscoverAttributesResponseRecord(attrid=16, datatype=25), DiscoverAttributesResponseRecord(attrid=17, datatype=31)]) +2026-03-15 19:04:52.162 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=64515, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:52.164 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=64515, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:52.164 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=155, command_id=, *direction=) +2026-03-15 19:04:52.165 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Sending request: Read_Attributes(attribute_ids=[1, 2, 16]) +2026-03-15 19:04:52.165 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 52, 165198, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=155, profile_id=260, cluster_id=64515, data=Serialized[b'\x04\x0b\x10\x9b\x00\x01\x00\x02\x00\x10\x00'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:52.306 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=155, command_id=1, *direction=) +2026-03-15 19:04:52.306 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Decoded ZCL frame: PhilipsHueLightCluster:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=1, status=, value=TypeValue(type=bitmap32, value=)), ReadAttributeRecord(attrid=2, status=, value=TypeValue(type=LVBytes, value=b'\x0b\x00\x00\x90^\x80Oj')), ReadAttributeRecord(attrid=16, status=, value=TypeValue(type=bitmap16, value=))]) +2026-03-15 19:04:52.318 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=156, command_id=, *direction=) +2026-03-15 19:04:52.318 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Sending request: Read_Attributes(attribute_ids=[17]) +2026-03-15 19:04:52.318 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 52, 318671, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=156, profile_id=260, cluster_id=64515, data=Serialized[b'\x04\x0b\x10\x9c\x00\x11\x00'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:52.372 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=156, command_id=1, *direction=) +2026-03-15 19:04:52.372 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Decoded ZCL frame: PhilipsHueLightCluster:ReadAttributesResponse(status_records=[ReadAttributeRecord(attrid=17, status=, value=TypeValue(type=bitmap64, value=))]) +2026-03-15 19:04:52.384 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=64515, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:52.385 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=64515, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:52.385 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=157, command_id=, *direction=) +2026-03-15 19:04:52.385 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Sending request: Discover_Commands_Received(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:52.386 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 52, 386054, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=157, profile_id=260, cluster_id=64515, data=Serialized[b'\x04\x0b\x10\x9d\x11\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:52.425 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=157, command_id=11, *direction=) +2026-03-15 19:04:52.425 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Decoded ZCL frame: PhilipsHueLightCluster:DefaultResponse(command_id=17, status=) +2026-03-15 19:04:52.425 WARNING (MainThread) [zigpy.device_scanner] Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0xfc03 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND +2026-03-15 19:04:52.436 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=64515, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:52.437 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=64515, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:52.438 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Sending request header: ZCLHeader(frame_control=FrameControl<0x04>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=158, command_id=, *direction=) +2026-03-15 19:04:52.438 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Sending request: Discover_Commands_Generated(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:52.438 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 52, 438244, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=158, profile_id=260, cluster_id=64515, data=Serialized[b'\x04\x0b\x10\x9e\x13\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:52.474 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=158, command_id=11, *direction=) +2026-03-15 19:04:52.474 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0xfc03] Decoded ZCL frame: PhilipsHueLightCluster:DefaultResponse(command_id=19, status=) +2026-03-15 19:04:52.474 WARNING (MainThread) [zigpy.device_scanner] Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0xfc03 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND +2026-03-15 19:04:52.486 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=64515, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:52.486 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=25, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:52.486 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Sending request header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=159, command_id=, *direction=) +2026-03-15 19:04:52.486 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Sending request: Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:52.486 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 52, 486765, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=159, profile_id=260, cluster_id=25, data=Serialized[b'\x1c\x0b\x10\x9f\x15\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:52.524 DEBUG (MainThread) [zigpy.device] Cluster 0x0019 on has incorrect direction (got for cluster). Please report this here: https://github.com/zigpy/zigpy/issues/1640 +2026-03-15 19:04:52.524 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=159, command_id=11, *direction=) +2026-03-15 19:04:52.524 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Decoded ZCL frame: Ota:DefaultResponse(command_id=21, status=) +2026-03-15 19:04:52.525 WARNING (MainThread) [zigpy.device_scanner] Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0019 (client): UNSUP_MANUF_GENERAL_COMMAND; falling back to discover_attributes +2026-03-15 19:04:52.525 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Sending request header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=160, command_id=, *direction=) +2026-03-15 19:04:52.525 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Sending request: Discover_Attributes(start_attribute_id=0, max_attribute_ids=16) +2026-03-15 19:04:52.525 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 52, 525466, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=160, profile_id=260, cluster_id=25, data=Serialized[b'\x1c\x0b\x10\xa0\x0c\x00\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:52.573 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x14>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=160, command_id=13, *direction=) +2026-03-15 19:04:52.573 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Decoded ZCL frame: Ota:Discover_Attributes_rsp(discovery_complete=, attribute_info=[]) +2026-03-15 19:04:52.584 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_discovery', endpoint_id=11, cluster_id=25, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:52.585 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=25, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:52.586 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='attribute_reads', endpoint_id=11, cluster_id=25, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:52.587 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=25, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:52.587 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Sending request header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=161, command_id=, *direction=) +2026-03-15 19:04:52.587 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Sending request: Discover_Commands_Received(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:52.587 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 52, 587989, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=161, profile_id=260, cluster_id=25, data=Serialized[b'\x1c\x0b\x10\xa1\x11\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:52.784 DEBUG (MainThread) [zigpy.device] Cluster 0x0019 on has incorrect direction (got for cluster). Please report this here: https://github.com/zigpy/zigpy/issues/1640 +2026-03-15 19:04:52.784 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=161, command_id=11, *direction=) +2026-03-15 19:04:52.784 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Decoded ZCL frame: Ota:DefaultResponse(command_id=17, status=) +2026-03-15 19:04:52.785 WARNING (MainThread) [zigpy.device_scanner] Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0019 (client): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND +2026-03-15 19:04:52.796 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_received', endpoint_id=11, cluster_id=25, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:52.798 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_started for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='started', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=25, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:52.798 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Sending request header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=162, command_id=, *direction=) +2026-03-15 19:04:52.798 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Sending request: Discover_Commands_Generated(start_command_id=0, max_command_ids=16) +2026-03-15 19:04:52.799 DEBUG (MainThread) [bellows.zigbee.application] Sending packet ZigbeePacket(timestamp=datetime.datetime(2026, 3, 15, 23, 4, 52, 799056, tzinfo=datetime.timezone.utc), priority=None, src=AddrModeAddress(addr_mode=, address=0x0000), src_ep=11, dst=AddrModeAddress(addr_mode=, address=0x40D1), dst_ep=11, source_route=[], extended_timeout=False, tsn=162, profile_id=260, cluster_id=25, data=Serialized[b'\x1c\x0b\x10\xa2\x13\x00\x10'], tx_options=, radius=0, non_member_radius=0, lqi=None, rssi=None) +2026-03-15 19:04:52.832 DEBUG (MainThread) [zigpy.device] Cluster 0x0019 on has incorrect direction (got for cluster). Please report this here: https://github.com/zigpy/zigpy/issues/1640 +2026-03-15 19:04:52.833 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl<0x1C>(frame_type=, is_manufacturer_specific=1, direction=, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), manufacturer=4107, tsn=162, command_id=11, *direction=) +2026-03-15 19:04:52.833 DEBUG (MainThread) [zigpy.zcl] [0x40D1:11:0x0019] Decoded ZCL frame: Ota:DefaultResponse(command_id=19, status=) +2026-03-15 19:04:52.833 WARNING (MainThread) [zigpy.device_scanner] Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0019 (client): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND +2026-03-15 19:04:52.844 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan step_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome=None, step='command_discovery_generated', endpoint_id=11, cluster_id=25, cluster_type=, manufacturer_code_scope=4107, error_code=None, error=None) +2026-03-15 19:04:52.844 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan scan_finished for 00:17:88:01:0b:4b:51:fc: DeviceScanProgressEvent(ieee=00:17:88:01:0b:4b:51:fc, status='success', outcome='success', step=None, endpoint_id=None, cluster_id=None, cluster_type=None, manufacturer_code_scope=None, error_code=None, error=None) +2026-03-15 19:04:52.844 INFO (MainThread) [homeassistant.components.zha.websocket_api] Device scan summary for 00:17:88:01:0b:4b:51:fc: DeviceScanSummary(ieee=00:17:88:01:0b:4b:51:fc, completed=True, outcome='success', used_resume=False, force_full=True, descriptor_refresh_performed=True, last_finished=datetime.datetime(2026, 3, 15, 23, 4, 52, 844612, tzinfo=datetime.timezone.utc), error_code=None, last_error=None) diff --git a/docs/plans/device_scan_explanation.md b/docs/plans/device_scan_explanation.md new file mode 100644 index 000000000..22b38ea0b --- /dev/null +++ b/docs/plans/device_scan_explanation.md @@ -0,0 +1,420 @@ +# Successful Device Scan: 00:17:88:01:0b:4b:51:fc + +## Source + +- Source log: `/Users/davidmulcahey/.homeassistant/home-assistant.log` +- Source line range: `107962-114796` +- Extracted transaction log: `/Users/davidmulcahey/zha_refactor/device_scan.log` + +The extracted log keeps the scan service events, outbound Zigbee requests, decoded ZDO/ZCL responses, and scanner warnings for this transaction. It intentionally omits repetitive EZSP bookkeeping, raw ASH serial frames, and unrelated Home Assistant noise. + +## Summary + +- Device IEEE: `00:17:88:01:0b:4b:51:fc` +- Device NWK: `0x40d1` +- Scan window: `2026-03-15 19:04:33.046000` to `2026-03-15 19:04:52.844000` +- Duration: `0:00:19.798000` +- Outcome: `success` +- `used_resume`: `False` +- `force_full`: `True` +- `descriptor_refresh_performed`: `True` +- Standard-scope targets scanned: `11` +- Manufacturer-scope targets scanned: `11` + +## Descriptor Refresh + +- Node descriptor: `NodeDescriptor(logical_type=, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=, mac_capability_flags=, manufacturer_code=4107, maximum_buffer_size=82, maximum_incoming_transfer_size=128, server_mask=11264, maximum_outgoing_transfer_size=128, descriptor_capability_field=, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=False, *is_full_function_device=True, *is_mains_powered=True, *is_receiver_on_when_idle=True, *is_router=True, *is_security_capable=False)` +- Active endpoints: `[11, 242]` +- SizePrefixedSimpleDescriptor(endpoint=11, profile=260, device_type=269, device_version=1, input_clusters=[0, 3, 4, 5, 6, 8, 4096, 64515, 768, 64513], output_clusters=[25]) +- SizePrefixedSimpleDescriptor(endpoint=242, profile=41440, device_type=97, device_version=0, input_clusters=[], output_clusters=[33]) + +Endpoint `11` is the application endpoint that was scanned in raw detail. Endpoint `242` is present in the descriptor refresh result as a Green Power proxy endpoint, but it is not part of the raw inventory loop. + +## Coverage + +- Standard scope targets: + - Endpoint `11` cluster `0x0000` `Basic` (server) + - Endpoint `11` cluster `0x0003` `Identify` (server) + - Endpoint `11` cluster `0x0004` `Groups` (server) + - Endpoint `11` cluster `0x0005` `Scenes` (server) + - Endpoint `11` cluster `0x0006` `OnOff` (server) + - Endpoint `11` cluster `0x0008` `LevelControl` (server) + - Endpoint `11` cluster `0x0300` `Color` (server) + - Endpoint `11` cluster `0x1000` `LightLink` (server) + - Endpoint `11` cluster `0xfc01` `ManufacturerSpecificCluster` (server) + - Endpoint `11` cluster `0xfc03` `PhilipsHueLightCluster` (server) + - Endpoint `11` cluster `0x0019` `Ota` (client) +- Manufacturer scope targets: + - Endpoint `11` cluster `0x0000` `Basic` (server), manufacturer code `4107` + - Endpoint `11` cluster `0x0003` `Identify` (server), manufacturer code `4107` + - Endpoint `11` cluster `0x0004` `Groups` (server), manufacturer code `4107` + - Endpoint `11` cluster `0x0005` `Scenes` (server), manufacturer code `4107` + - Endpoint `11` cluster `0x0006` `OnOff` (server), manufacturer code `4107` + - Endpoint `11` cluster `0x0008` `LevelControl` (server), manufacturer code `4107` + - Endpoint `11` cluster `0x0300` `Color` (server), manufacturer code `4107` + - Endpoint `11` cluster `0x1000` `LightLink` (server), manufacturer code `4107` + - Endpoint `11` cluster `0xfc01` `ManufacturerSpecificCluster` (server), manufacturer code `4107` + - Endpoint `11` cluster `0xfc03` `PhilipsHueLightCluster` (server), manufacturer code `4107` + - Endpoint `11` cluster `0x0019` `Ota` (client), manufacturer code `4107` + +## Detailed Results + +Each section below reflects one scanned target. Attribute IDs and command IDs are taken from the decoded discovery responses in the log. Raw read payloads remain in `device_scan.log` for exact value inspection. + +### Endpoint 11 / 0x0000 / Basic / server / standard + +- `attribute_discovery`: `success` +- Requests issued for `attribute_discovery`: `Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16)`, `Discover_Attributes(start_attribute_id=0, max_attribute_ids=16)` +- Discovered attribute IDs: `0`, `1`, `2`, `3`, `4`, `5`, `6`, `7`, `8`, `9`, `10`, `11`, `16384`, `65533` +- Note: `Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0000 (server): UNSUP_GENERAL_COMMAND; falling back to discover_attributes` +- `attribute_reads`: `success` +- Transport retries during `attribute_reads`: `2` +- Requests issued for `attribute_reads`: `Read_Attributes(attribute_ids=[0, 1, 2])`, `Read_Attributes(attribute_ids=[3, 4, 5])`, `Read_Attributes(attribute_ids=[6, 7, 8])`, `Read_Attributes(attribute_ids=[6, 7, 8])`, `Read_Attributes(attribute_ids=[9, 10, 11])`, `Read_Attributes(attribute_ids=[16384, 65533])` +- Read results: `0:SUCCESS`, `1:SUCCESS`, `2:SUCCESS`, `3:SUCCESS`, `4:SUCCESS`, `5:SUCCESS`, `6:SUCCESS`, `7:SUCCESS`, `8:SUCCESS`, `9:SUCCESS`, `10:SUCCESS`, `11:SUCCESS`, `16384:SUCCESS`, `65533:SUCCESS` +- `command_discovery_received`: `success` +- Requests issued for `command_discovery_received`: `Discover_Commands_Received(start_command_id=0, max_command_ids=16)` +- Note: `Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0000 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND` +- `command_discovery_generated`: `success` +- Requests issued for `command_discovery_generated`: `Discover_Commands_Generated(start_command_id=0, max_command_ids=16)` +- Note: `Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0000 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND` + +### Endpoint 11 / 0x0003 / Identify / server / standard + +- `attribute_discovery`: `success` +- Requests issued for `attribute_discovery`: `Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16)`, `Discover_Attributes(start_attribute_id=0, max_attribute_ids=16)` +- Discovered attribute IDs: `0`, `65533` +- Note: `Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0003 (server): UNSUP_GENERAL_COMMAND; falling back to discover_attributes` +- `attribute_reads`: `success` +- Requests issued for `attribute_reads`: `Read_Attributes(attribute_ids=[0, 65533])` +- Read results: `0:SUCCESS`, `65533:SUCCESS` +- `command_discovery_received`: `success` +- Requests issued for `command_discovery_received`: `Discover_Commands_Received(start_command_id=0, max_command_ids=16)` +- Note: `Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0003 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND` +- `command_discovery_generated`: `success` +- Requests issued for `command_discovery_generated`: `Discover_Commands_Generated(start_command_id=0, max_command_ids=16)` +- Note: `Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0003 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND` + +### Endpoint 11 / 0x0004 / Groups / server / standard + +- `attribute_discovery`: `success` +- Requests issued for `attribute_discovery`: `Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16)`, `Discover_Attributes(start_attribute_id=0, max_attribute_ids=16)` +- Discovered attribute IDs: `0`, `65533` +- Note: `Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0004 (server): UNSUP_GENERAL_COMMAND; falling back to discover_attributes` +- `attribute_reads`: `success` +- Requests issued for `attribute_reads`: `Read_Attributes(attribute_ids=[0, 65533])` +- Read results: `0:SUCCESS`, `65533:SUCCESS` +- `command_discovery_received`: `success` +- Requests issued for `command_discovery_received`: `Discover_Commands_Received(start_command_id=0, max_command_ids=16)` +- Note: `Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0004 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND` +- `command_discovery_generated`: `success` +- Requests issued for `command_discovery_generated`: `Discover_Commands_Generated(start_command_id=0, max_command_ids=16)` +- Note: `Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0004 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND` + +### Endpoint 11 / 0x0005 / Scenes / server / standard + +- `attribute_discovery`: `success` +- Requests issued for `attribute_discovery`: `Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16)`, `Discover_Attributes(start_attribute_id=0, max_attribute_ids=16)` +- Discovered attribute IDs: `0`, `1`, `2`, `3`, `4`, `65533` +- Note: `Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0005 (server): UNSUP_GENERAL_COMMAND; falling back to discover_attributes` +- `attribute_reads`: `success` +- Requests issued for `attribute_reads`: `Read_Attributes(attribute_ids=[0, 1, 2])`, `Read_Attributes(attribute_ids=[3, 4, 65533])` +- Read results: `0:SUCCESS`, `1:SUCCESS`, `2:SUCCESS`, `3:SUCCESS`, `4:SUCCESS`, `65533:SUCCESS` +- `command_discovery_received`: `success` +- Requests issued for `command_discovery_received`: `Discover_Commands_Received(start_command_id=0, max_command_ids=16)` +- Note: `Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0005 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND` +- `command_discovery_generated`: `success` +- Requests issued for `command_discovery_generated`: `Discover_Commands_Generated(start_command_id=0, max_command_ids=16)` +- Note: `Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0005 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND` + +### Endpoint 11 / 0x0006 / OnOff / server / standard + +- `attribute_discovery`: `success` +- Requests issued for `attribute_discovery`: `Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16)`, `Discover_Attributes(start_attribute_id=0, max_attribute_ids=16)` +- Discovered attribute IDs: `0`, `16384`, `16385`, `16386`, `16387`, `65533` +- Note: `Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0006 (server): UNSUP_GENERAL_COMMAND; falling back to discover_attributes` +- `attribute_reads`: `success` +- Requests issued for `attribute_reads`: `Read_Attributes(attribute_ids=[0, 16384, 16385])`, `Read_Attributes(attribute_ids=[16386, 16387, 65533])` +- Read results: `0:SUCCESS`, `16384:SUCCESS`, `16385:SUCCESS`, `16386:SUCCESS`, `16387:SUCCESS`, `65533:SUCCESS` +- `command_discovery_received`: `success` +- Requests issued for `command_discovery_received`: `Discover_Commands_Received(start_command_id=0, max_command_ids=16)` +- Note: `Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0006 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND` +- `command_discovery_generated`: `success` +- Requests issued for `command_discovery_generated`: `Discover_Commands_Generated(start_command_id=0, max_command_ids=16)` +- Note: `Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0006 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND` + +### Endpoint 11 / 0x0008 / LevelControl / server / standard + +- `attribute_discovery`: `success` +- Requests issued for `attribute_discovery`: `Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16)`, `Discover_Attributes(start_attribute_id=0, max_attribute_ids=16)` +- Discovered attribute IDs: `0`, `1`, `15`, `16384`, `65533` +- Note: `Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0008 (server): UNSUP_GENERAL_COMMAND; falling back to discover_attributes` +- `attribute_reads`: `success` +- Requests issued for `attribute_reads`: `Read_Attributes(attribute_ids=[0, 1, 15])`, `Read_Attributes(attribute_ids=[16384, 65533])` +- Read results: `0:SUCCESS`, `1:SUCCESS`, `15:SUCCESS`, `16384:SUCCESS`, `65533:SUCCESS` +- `command_discovery_received`: `success` +- Requests issued for `command_discovery_received`: `Discover_Commands_Received(start_command_id=0, max_command_ids=16)` +- Note: `Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0008 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND` +- `command_discovery_generated`: `success` +- Requests issued for `command_discovery_generated`: `Discover_Commands_Generated(start_command_id=0, max_command_ids=16)` +- Note: `Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0008 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND` + +### Endpoint 11 / 0x0300 / Color / server / standard + +- `attribute_discovery`: `success` +- Requests issued for `attribute_discovery`: `Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16)`, `Discover_Attributes(start_attribute_id=0, max_attribute_ids=16)`, `Discover_Attributes(start_attribute_id=26, max_attribute_ids=16)`, `Discover_Attributes(start_attribute_id=16387, max_attribute_ids=16)` +- Discovered attribute IDs: `0`, `1`, `2`, `3`, `4`, `7`, `8`, `15`, `16`, `17`, `18`, `19`, `21`, `22`, `23`, `25`, `26`, `27`, `48`, `49`, `50`, `51`, `52`, `54`, `55`, `56`, `58`, `59`, `60`, `16384`, `16385`, `16386`, `16387`, `16388`, `16389`, `16390`, `16394`, `16395`, `16396`, `16397`, `16400`, `65533` +- Note: `Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0300 (server): UNSUP_GENERAL_COMMAND; falling back to discover_attributes` +- `attribute_reads`: `success` +- Requests issued for `attribute_reads`: `Read_Attributes(attribute_ids=[0, 1, 2])`, `Read_Attributes(attribute_ids=[3, 4, 7])`, `Read_Attributes(attribute_ids=[8, 15, 16])`, `Read_Attributes(attribute_ids=[17, 18, 19])`, `Read_Attributes(attribute_ids=[21, 22, 23])`, `Read_Attributes(attribute_ids=[25, 26, 27])`, `Read_Attributes(attribute_ids=[48, 49, 50])`, `Read_Attributes(attribute_ids=[51, 52, 54])`, `Read_Attributes(attribute_ids=[55, 56, 58])`, `Read_Attributes(attribute_ids=[59, 60, 16384])`, `Read_Attributes(attribute_ids=[16385, 16386, 16387])`, `Read_Attributes(attribute_ids=[16388, 16389, 16390])`, `Read_Attributes(attribute_ids=[16394, 16395, 16396])`, `Read_Attributes(attribute_ids=[16397, 16400, 65533])` +- Read results: `0:SUCCESS`, `1:SUCCESS`, `2:SUCCESS`, `3:SUCCESS`, `4:SUCCESS`, `7:SUCCESS`, `8:SUCCESS`, `15:SUCCESS`, `16:SUCCESS`, `17:SUCCESS`, `18:SUCCESS`, `19:SUCCESS`, `21:SUCCESS`, `22:SUCCESS`, `23:SUCCESS`, `25:SUCCESS`, `26:SUCCESS`, `27:SUCCESS`, `48:SUCCESS`, `49:SUCCESS`, `50:SUCCESS`, `51:SUCCESS`, `52:SUCCESS`, `54:SUCCESS`, `55:SUCCESS`, `56:SUCCESS`, `58:SUCCESS`, `59:SUCCESS`, `60:SUCCESS`, `16384:SUCCESS`, `16385:SUCCESS`, `16386:SUCCESS`, `16387:SUCCESS`, `16388:SUCCESS`, `16389:SUCCESS`, `16390:SUCCESS`, `16394:SUCCESS`, `16395:SUCCESS`, `16396:SUCCESS`, `16397:SUCCESS`, `16400:SUCCESS`, `65533:SUCCESS` +- `command_discovery_received`: `success` +- Requests issued for `command_discovery_received`: `Discover_Commands_Received(start_command_id=0, max_command_ids=16)` +- Note: `Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0300 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND` +- `command_discovery_generated`: `success` +- Requests issued for `command_discovery_generated`: `Discover_Commands_Generated(start_command_id=0, max_command_ids=16)` +- Note: `Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0300 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND` + +### Endpoint 11 / 0x1000 / LightLink / server / standard + +- `attribute_discovery`: `success` +- Requests issued for `attribute_discovery`: `Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16)`, `Discover_Attributes(start_attribute_id=0, max_attribute_ids=16)` +- Discovered attribute IDs: `65533` +- Note: `Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x1000 (server): UNSUP_GENERAL_COMMAND; falling back to discover_attributes` +- `attribute_reads`: `success` +- Requests issued for `attribute_reads`: `Read_Attributes(attribute_ids=[65533])` +- Read results: `65533:SUCCESS` +- `command_discovery_received`: `success` +- Requests issued for `command_discovery_received`: `Discover_Commands_Received(start_command_id=0, max_command_ids=16)` +- Note: `Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x1000 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND` +- `command_discovery_generated`: `success` +- Requests issued for `command_discovery_generated`: `Discover_Commands_Generated(start_command_id=0, max_command_ids=16)` +- Note: `Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x1000 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND` + +### Endpoint 11 / 0xfc01 / ManufacturerSpecificCluster / server / standard + +- `attribute_discovery`: `success` +- Requests issued for `attribute_discovery`: `Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16)`, `Discover_Attributes(start_attribute_id=0, max_attribute_ids=16)` +- Note: `Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0xfc01 (server): UNSUP_GENERAL_COMMAND; falling back to discover_attributes` +- `attribute_reads`: `success` +- `command_discovery_received`: `success` +- Requests issued for `command_discovery_received`: `Discover_Commands_Received(start_command_id=0, max_command_ids=16)` +- Note: `Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0xfc01 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND` +- `command_discovery_generated`: `success` +- Requests issued for `command_discovery_generated`: `Discover_Commands_Generated(start_command_id=0, max_command_ids=16)` +- Note: `Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0xfc01 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND` + +### Endpoint 11 / 0xfc03 / PhilipsHueLightCluster / server / standard + +- `attribute_discovery`: `success` +- Requests issued for `attribute_discovery`: `Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16)`, `Discover_Attributes(start_attribute_id=0, max_attribute_ids=16)` +- Note: `Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0xfc03 (server): UNSUP_GENERAL_COMMAND; falling back to discover_attributes` +- `attribute_reads`: `success` +- `command_discovery_received`: `success` +- Requests issued for `command_discovery_received`: `Discover_Commands_Received(start_command_id=0, max_command_ids=16)` +- Note: `Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0xfc03 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND` +- `command_discovery_generated`: `success` +- Requests issued for `command_discovery_generated`: `Discover_Commands_Generated(start_command_id=0, max_command_ids=16)` +- Note: `Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0xfc03 (server): unsupported command discovery response UNSUP_GENERAL_COMMAND` + +### Endpoint 11 / 0x0019 / Ota / client / standard + +- `attribute_discovery`: `success` +- Requests issued for `attribute_discovery`: `Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16)`, `Discover_Attributes(start_attribute_id=0, max_attribute_ids=16)` +- Discovered attribute IDs: `0`, `1`, `2`, `3`, `4`, `6`, `7`, `8`, `9`, `65533` +- Note: `Cluster 0x0019 on has incorrect direction (got for cluster). Please report this here: https://github.com/zigpy/zigpy/issues/1640` +- Note: `Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0019 (client): UNSUP_GENERAL_COMMAND; falling back to discover_attributes` +- `attribute_reads`: `success` +- Requests issued for `attribute_reads`: `Read_Attributes(attribute_ids=[0, 1, 2])`, `Read_Attributes(attribute_ids=[3, 4, 6])`, `Read_Attributes(attribute_ids=[7, 8, 9])`, `Read_Attributes(attribute_ids=[65533])` +- Read results: `0:SUCCESS`, `1:SUCCESS`, `2:SUCCESS`, `3:SUCCESS`, `4:SUCCESS`, `6:SUCCESS`, `7:SUCCESS`, `8:SUCCESS`, `9:SUCCESS`, `65533:SUCCESS` +- `command_discovery_received`: `success` +- Requests issued for `command_discovery_received`: `Discover_Commands_Received(start_command_id=0, max_command_ids=16)` +- Note: `Cluster 0x0019 on has incorrect direction (got for cluster). Please report this here: https://github.com/zigpy/zigpy/issues/1640` +- Note: `Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0019 (client): unsupported command discovery response UNSUP_GENERAL_COMMAND` +- `command_discovery_generated`: `success` +- Requests issued for `command_discovery_generated`: `Discover_Commands_Generated(start_command_id=0, max_command_ids=16)` +- Note: `Cluster 0x0019 on has incorrect direction (got for cluster). Please report this here: https://github.com/zigpy/zigpy/issues/1640` +- Note: `Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0019 (client): unsupported command discovery response UNSUP_GENERAL_COMMAND` + +### Endpoint 11 / 0x0000 / Basic / server / manufacturer 4107 + +- `attribute_discovery`: `success` +- Requests issued for `attribute_discovery`: `Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16)`, `Discover_Attributes(start_attribute_id=0, max_attribute_ids=16)`, `Discover_Attributes(start_attribute_id=61707, max_attribute_ids=16)`, `Discover_Attributes(start_attribute_id=61731, max_attribute_ids=16)` +- Discovered attribute IDs: `1`, `32`, `33`, `64`, `65`, `80`, `81`, `82`, `61697`, `61698`, `61700`, `61702`, `61703`, `61704`, `61705`, `61706`, `61707`, `61708`, `61716`, `61717`, `61718`, `61719`, `61720`, `61721`, `61722`, `61724`, `61725`, `61726`, `61727`, `61728`, `61729`, `61730`, `61731`, `61733`, `61734`, `61735` +- Note: `Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0000 (server): UNSUP_MANUF_GENERAL_COMMAND; falling back to discover_attributes` +- `attribute_reads`: `success` +- Transport retries during `attribute_reads`: `1` +- Requests issued for `attribute_reads`: `Read_Attributes(attribute_ids=[1, 32, 33])`, `Read_Attributes(attribute_ids=[64, 65, 80])`, `Read_Attributes(attribute_ids=[81, 82, 61697])`, `Read_Attributes(attribute_ids=[61698, 61700, 61702])`, `Read_Attributes(attribute_ids=[61703, 61704, 61705])`, `Read_Attributes(attribute_ids=[61706, 61707, 61708])`, `Read_Attributes(attribute_ids=[61716, 61717, 61718])`, `Read_Attributes(attribute_ids=[61719, 61720, 61721])`, `Read_Attributes(attribute_ids=[61721])`, `Read_Attributes(attribute_ids=[61722, 61724, 61725])`, `Read_Attributes(attribute_ids=[61724])`, `Read_Attributes(attribute_ids=[61725])`, `Read_Attributes(attribute_ids=[61726, 61727, 61728])`, `Read_Attributes(attribute_ids=[61729, 61730, 61731])`, `Read_Attributes(attribute_ids=[61733, 61734, 61735])` +- Read results: `1:SUCCESS`, `32:SUCCESS`, `33:SUCCESS`, `64:SUCCESS`, `65:SUCCESS`, `80:SUCCESS`, `81:SUCCESS`, `82:WRITE_ONLY`, `61697:SUCCESS`, `61698:WRITE_ONLY`, `61700:SUCCESS`, `61702:SUCCESS`, `61703:WRITE_ONLY`, `61704:SUCCESS`, `61705:SUCCESS`, `61706:SUCCESS`, `61707:SUCCESS`, `61708:SUCCESS`, `61716:SUCCESS`, `61717:WRITE_ONLY`, `61718:SUCCESS`, `61719:SUCCESS`, `61720:SUCCESS`, `61721:SUCCESS`, `61722:SUCCESS`, `61724:SUCCESS`, `61725:SUCCESS`, `61726:SUCCESS`, `61727:SUCCESS`, `61728:SUCCESS`, `61729:SUCCESS`, `61730:SUCCESS`, `61731:SUCCESS`, `61733:SUCCESS`, `61734:SUCCESS`, `61735:SUCCESS` +- Note: `Read_Attributes response missing status records for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0000 (server) manufacturer 4107: missing 0xf119; response contained 0xf117, 0xf118` +- Note: `Read_Attributes response missing status records for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0000 (server) manufacturer 4107: missing 0xf11c, 0xf11d; response contained 0xf11a` +- `command_discovery_received`: `success` +- Requests issued for `command_discovery_received`: `Discover_Commands_Received(start_command_id=0, max_command_ids=16)` +- Note: `Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0000 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND` +- `command_discovery_generated`: `success` +- Requests issued for `command_discovery_generated`: `Discover_Commands_Generated(start_command_id=0, max_command_ids=16)` +- Note: `Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0000 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND` + +### Endpoint 11 / 0x0003 / Identify / server / manufacturer 4107 + +- `attribute_discovery`: `success` +- Requests issued for `attribute_discovery`: `Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16)`, `Discover_Attributes(start_attribute_id=0, max_attribute_ids=16)` +- Note: `Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0003 (server): UNSUP_MANUF_GENERAL_COMMAND; falling back to discover_attributes` +- `attribute_reads`: `success` +- `command_discovery_received`: `success` +- Requests issued for `command_discovery_received`: `Discover_Commands_Received(start_command_id=0, max_command_ids=16)` +- Note: `Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0003 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND` +- `command_discovery_generated`: `success` +- Requests issued for `command_discovery_generated`: `Discover_Commands_Generated(start_command_id=0, max_command_ids=16)` +- Note: `Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0003 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND` + +### Endpoint 11 / 0x0004 / Groups / server / manufacturer 4107 + +- `attribute_discovery`: `success` +- Requests issued for `attribute_discovery`: `Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16)`, `Discover_Attributes(start_attribute_id=0, max_attribute_ids=16)` +- Note: `Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0004 (server): UNSUP_MANUF_GENERAL_COMMAND; falling back to discover_attributes` +- `attribute_reads`: `success` +- `command_discovery_received`: `success` +- Requests issued for `command_discovery_received`: `Discover_Commands_Received(start_command_id=0, max_command_ids=16)` +- Note: `Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0004 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND` +- `command_discovery_generated`: `success` +- Requests issued for `command_discovery_generated`: `Discover_Commands_Generated(start_command_id=0, max_command_ids=16)` +- Note: `Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0004 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND` + +### Endpoint 11 / 0x0005 / Scenes / server / manufacturer 4107 + +- `attribute_discovery`: `success` +- Requests issued for `attribute_discovery`: `Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16)`, `Discover_Attributes(start_attribute_id=0, max_attribute_ids=16)` +- Discovered attribute IDs: `1` +- Note: `Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0005 (server): UNSUP_MANUF_GENERAL_COMMAND; falling back to discover_attributes` +- `attribute_reads`: `success` +- Requests issued for `attribute_reads`: `Read_Attributes(attribute_ids=[1])` +- Read results: `1:SUCCESS` +- `command_discovery_received`: `success` +- Requests issued for `command_discovery_received`: `Discover_Commands_Received(start_command_id=0, max_command_ids=16)` +- Note: `Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0005 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND` +- `command_discovery_generated`: `success` +- Requests issued for `command_discovery_generated`: `Discover_Commands_Generated(start_command_id=0, max_command_ids=16)` +- Note: `Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0005 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND` + +### Endpoint 11 / 0x0006 / OnOff / server / manufacturer 4107 + +- `attribute_discovery`: `success` +- Requests issued for `attribute_discovery`: `Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16)`, `Discover_Attributes(start_attribute_id=0, max_attribute_ids=16)` +- Note: `Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0006 (server): UNSUP_MANUF_GENERAL_COMMAND; falling back to discover_attributes` +- `attribute_reads`: `success` +- `command_discovery_received`: `success` +- Requests issued for `command_discovery_received`: `Discover_Commands_Received(start_command_id=0, max_command_ids=16)` +- Note: `Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0006 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND` +- `command_discovery_generated`: `success` +- Requests issued for `command_discovery_generated`: `Discover_Commands_Generated(start_command_id=0, max_command_ids=16)` +- Note: `Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0006 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND` + +### Endpoint 11 / 0x0008 / LevelControl / server / manufacturer 4107 + +- `attribute_discovery`: `success` +- Requests issued for `attribute_discovery`: `Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16)`, `Discover_Attributes(start_attribute_id=0, max_attribute_ids=16)` +- Discovered attribute IDs: `3` +- Note: `Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0008 (server): UNSUP_MANUF_GENERAL_COMMAND; falling back to discover_attributes` +- `attribute_reads`: `success` +- Requests issued for `attribute_reads`: `Read_Attributes(attribute_ids=[3])` +- Read results: `3:SUCCESS` +- `command_discovery_received`: `success` +- Requests issued for `command_discovery_received`: `Discover_Commands_Received(start_command_id=0, max_command_ids=16)` +- Note: `Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0008 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND` +- `command_discovery_generated`: `success` +- Requests issued for `command_discovery_generated`: `Discover_Commands_Generated(start_command_id=0, max_command_ids=16)` +- Note: `Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0008 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND` + +### Endpoint 11 / 0x0300 / Color / server / manufacturer 4107 + +- `attribute_discovery`: `success` +- Requests issued for `attribute_discovery`: `Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16)`, `Discover_Attributes(start_attribute_id=0, max_attribute_ids=16)` +- Discovered attribute IDs: `3`, `4`, `61440`, `61445`, `61446`, `61447`, `61448`, `61697`, `61953`, `62209`, `62465`, `62721`, `64000` +- Note: `Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0300 (server): UNSUP_MANUF_GENERAL_COMMAND; falling back to discover_attributes` +- `attribute_reads`: `success` +- Requests issued for `attribute_reads`: `Read_Attributes(attribute_ids=[3, 4, 61440])`, `Read_Attributes(attribute_ids=[61445, 61446, 61447])`, `Read_Attributes(attribute_ids=[61448, 61697, 61953])`, `Read_Attributes(attribute_ids=[62209, 62465, 62721])`, `Read_Attributes(attribute_ids=[64000])` +- Read results: `3:SUCCESS`, `4:SUCCESS`, `61440:SUCCESS`, `61445:WRITE_ONLY`, `61446:SUCCESS`, `61447:SUCCESS`, `61448:SUCCESS`, `61697:SUCCESS`, `61953:SUCCESS`, `62209:SUCCESS`, `62465:SUCCESS`, `62721:SUCCESS`, `64000:SUCCESS` +- `command_discovery_received`: `success` +- Requests issued for `command_discovery_received`: `Discover_Commands_Received(start_command_id=0, max_command_ids=16)` +- Note: `Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0300 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND` +- `command_discovery_generated`: `success` +- Requests issued for `command_discovery_generated`: `Discover_Commands_Generated(start_command_id=0, max_command_ids=16)` +- Note: `Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0300 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND` + +### Endpoint 11 / 0x1000 / LightLink / server / manufacturer 4107 + +- `attribute_discovery`: `success` +- Requests issued for `attribute_discovery`: `Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16)`, `Discover_Attributes(start_attribute_id=0, max_attribute_ids=16)` +- Discovered attribute IDs: `61440`, `61441`, `61442`, `61443`, `61696`, `61697` +- Note: `Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x1000 (server): UNSUP_MANUF_GENERAL_COMMAND; falling back to discover_attributes` +- `attribute_reads`: `success` +- Requests issued for `attribute_reads`: `Read_Attributes(attribute_ids=[61440, 61441, 61442])`, `Read_Attributes(attribute_ids=[61443, 61696, 61697])` +- Read results: `61440:SUCCESS`, `61441:SUCCESS`, `61442:SUCCESS`, `61443:SUCCESS`, `61696:SUCCESS`, `61697:SUCCESS` +- `command_discovery_received`: `success` +- Requests issued for `command_discovery_received`: `Discover_Commands_Received(start_command_id=0, max_command_ids=16)` +- Note: `Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x1000 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND` +- `command_discovery_generated`: `success` +- Requests issued for `command_discovery_generated`: `Discover_Commands_Generated(start_command_id=0, max_command_ids=16)` +- Note: `Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x1000 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND` + +### Endpoint 11 / 0xfc01 / ManufacturerSpecificCluster / server / manufacturer 4107 + +- `attribute_discovery`: `success` +- Requests issued for `attribute_discovery`: `Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16)`, `Discover_Attributes(start_attribute_id=0, max_attribute_ids=16)` +- Discovered attribute IDs: `0`, `1`, `5`, `53249` +- Note: `Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0xfc01 (server): UNSUP_MANUF_GENERAL_COMMAND; falling back to discover_attributes` +- `attribute_reads`: `success` +- Requests issued for `attribute_reads`: `Read_Attributes(attribute_ids=[0, 1, 5])`, `Read_Attributes(attribute_ids=[53249])` +- Read results: `0:SUCCESS`, `1:SUCCESS`, `5:SUCCESS`, `53249:SUCCESS` +- `command_discovery_received`: `success` +- Requests issued for `command_discovery_received`: `Discover_Commands_Received(start_command_id=0, max_command_ids=16)` +- Note: `Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0xfc01 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND` +- `command_discovery_generated`: `success` +- Transport retries during `command_discovery_generated`: `1` +- Requests issued for `command_discovery_generated`: `Discover_Commands_Generated(start_command_id=0, max_command_ids=16)` +- Note: `Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0xfc01 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND` + +### Endpoint 11 / 0xfc03 / PhilipsHueLightCluster / server / manufacturer 4107 + +- `attribute_discovery`: `success` +- Requests issued for `attribute_discovery`: `Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16)`, `Discover_Attributes(start_attribute_id=0, max_attribute_ids=16)` +- Discovered attribute IDs: `1`, `2`, `16`, `17` +- Note: `Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0xfc03 (server): UNSUP_MANUF_GENERAL_COMMAND; falling back to discover_attributes` +- `attribute_reads`: `success` +- Requests issued for `attribute_reads`: `Read_Attributes(attribute_ids=[1, 2, 16])`, `Read_Attributes(attribute_ids=[17])` +- Read results: `1:SUCCESS`, `2:SUCCESS`, `16:SUCCESS`, `17:SUCCESS` +- `command_discovery_received`: `success` +- Requests issued for `command_discovery_received`: `Discover_Commands_Received(start_command_id=0, max_command_ids=16)` +- Note: `Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0xfc03 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND` +- `command_discovery_generated`: `success` +- Requests issued for `command_discovery_generated`: `Discover_Commands_Generated(start_command_id=0, max_command_ids=16)` +- Note: `Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0xfc03 (server): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND` + +### Endpoint 11 / 0x0019 / Ota / client / manufacturer 4107 + +- `attribute_discovery`: `success` +- Requests issued for `attribute_discovery`: `Discover_Attribute_Extended(start_attribute_id=0, max_attribute_ids=16)`, `Discover_Attributes(start_attribute_id=0, max_attribute_ids=16)` +- Note: `Cluster 0x0019 on has incorrect direction (got for cluster). Please report this here: https://github.com/zigpy/zigpy/issues/1640` +- Note: `Extended attribute discovery unsupported for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0019 (client): UNSUP_MANUF_GENERAL_COMMAND; falling back to discover_attributes` +- `attribute_reads`: `success` +- `command_discovery_received`: `success` +- Requests issued for `command_discovery_received`: `Discover_Commands_Received(start_command_id=0, max_command_ids=16)` +- Note: `Cluster 0x0019 on has incorrect direction (got for cluster). Please report this here: https://github.com/zigpy/zigpy/issues/1640` +- Note: `Skipping received command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0019 (client): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND` +- `command_discovery_generated`: `success` +- Requests issued for `command_discovery_generated`: `Discover_Commands_Generated(start_command_id=0, max_command_ids=16)` +- Note: `Cluster 0x0019 on has incorrect direction (got for cluster). Please report this here: https://github.com/zigpy/zigpy/issues/1640` +- Note: `Skipping generated command discovery for 00:17:88:01:0b:4b:51:fc endpoint 11 cluster 0x0019 (client): unsupported command discovery response UNSUP_MANUF_GENERAL_COMMAND` + +## Notable Behaviors + +- The scan succeeds even when several clusters reject `Discover_Attribute_Extended`; those targets fall back to `Discover_Attributes` and continue. +- Manufacturer-scoped command discovery on some Philips clusters returns `UNSUP_MANUF_GENERAL_COMMAND`; the scanner logs that and treats it as a supported skip, not a scan failure. +- The OTA client cluster (`0x0019`) replies on the manufacturer-specific path with the wrong direction bit for a client cluster. zigpy logs the incorrect direction, but the transaction still completes successfully. +- Descriptor refresh hits a single delivery retry on the initial Node Descriptor request and then succeeds on the retried send with forced route discovery. + +## Reference + +- Use `device_scan.log` for exact packet payloads, decoded ZCL bodies, and the precise request order. +- Use this markdown file as the human-readable map of what the successful scan covered and what it discovered. + diff --git a/tests/test_appdb.py b/tests/test_appdb.py index dc000c25a..4500c88fa 100644 --- a/tests/test_appdb.py +++ b/tests/test_appdb.py @@ -1517,6 +1517,140 @@ async def test_attribute_cache_null_manufacturer_code_uniqueness(tmp_path): assert row[0] == "Model 2" +async def test_device_scan_rows_persist_and_clear(tmp_path): + db = tmp_path / "test.db" + app = await make_app_with_db(db) + + ieee = t.EUI64.convert("aa:bb:cc:dd:11:22:33:44") + dev = app.add_device(ieee=ieee, nwk=0x1234) + dev.node_desc = make_node_desc(logical_type=zdo_t.LogicalType.Router) + + ep = dev.add_endpoint(1) + ep.status = zigpy.endpoint.Status.ZDO_INIT + ep.profile_id = profiles.zha.PROFILE_ID + ep.device_type = profiles.zha.DeviceType.ON_OFF_SWITCH + ep.add_input_cluster(Basic.cluster_id) + + await app._dblistener._save_device(dev) + + await app._dblistener.upsert_device_scan_progress( + ieee=ieee, + endpoint_id=1, + cluster_type=ClusterType.Server, + cluster_id=Basic.cluster_id, + manufacturer_code_scope=None, + attr_discovery_complete=False, + attr_discovery_next_id=0x0020, + attr_reads_complete=False, + cmd_rx_complete=False, + cmd_rx_next_id=0, + cmd_tx_complete=False, + cmd_tx_next_id=0, + last_started=1.0, + last_finished=None, + last_error_code="transport_failure", + last_error="timeout", + last_success=None, + ) + await app._dblistener.upsert_device_scan_attribute( + ieee=ieee, + endpoint_id=1, + cluster_type=ClusterType.Server, + cluster_id=Basic.cluster_id, + manufacturer_code_scope=None, + attr_id=0x0004, + attribute_name="manufacturer", + datatype=zigpy.zcl.foundation.DataTypeId.string, + access=0x01, + discovered_at=2.0, + read_complete=True, + read_status="success", + value=b"\x06Vendor", + last_read=3.0, + last_error_code=None, + last_error=None, + ) + await app._dblistener.upsert_device_scan_attribute( + ieee=ieee, + endpoint_id=1, + cluster_type=ClusterType.Server, + cluster_id=Basic.cluster_id, + manufacturer_code_scope=0x1234, + attr_id=0x0004, + attribute_name="manufacturer", + datatype=zigpy.zcl.foundation.DataTypeId.string, + access=0x01, + discovered_at=2.5, + read_complete=False, + read_status="transport_failure", + value=None, + last_read=None, + last_error_code="transport_failure", + last_error="radio timeout", + ) + await app._dblistener.upsert_device_scan_command( + ieee=ieee, + endpoint_id=1, + cluster_type=ClusterType.Server, + cluster_id=Basic.cluster_id, + manufacturer_code_scope=None, + direction="received", + command_id=0x00, + command_name="reset_to_factory_defaults", + command_schema="()", + discovered_at=4.0, + ) + + rows = await app._dblistener.get_device_scan_rows(ieee) + + assert len(rows.progress) == 1 + assert rows.progress[0].last_error_code == "transport_failure" + assert rows.progress[0].last_error == "timeout" + + assert len(rows.attributes) == 2 + assert {row.manufacturer_code_scope for row in rows.attributes} == {None, 0x1234} + assert rows.attributes[0].datatype == zigpy.zcl.foundation.DataTypeId.string + assert any(row.value == b"\x06Vendor" for row in rows.attributes) + + assert len(rows.commands) == 1 + assert rows.commands[0].direction == "received" + + await app._dblistener.clear_device_scan_data(ieee) + + rows = await app._dblistener.get_device_scan_rows(ieee) + assert rows.progress == [] + assert rows.attributes == [] + assert rows.commands == [] + + await app.shutdown() + + +async def test_raw_topology_rows_preserve_null_manufacturer_code(tmp_path): + db = tmp_path / "test.db" + app = await make_app_with_db(db) + + ieee = t.EUI64.convert("aa:bb:cc:dd:44:55:66:77") + dev = app.add_device(ieee=ieee, nwk=0x2345) + dev.node_desc = make_node_desc( + logical_type=zdo_t.LogicalType.Router, manufacturer_code=None + ) + + ep = dev.add_endpoint(1) + ep.status = zigpy.endpoint.Status.ZDO_INIT + ep.profile_id = profiles.zha.PROFILE_ID + ep.device_type = profiles.zha.DeviceType.ON_OFF_SWITCH + ep.add_input_cluster(Basic.cluster_id) + + await app._dblistener._save_device(dev) + + topology = await app._dblistener.get_raw_topology_rows(ieee) + + assert topology.node_descriptor is not None + assert topology.node_descriptor.manufacturer_code is None + + await app.shutdown() + + @patch("zigpy.quirks.DEVICE_REGISTRY", new=DeviceRegistry()) async def test_device_signature_ignores_quirks(tmp_path) -> None: """Test that `device.original_signature` is populated before quirks modify the device.""" diff --git a/tests/test_appdb_migration.py b/tests/test_appdb_migration.py index 06934d936..20450bc21 100644 --- a/tests/test_appdb_migration.py +++ b/tests/test_appdb_migration.py @@ -506,6 +506,97 @@ def test_db_version_is_latest_schema_version(): assert max(zigpy.appdb_schemas.SCHEMAS.keys()) == zigpy.appdb.DB_VERSION +async def test_migration_v14_to_v15_adds_device_scan_tables(tmp_path): + db_path = tmp_path / "test_v14_to_v15.db" + + with sqlite3.connect(db_path) as conn: + conn.executescript(zigpy.appdb_schemas.SCHEMAS[14]) + conn.commit() + + app = await make_app_with_db(db_path) + await app.shutdown() + + with sqlite3.connect(db_path) as conn: + cur = conn.cursor() + + cur.execute("PRAGMA user_version") + assert cur.fetchone() == (zigpy.appdb.DB_VERSION,) + + table_columns = {} + + for table in ( + "device_scan_progress_v15", + "device_scan_attributes_v15", + "device_scan_commands_v15", + ): + cur.execute(f"PRAGMA table_info({table})") + table_columns[table] = {row[1] for row in cur.fetchall()} + + cur.execute("PRAGMA table_info(node_descriptors_v15)") + node_descriptor_columns = {row[1]: row for row in cur.fetchall()} + + assert { + "ieee", + "endpoint_id", + "cluster_type", + "cluster_id", + "manufacturer_code_scope", + "attr_discovery_complete", + "attr_discovery_next_id", + "attr_reads_complete", + "cmd_rx_complete", + "cmd_rx_next_id", + "cmd_tx_complete", + "cmd_tx_next_id", + "last_started", + "last_finished", + "last_error_code", + "last_error", + "last_success", + } <= table_columns["device_scan_progress_v15"] + + assert { + "ieee", + "endpoint_id", + "cluster_type", + "cluster_id", + "manufacturer_code_scope", + "attr_id", + "attribute_name", + "datatype", + "access", + "discovered_at", + "read_complete", + "read_status", + "value", + "last_read", + "last_error_code", + "last_error", + } <= table_columns["device_scan_attributes_v15"] + + assert { + "ieee", + "endpoint_id", + "cluster_type", + "cluster_id", + "manufacturer_code_scope", + "direction", + "command_id", + "command_name", + "command_schema", + "discovered_at", + } <= table_columns["device_scan_commands_v15"] + + cur.execute("SELECT name FROM sqlite_master WHERE type='index'") + indexes = {row[0] for row in cur.fetchall()} + + assert "idx_device_scan_progress_v15_ieee" in indexes + assert "idx_device_scan_attributes_v15_ieee" in indexes + assert "idx_device_scan_attributes_v15_pending_reads" in indexes + assert "idx_device_scan_commands_v15_ieee" in indexes + assert node_descriptor_columns["manufacturer_code"][3] == 0 + + async def test_last_seen_migration_v8_to_v9(test_db): test_db_v8 = test_db("simple_v8.sql") @@ -539,9 +630,11 @@ async def test_unknown_manufacturer_code_migration(test_db, caplog): await app.shutdown() # Count rows after migration + attr_cache_table = f"attributes_cache{zigpy.appdb.DB_V}" + with sqlite3.connect(test_db_prod) as conn: cur = conn.cursor() - cur.execute("SELECT COUNT(*) FROM attributes_cache_v14") + cur.execute(f"SELECT COUNT(*) FROM {attr_cache_table}") after_total = cur.fetchone()[0] assert after_total == before_total @@ -553,6 +646,7 @@ async def test_unknown_manufacturer_code_migration(test_db, caplog): ) async def test_manufacturer_code_migration_uses_device_manufacturer_id(test_db): """Test that attributes on manufacturer-specific clusters get the device's manufacturer_id.""" + attr_cache_table = f"attributes_cache{zigpy.appdb.DB_V}" # Simple quirk for Third Reality night light with is_manufacturer_specific=True. # The real device (f4:42:50:c3:96:14:00:00) has cached attrs 2-5 on 0xFC00 and @@ -590,9 +684,9 @@ class AttributeDefs(BaseAttributeDefs): with sqlite3.connect(test_db_path) as conn: cur = conn.cursor() cur.execute( - """ + f""" SELECT manufacturer_code - FROM attributes_cache_v14 + FROM {attr_cache_table} WHERE cluster_id = 0xFC00 AND attr_id = 0x0002 """, ) @@ -604,9 +698,9 @@ class AttributeDefs(BaseAttributeDefs): with sqlite3.connect(test_db_path) as conn: cur = conn.cursor() cur.execute( - """ + f""" SELECT manufacturer_code, status - FROM attributes_cache_v14 + FROM {attr_cache_table} WHERE ieee = ? AND cluster_id = 0xFC00 AND attr_id = 0x0004 """, (third_reality_ieee,), @@ -637,6 +731,7 @@ class AttributeDefs(BaseAttributeDefs): ) async def test_data_migration_ambiguous_attributes(tmp_path): """Test data migration disambiguation when find_attributes returns multiple.""" + attr_cache_table = f"attributes_cache{zigpy.appdb.DB_V}" class DisambiguatedCluster(CustomCluster): cluster_id = 0xFC01 @@ -697,7 +792,7 @@ class AttributeDefs(BaseAttributeDefs): with sqlite3.connect(db_path) as conn: conn.executemany( - "INSERT INTO attributes_cache_v14" + f"INSERT INTO {attr_cache_table}" " (ieee, endpoint_id, cluster_type, cluster_id," " attr_id, manufacturer_code, status, value, last_updated)" " VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)", @@ -736,7 +831,7 @@ class AttributeDefs(BaseAttributeDefs): with sqlite3.connect(db_path) as conn: # The disambiguated unmigrated row was deleted (a row with 0xABCD already existed) rows = conn.execute( - "SELECT manufacturer_code FROM attributes_cache_v14" + f"SELECT manufacturer_code FROM {attr_cache_table}" " WHERE ieee = ? AND cluster_id = ? AND attr_id = ?", (str(dev.ieee), 0xFC01, 0x0010), ).fetchall() @@ -744,7 +839,7 @@ class AttributeDefs(BaseAttributeDefs): # The ambiguous unmigrated row is still present rows = conn.execute( - "SELECT manufacturer_code FROM attributes_cache_v14" + f"SELECT manufacturer_code FROM {attr_cache_table}" " WHERE ieee = ? AND cluster_id = ? AND attr_id = ?", (str(dev.ieee), 0xFC02, 0x0020), ).fetchall() diff --git a/tests/test_application.py b/tests/test_application.py index d7ec8ea68..a48902f02 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -12,6 +12,7 @@ import zigpy.application import zigpy.config as conf from zigpy.datastructures import RequestLimiter +import zigpy.device_scanner from zigpy.exceptions import ( DeliveryError, NetworkNotFormed, @@ -222,6 +223,11 @@ def test_config(app): assert app.config == app._config +def test_device_scanner_service_property(app): + assert isinstance(app.device_scanner, zigpy.device_scanner.DeviceScanner) + assert app.device_scanner._app is app + + def test_deserialize(app, ieee): dev = MagicMock() app.deserialize(dev, 1, 1, b"") diff --git a/tests/test_device.py b/tests/test_device.py index 98df8ba3e..9d09b9842 100644 --- a/tests/test_device.py +++ b/tests/test_device.py @@ -80,6 +80,58 @@ async def mock_ep_get_model_info(self): assert dev._application.device_initialized.call_count == 3 +async def test_initialize_and_scanner_descriptor_refresh_share_raw_walk(app): + dev = app.add_device( + nwk=0x1234, + ieee=t.EUI64.convert("aa:bb:cc:dd:ee:ff:00:11"), + ) + node_desc = make_node_desc(manufacturer_code=0xABCD) + + async def mock_active_ep_req(nwk): + assert nwk == dev.nwk + return [zdo_t.Status.SUCCESS, None, [1, 2]] + + async def mock_simple_desc_req(nwk, endpoint_id): + assert nwk == dev.nwk + + sd = zdo_t.SimpleDescriptor() + sd.endpoint = endpoint_id + + if endpoint_id == 1: + sd.profile = zha.PROFILE_ID + sd.device_type = zha.DeviceType.PUMP + sd.input_clusters = [Basic.cluster_id] + sd.output_clusters = [] + return [zdo_t.Status.SUCCESS, None, sd] + + return [zdo_t.Status.NOT_ACTIVE, None, sd] + + dev.zdo.Node_Desc_req = AsyncMock( + return_value=(zdo_t.Status.SUCCESS, dev.nwk, node_desc) + ) + dev.zdo.Active_EP_req = AsyncMock(side_effect=mock_active_ep_req) + dev.zdo.Simple_Desc_req = AsyncMock(side_effect=mock_simple_desc_req) + + with patch.object( + endpoint.Endpoint, "get_model_info", AsyncMock(return_value=(None, None)) + ): + scan_result = await app.device_scanner._discover_raw_descriptors(dev) + await dev.initialize() + + descriptor_by_endpoint = { + descriptor.endpoint_id: descriptor for descriptor in scan_result.endpoints + } + + assert scan_result.node_descriptor == node_desc + assert descriptor_by_endpoint[1].profile_id == dev.endpoints[1].profile_id + assert descriptor_by_endpoint[1].device_type == dev.endpoints[1].device_type + assert descriptor_by_endpoint[1].input_clusters == tuple( + dev.endpoints[1].in_clusters + ) + assert descriptor_by_endpoint[2].status == endpoint.Status.ENDPOINT_INACTIVE + assert dev.endpoints[2].status == endpoint.Status.ENDPOINT_INACTIVE + + async def test_initialize_read_ota( app: zigpy.application.ControllerApplication, ) -> None: @@ -1899,3 +1951,143 @@ async def test_attribute_report_not_matched_with_request(dev): result = await request_task assert result == default_rsp_cmd + + +async def test_client_cluster_default_response_with_wrong_direction_matches_request( + dev, +): + ep = dev.add_endpoint(1) + ep.add_output_cluster(Ota.cluster_id) + + with patch.object(dev._application, "send_packet") as mock_packet_send: + request_task = asyncio.create_task( + ep.out_clusters[Ota.cluster_id].discover_attributes_extended(0, 16) + ) + + await asyncio.sleep(0) + assert len(mock_packet_send.mock_calls) == 1 + sent_packet = mock_packet_send.mock_calls[0].args[0] + + tsn_hdr, _ = foundation.ZCLHeader.deserialize(sent_packet.data.serialize()) + + default_rsp_hdr = foundation.ZCLHeader( + frame_control=foundation.FrameControl( + frame_type=foundation.FrameType.GLOBAL_COMMAND, + is_manufacturer_specific=False, + direction=foundation.Direction.Server_to_Client, + disable_default_response=True, + reserved=0, + ), + tsn=tsn_hdr.tsn, + command_id=foundation.GeneralCommand.Default_Response, + ) + default_rsp_cmd = foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Default_Response + ].schema( + command_id=foundation.GeneralCommand.Discover_Attribute_Extended, + status=foundation.Status.UNSUP_GENERAL_COMMAND, + ) + + dev.packet_received( + t.ZigbeePacket( + src=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=dev.nwk), + src_ep=1, + dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=0x0000), + dst_ep=1, + profile_id=260, + cluster_id=Ota.cluster_id, + data=t.SerializableBytes( + default_rsp_hdr.serialize() + default_rsp_cmd.serialize() + ), + lqi=255, + rssi=-30, + ) + ) + + result = await asyncio.wait_for(request_task, timeout=0.2) + assert result == default_rsp_cmd + + +async def test_client_cluster_wrong_direction_non_response_does_not_match_request(dev): + ep = dev.add_endpoint(1) + ep.add_output_cluster(Ota.cluster_id) + + with patch.object(dev._application, "send_packet") as mock_packet_send: + request_task = asyncio.create_task( + ep.out_clusters[Ota.cluster_id].discover_attributes_extended(0, 16) + ) + + await asyncio.sleep(0) + assert len(mock_packet_send.mock_calls) == 1 + sent_packet = mock_packet_send.mock_calls[0].args[0] + + tsn_hdr, _ = foundation.ZCLHeader.deserialize(sent_packet.data.serialize()) + + request_hdr = foundation.ZCLHeader( + frame_control=foundation.FrameControl( + frame_type=foundation.FrameType.GLOBAL_COMMAND, + is_manufacturer_specific=False, + direction=foundation.Direction.Server_to_Client, + disable_default_response=True, + reserved=0, + ), + tsn=tsn_hdr.tsn, + command_id=foundation.GeneralCommand.Discover_Attributes, + ) + request_cmd = foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Discover_Attributes + ].schema(start_attribute_id=0, max_attribute_ids=16) + + dev.packet_received( + t.ZigbeePacket( + src=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=dev.nwk), + src_ep=1, + dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=0x0000), + dst_ep=1, + profile_id=260, + cluster_id=Ota.cluster_id, + data=t.SerializableBytes(request_hdr.serialize() + request_cmd.serialize()), + lqi=255, + rssi=-30, + ) + ) + await asyncio.sleep(0) + + assert not request_task.done() + + default_rsp_hdr = foundation.ZCLHeader( + frame_control=foundation.FrameControl( + frame_type=foundation.FrameType.GLOBAL_COMMAND, + is_manufacturer_specific=False, + direction=foundation.Direction.Server_to_Client, + disable_default_response=True, + reserved=0, + ), + tsn=tsn_hdr.tsn, + command_id=foundation.GeneralCommand.Default_Response, + ) + default_rsp_cmd = foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Default_Response + ].schema( + command_id=foundation.GeneralCommand.Discover_Attribute_Extended, + status=foundation.Status.UNSUP_GENERAL_COMMAND, + ) + + dev.packet_received( + t.ZigbeePacket( + src=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=dev.nwk), + src_ep=1, + dst=t.AddrModeAddress(addr_mode=t.AddrMode.NWK, address=0x0000), + dst_ep=1, + profile_id=260, + cluster_id=Ota.cluster_id, + data=t.SerializableBytes( + default_rsp_hdr.serialize() + default_rsp_cmd.serialize() + ), + lqi=255, + rssi=-30, + ) + ) + + result = await asyncio.wait_for(request_task, timeout=0.2) + assert result == default_rsp_cmd diff --git a/tests/test_device_scanner.py b/tests/test_device_scanner.py new file mode 100644 index 000000000..ad464497b --- /dev/null +++ b/tests/test_device_scanner.py @@ -0,0 +1,3911 @@ +from __future__ import annotations + +import asyncio +import dataclasses +from datetime import UTC, datetime +import logging +from pathlib import Path + +import pytest + +from zigpy.appdb import sqlite3 +import zigpy.device_scanner +import zigpy.endpoint +import zigpy.exceptions +from zigpy.profiles import zha +import zigpy.types as t +from zigpy.zcl import ClusterType, foundation +from zigpy.zcl.clusters.general import Basic, OnOff +from zigpy.zdo import types as zdo_t + +from .async_mock import AsyncMock, call, patch +from .conftest import make_node_desc +from .test_appdb import make_app_with_db + + +def test_device_scanner_public_contract_types(): + summary_fields = [ + field.name + for field in dataclasses.fields(zigpy.device_scanner.DeviceScanSummary) + ] + assert summary_fields == [ + "ieee", + "completed", + "outcome", + "used_resume", + "force_full", + "descriptor_refresh_performed", + "last_finished", + "error_code", + "last_error", + ] + + snapshot_fields = [ + field.name + for field in dataclasses.fields(zigpy.device_scanner.DeviceScanSnapshot) + ] + assert snapshot_fields == [ + "ieee", + "raw_node_descriptor", + "last_snapshot_at", + "endpoints", + ] + + event_fields = [ + field.name + for field in dataclasses.fields(zigpy.device_scanner.DeviceScanProgressEvent) + ] + assert event_fields == [ + "ieee", + "status", + "outcome", + "step", + "endpoint_id", + "cluster_id", + "cluster_type", + "manufacturer_code_scope", + "error_code", + "error", + ] + + +def test_device_scanner_public_error_types(): + for error_type in ( + zigpy.device_scanner.InvalidScanOptionsError, + zigpy.device_scanner.ScanInProgressError, + zigpy.device_scanner.DeviceScanTargetMissingError, + zigpy.device_scanner.DeviceScanSnapshotNotFoundError, + ): + assert issubclass(error_type, Exception) + + +def test_device_scanner_vocabulary_is_fixed(): + assert zigpy.device_scanner.SCAN_EVENT_NAMES == ( + "scan_queued", + "scan_started", + "step_started", + "step_finished", + "scan_finished", + ) + assert zigpy.device_scanner.SCAN_STATUSES == ( + "queued", + "started", + "success", + "failed", + "skipped", + ) + assert zigpy.device_scanner.SCAN_OUTCOMES == ( + "success", + "partial", + "failed", + ) + assert zigpy.device_scanner.SCAN_ERROR_CODES == ( + "invalid_scan_options", + "scan_in_progress", + "device_scan_target_missing", + "descriptor_refresh_failed", + "scan_deadline_exceeded", + "unsupported_discovery_command", + "transport_failure", + "attribute_unsupported", + "missing_raw_manufacturer_code", + ) + + +async def test_device_scanner_invalid_scan_options(app): + ieee = t.EUI64.convert("aa:bb:cc:dd:11:22:33:44") + + with pytest.raises(zigpy.device_scanner.InvalidScanOptionsError): + await app.device_scanner.scan(ieee, resume=True, force_full=True) + + +def test_device_scanner_progress_events_emit_on_scanner(app): + ieee = t.EUI64.convert("aa:bb:cc:dd:11:22:33:44") + events = [] + + class Listener: + def scan_finished(self, event): + events.append(event) + + app.device_scanner.add_listener(Listener()) + + event = app.device_scanner._emit_progress( + "scan_finished", + ieee=ieee, + status="failed", + outcome="failed", + error_code="scan_deadline_exceeded", + error="deadline exceeded", + ) + + assert isinstance(event, zigpy.device_scanner.DeviceScanProgressEvent) + assert events == [event] + + +async def _make_basic_scan_target(tmp_path: Path): + app = await make_app_with_db(tmp_path / "test.db") + dev = app.add_device( + nwk=0x1234, + ieee=t.EUI64.convert("aa:bb:cc:dd:ee:ff:00:11"), + ) + dev.node_desc = make_node_desc() + + ep1 = dev.add_endpoint(1) + ep1.status = zigpy.endpoint.Status.ZDO_INIT + ep1.profile_id = zha.PROFILE_ID + ep1.device_type = zha.DeviceType.PUMP + ep1.add_input_cluster(Basic.cluster_id) + + await app._dblistener._save_device(dev) + + target = next( + target + for target in await app.device_scanner._build_raw_scan_targets(dev) + if target.endpoint_id == 1 + and target.cluster.cluster_type == ClusterType.Server + and target.cluster.cluster_id == Basic.cluster_id + ) + return app, dev, target + + +async def _seed_scan_progress(app, target): + await app._dblistener.upsert_device_scan_progress( + ieee=target.endpoint.device.ieee, + endpoint_id=target.endpoint_id, + cluster_type=target.cluster.cluster_type, + cluster_id=target.cluster.cluster_id, + manufacturer_code_scope=target.scope.manufacturer_code_scope, + attr_discovery_complete=False, + attr_discovery_next_id=0, + attr_reads_complete=False, + cmd_rx_complete=False, + cmd_rx_next_id=0, + cmd_tx_complete=False, + cmd_tx_next_id=0, + last_started=None, + last_finished=None, + last_error_code=None, + last_error=None, + last_success=None, + ) + + +async def _seed_discovered_attribute( + app, + target, + *, + attr_id: int, + attribute_name: str | None, + datatype: int, + access: int | None, + read_complete: bool = False, + read_status: str | None = None, + value: bytes | None = None, + last_error_code: str | None = None, +): + await app._dblistener.upsert_device_scan_attribute( + ieee=target.endpoint.device.ieee, + endpoint_id=target.endpoint_id, + cluster_type=target.cluster.cluster_type, + cluster_id=target.cluster.cluster_id, + manufacturer_code_scope=target.scope.manufacturer_code_scope, + attr_id=attr_id, + attribute_name=attribute_name, + datatype=datatype, + access=access, + discovered_at=1.0, + read_complete=read_complete, + read_status=read_status, + value=value, + last_read=None, + last_error_code=last_error_code, + last_error=None, + ) + + +async def _make_onoff_scan_targets(tmp_path: Path): + app = await make_app_with_db(tmp_path / "test.db") + dev = app.add_device( + nwk=0x1234, + ieee=t.EUI64.convert("aa:bb:cc:dd:ee:ff:00:22"), + ) + dev.node_desc = make_node_desc() + + ep1 = dev.add_endpoint(1) + ep1.status = zigpy.endpoint.Status.ZDO_INIT + ep1.profile_id = zha.PROFILE_ID + ep1.device_type = zha.DeviceType.PUMP + ep1.add_input_cluster(OnOff.cluster_id) + ep1.add_output_cluster(OnOff.cluster_id) + + await app._dblistener._save_device(dev) + + targets = await app.device_scanner._build_raw_scan_targets(dev) + server_target = next( + target + for target in targets + if target.cluster.cluster_type == ClusterType.Server + and target.cluster.cluster_id == OnOff.cluster_id + ) + client_target = next( + target + for target in targets + if target.cluster.cluster_type == ClusterType.Client + and target.cluster.cluster_id == OnOff.cluster_id + ) + return app, dev, server_target, client_target + + +def _make_scan_summary( + ieee: t.EUI64, + *, + outcome: str = "success", + error_code: str | None = None, + last_error: str | None = None, +) -> zigpy.device_scanner.DeviceScanSummary: + return zigpy.device_scanner.DeviceScanSummary( + ieee=ieee, + completed=True, + outcome=outcome, + used_resume=True, + force_full=False, + descriptor_refresh_performed=True, + last_finished=None, + error_code=error_code, + last_error=last_error, + ) + + +async def test_device_scanner_descriptor_refresh_does_not_mutate_live_runtime_dictionaries( + app, +): + dev = app.add_device( + nwk=0x1234, + ieee=t.EUI64.convert("aa:bb:cc:dd:ee:ff:00:11"), + ) + dev.node_desc = make_node_desc() + + live_ep = dev.add_endpoint(1) + live_ep.status = zigpy.endpoint.Status.ZDO_INIT + live_ep.profile_id = zha.PROFILE_ID + live_ep.device_type = zha.DeviceType.PUMP + live_ep.add_input_cluster(Basic.cluster_id) + + original_endpoints = set(dev.endpoints) + original_input_clusters = dict(live_ep.in_clusters) + + async def mock_active_ep_req(nwk): + assert nwk == dev.nwk + return [zdo_t.Status.SUCCESS, None, [1, 2]] + + async def mock_simple_desc_req(nwk, endpoint_id): + assert nwk == dev.nwk + + sd = zdo_t.SimpleDescriptor() + sd.endpoint = endpoint_id + sd.profile = zha.PROFILE_ID + sd.device_type = zha.DeviceType.PUMP + + if endpoint_id == 1: + sd.input_clusters = [Basic.cluster_id] + else: + sd.input_clusters = [OnOff.cluster_id] + + sd.output_clusters = [] + return [zdo_t.Status.SUCCESS, None, sd] + + dev.zdo.Node_Desc_req = AsyncMock( + return_value=(zdo_t.Status.SUCCESS, dev.nwk, dev.node_desc) + ) + dev.zdo.Active_EP_req = AsyncMock(side_effect=mock_active_ep_req) + dev.zdo.Simple_Desc_req = AsyncMock(side_effect=mock_simple_desc_req) + + result = await app.device_scanner._discover_raw_descriptors(dev) + + assert set(dev.endpoints) == original_endpoints + assert 2 not in dev.endpoints + assert dict(live_ep.in_clusters) == original_input_clusters + assert {descriptor.endpoint_id for descriptor in result.endpoints} == {1, 2} + + +async def test_device_scanner_refresh_replaces_raw_rows_and_cleans_removed_scan_scopes( + tmp_path: Path, +): + app = await make_app_with_db(tmp_path / "test.db") + dev = app.add_device( + nwk=0x1234, + ieee=t.EUI64.convert("aa:bb:cc:dd:ee:ff:00:11"), + ) + dev.node_desc = make_node_desc(manufacturer_code=0x1111) + + ep1 = dev.add_endpoint(1) + ep1.status = zigpy.endpoint.Status.ZDO_INIT + ep1.profile_id = zha.PROFILE_ID + ep1.device_type = zha.DeviceType.PUMP + ep1.add_input_cluster(Basic.cluster_id) + ep1.add_input_cluster(OnOff.cluster_id) + + ep2 = dev.add_endpoint(2) + ep2.status = zigpy.endpoint.Status.ZDO_INIT + ep2.profile_id = zha.PROFILE_ID + ep2.device_type = zha.DeviceType.PUMP + ep2.add_input_cluster(Basic.cluster_id) + + await app._dblistener._save_device(dev) + await app._dblistener.upsert_device_scan_progress( + ieee=dev.ieee, + endpoint_id=1, + cluster_type=ClusterType.Server, + cluster_id=OnOff.cluster_id, + manufacturer_code_scope=None, + attr_discovery_complete=False, + attr_discovery_next_id=0, + attr_reads_complete=False, + cmd_rx_complete=False, + cmd_rx_next_id=0, + cmd_tx_complete=False, + cmd_tx_next_id=0, + last_started=1.0, + last_finished=None, + last_error_code=None, + last_error=None, + last_success=None, + ) + await app._dblistener.upsert_device_scan_progress( + ieee=dev.ieee, + endpoint_id=2, + cluster_type=ClusterType.Server, + cluster_id=Basic.cluster_id, + manufacturer_code_scope=None, + attr_discovery_complete=False, + attr_discovery_next_id=0, + attr_reads_complete=False, + cmd_rx_complete=False, + cmd_rx_next_id=0, + cmd_tx_complete=False, + cmd_tx_next_id=0, + last_started=1.0, + last_finished=None, + last_error_code=None, + last_error=None, + last_success=None, + ) + + events = [] + + class Listener: + def step_started(self, event): + events.append(("step_started", event)) + + def step_finished(self, event): + events.append(("step_finished", event)) + + app.device_scanner.add_listener(Listener()) + + refreshed_node_desc = make_node_desc(manufacturer_code=0x2222) + + async def mock_active_ep_req(nwk): + assert nwk == dev.nwk + return [zdo_t.Status.SUCCESS, None, [1]] + + async def mock_simple_desc_req(nwk, endpoint_id): + assert nwk == dev.nwk + assert endpoint_id == 1 + + sd = zdo_t.SimpleDescriptor() + sd.endpoint = endpoint_id + sd.profile = zha.PROFILE_ID + sd.device_type = zha.DeviceType.PUMP + sd.input_clusters = [Basic.cluster_id] + sd.output_clusters = [OnOff.cluster_id] + return [zdo_t.Status.SUCCESS, None, sd] + + dev.zdo.Node_Desc_req = AsyncMock( + return_value=(zdo_t.Status.SUCCESS, dev.nwk, refreshed_node_desc) + ) + dev.zdo.Active_EP_req = AsyncMock(side_effect=mock_active_ep_req) + dev.zdo.Simple_Desc_req = AsyncMock(side_effect=mock_simple_desc_req) + + await app.device_scanner._refresh_raw_descriptors(dev) + + with sqlite3.connect(tmp_path / "test.db") as conn: + endpoint_rows = conn.execute( + "SELECT endpoint_id, profile_id, device_type, status" + " FROM endpoints_v15 WHERE ieee = ? ORDER BY endpoint_id", + (str(dev.ieee),), + ).fetchall() + cluster_rows = conn.execute( + "SELECT endpoint_id, cluster_type, cluster_id" + " FROM clusters_v15 WHERE ieee = ? ORDER BY endpoint_id, cluster_type, cluster_id", + (str(dev.ieee),), + ).fetchall() + node_desc_row = conn.execute( + "SELECT manufacturer_code FROM node_descriptors_v15 WHERE ieee = ?", + (str(dev.ieee),), + ).fetchone() + + assert endpoint_rows == [ + (1, zha.PROFILE_ID, zha.DeviceType.PUMP, zigpy.endpoint.Status.ZDO_INIT) + ] + assert cluster_rows == [ + (1, ClusterType.Server, Basic.cluster_id), + (1, ClusterType.Client, OnOff.cluster_id), + ] + assert node_desc_row == (0x2222,) + + rows = await app._dblistener.get_device_scan_rows(dev.ieee) + assert rows.progress == [] + + assert [name for name, _ in events] == ["step_started", "step_finished"] + assert events[0][1].step == "descriptor_refresh" + assert events[0][1].endpoint_id is None + assert events[1][1].status == "success" + assert events[1][1].step == "descriptor_refresh" + + await app.shutdown() + + +async def test_device_scanner_refresh_preserves_group_memberships_for_kept_endpoints( + tmp_path: Path, +): + db_path = tmp_path / "test.db" + app = await make_app_with_db(db_path) + dev = app.add_device( + nwk=0x1234, + ieee=t.EUI64.convert("aa:bb:cc:dd:ee:ff:00:12"), + ) + dev.node_desc = make_node_desc(manufacturer_code=0x1111) + + ep1 = dev.add_endpoint(1) + ep1.status = zigpy.endpoint.Status.ZDO_INIT + ep1.profile_id = zha.PROFILE_ID + ep1.device_type = zha.DeviceType.PUMP + ep1.add_input_cluster(Basic.cluster_id) + + await app._dblistener._save_device(dev) + + with sqlite3.connect(db_path) as conn: + conn.execute( + "INSERT INTO groups_v15 (group_id, name) VALUES (?, ?)", + (0x1234, "kitchen"), + ) + conn.execute( + "INSERT INTO group_members_v15 (group_id, ieee, endpoint_id)" + " VALUES (?, ?, ?)", + (0x1234, str(dev.ieee), 1), + ) + conn.commit() + + async def mock_active_ep_req(nwk): + assert nwk == dev.nwk + return [zdo_t.Status.SUCCESS, None, [1]] + + async def mock_simple_desc_req(nwk, endpoint_id): + assert nwk == dev.nwk + assert endpoint_id == 1 + + sd = zdo_t.SimpleDescriptor() + sd.endpoint = endpoint_id + sd.profile = zha.PROFILE_ID + sd.device_type = zha.DeviceType.PUMP + sd.input_clusters = [Basic.cluster_id] + sd.output_clusters = [] + return [zdo_t.Status.SUCCESS, None, sd] + + dev.zdo.Node_Desc_req = AsyncMock( + return_value=(zdo_t.Status.SUCCESS, dev.nwk, dev.node_desc) + ) + dev.zdo.Active_EP_req = AsyncMock(side_effect=mock_active_ep_req) + dev.zdo.Simple_Desc_req = AsyncMock(side_effect=mock_simple_desc_req) + + await app.device_scanner._refresh_raw_descriptors(dev) + + with sqlite3.connect(db_path) as conn: + group_members = conn.execute( + "SELECT group_id, ieee, endpoint_id FROM group_members_v15 WHERE ieee = ?", + (str(dev.ieee),), + ).fetchall() + + assert group_members == [(0x1234, str(dev.ieee), 1)] + + await app.shutdown() + + +async def test_device_scanner_refresh_failure_keeps_prior_raw_rows_and_emits_failed_step( + tmp_path: Path, +): + app = await make_app_with_db(tmp_path / "test.db") + dev = app.add_device( + nwk=0x1234, + ieee=t.EUI64.convert("aa:bb:cc:dd:ee:ff:00:11"), + ) + dev.node_desc = make_node_desc(manufacturer_code=0x1111) + + ep1 = dev.add_endpoint(1) + ep1.status = zigpy.endpoint.Status.ZDO_INIT + ep1.profile_id = zha.PROFILE_ID + ep1.device_type = zha.DeviceType.PUMP + ep1.add_input_cluster(Basic.cluster_id) + + await app._dblistener._save_device(dev) + + events = [] + + class Listener: + def step_started(self, event): + events.append(("step_started", event)) + + def step_finished(self, event): + events.append(("step_finished", event)) + + app.device_scanner.add_listener(Listener()) + + dev.zdo.Node_Desc_req = AsyncMock( + return_value=( + zdo_t.Status.SUCCESS, + dev.nwk, + make_node_desc(manufacturer_code=0x2222), + ) + ) + dev.zdo.Active_EP_req = AsyncMock(return_value=(zdo_t.Status.NOT_ACTIVE, None, [])) + + with pytest.raises(zigpy.exceptions.InvalidResponse): + await app.device_scanner._refresh_raw_descriptors(dev) + + with sqlite3.connect(tmp_path / "test.db") as conn: + endpoint_rows = conn.execute( + "SELECT endpoint_id FROM endpoints_v15 WHERE ieee = ? ORDER BY endpoint_id", + (str(dev.ieee),), + ).fetchall() + node_desc_row = conn.execute( + "SELECT manufacturer_code FROM node_descriptors_v15 WHERE ieee = ?", + (str(dev.ieee),), + ).fetchone() + + assert endpoint_rows == [(1,)] + assert node_desc_row == (0x1111,) + assert [name for name, _ in events] == ["step_started", "step_finished"] + assert events[1][1].status == "failed" + assert events[1][1].error_code == "descriptor_refresh_failed" + + await app.shutdown() + + +async def test_device_scanner_descriptor_refresh_applies_request_pacing(app): + dev = app.add_device( + nwk=0x1234, + ieee=t.EUI64.convert("aa:bb:cc:dd:ee:ff:00:11"), + ) + + async def mock_active_ep_req(nwk): + return [zdo_t.Status.SUCCESS, None, [1]] + + async def mock_simple_desc_req(nwk, endpoint_id): + sd = zdo_t.SimpleDescriptor() + sd.endpoint = endpoint_id + sd.profile = zha.PROFILE_ID + sd.device_type = zha.DeviceType.PUMP + sd.input_clusters = [Basic.cluster_id] + sd.output_clusters = [] + return [zdo_t.Status.SUCCESS, None, sd] + + dev.zdo.Node_Desc_req = AsyncMock( + return_value=(zdo_t.Status.SUCCESS, dev.nwk, make_node_desc()) + ) + dev.zdo.Active_EP_req = AsyncMock(side_effect=mock_active_ep_req) + dev.zdo.Simple_Desc_req = AsyncMock(side_effect=mock_simple_desc_req) + + with patch("zigpy.device_scanner.asyncio.sleep", new=AsyncMock()) as sleep_mock: + await app.device_scanner._discover_raw_descriptors(dev) + + assert sleep_mock.await_count == 3 + + +async def test_device_scanner_builds_targets_from_raw_db_rows_with_output_cluster_role( + tmp_path: Path, +): + app = await make_app_with_db(tmp_path / "test.db") + dev = app.add_device( + nwk=0x1234, + ieee=t.EUI64.convert("aa:bb:cc:dd:ee:ff:00:11"), + ) + dev.node_desc = make_node_desc() + + raw_ep = dev.add_endpoint(1) + raw_ep.status = zigpy.endpoint.Status.ZDO_INIT + raw_ep.profile_id = zha.PROFILE_ID + raw_ep.device_type = zha.DeviceType.PUMP + raw_ep.add_input_cluster(Basic.cluster_id) + raw_ep.add_output_cluster(OnOff.cluster_id) + + await app._dblistener._save_device(dev) + + quirk_ep = dev.add_endpoint(99) + quirk_ep.status = zigpy.endpoint.Status.ZDO_INIT + quirk_ep.profile_id = zha.PROFILE_ID + quirk_ep.device_type = zha.DeviceType.PUMP + quirk_ep.add_input_cluster(0xFC00) + + targets = await app.device_scanner._build_raw_scan_targets(dev) + + assert { + (target.endpoint_id, target.cluster.cluster_type, target.cluster.cluster_id) + for target in targets + } == { + (1, ClusterType.Server, Basic.cluster_id), + (1, ClusterType.Client, OnOff.cluster_id), + } + onoff_target = next( + target for target in targets if target.cluster.cluster_id == OnOff.cluster_id + ) + assert onoff_target.cluster.is_client is True + assert onoff_target.cluster.is_server is False + assert onoff_target.endpoint.endpoint_id == 1 + assert onoff_target.endpoint is not dev.endpoints[1] + assert 99 not in {target.endpoint_id for target in targets} + + await app.shutdown() + + +async def test_device_scanner_build_raw_scan_targets_skips_endpoint_242( + tmp_path: Path, +): + app = await make_app_with_db(tmp_path / "test.db") + dev = app.add_device( + nwk=0x1234, + ieee=t.EUI64.convert("aa:bb:cc:dd:ee:ff:00:33"), + ) + dev.node_desc = make_node_desc() + + ep1 = dev.add_endpoint(1) + ep1.status = zigpy.endpoint.Status.ZDO_INIT + ep1.profile_id = zha.PROFILE_ID + ep1.device_type = zha.DeviceType.PUMP + ep1.add_input_cluster(Basic.cluster_id) + + ep242 = dev.add_endpoint(242) + ep242.status = zigpy.endpoint.Status.ZDO_INIT + ep242.profile_id = 0xA1E0 + ep242.device_type = 0x0061 + ep242.add_output_cluster(0x0021) + + await app._dblistener._save_device(dev) + + targets = await app.device_scanner._build_raw_scan_targets(dev) + + assert { + (target.endpoint_id, target.cluster.cluster_type, target.cluster.cluster_id) + for target in targets + } == { + (1, ClusterType.Server, Basic.cluster_id), + } + + await app.shutdown() + + +async def test_device_scanner_attribute_discovery_persists_pages_and_progress( + tmp_path: Path, +): + app, dev, target = await _make_basic_scan_target(tmp_path) + + discover_rsp = foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Discover_Attribute_Extended_rsp + ].schema + target.cluster.discover_attributes_extended = AsyncMock( + side_effect=[ + discover_rsp( + discovery_complete=False, + extended_attr_info=[ + foundation.DiscoverAttributesExtendedResponseRecord( + attrid=0x0000, + datatype=foundation.DataTypeId.uint8, + acl=foundation.AttributeAccessControl.READ, + ), + foundation.DiscoverAttributesExtendedResponseRecord( + attrid=0x0001, + datatype=foundation.DataTypeId.uint8, + acl=foundation.AttributeAccessControl.READ + | foundation.AttributeAccessControl.WRITE, + ), + ], + ), + discover_rsp( + discovery_complete=True, + extended_attr_info=[ + foundation.DiscoverAttributesExtendedResponseRecord( + attrid=0x0002, + datatype=foundation.DataTypeId.uint8, + acl=foundation.AttributeAccessControl.READ, + ) + ], + ), + ] + ) + + await app.device_scanner._discover_attributes_for_target(target) + + rows = await app._dblistener.get_device_scan_rows(dev.ieee) + assert [row.attr_id for row in rows.attributes] == [0x0000, 0x0001, 0x0002] + assert [row.datatype for row in rows.attributes] == [ + foundation.DataTypeId.uint8, + foundation.DataTypeId.uint8, + foundation.DataTypeId.uint8, + ] + assert [row.access for row in rows.attributes] == [ + foundation.AttributeAccessControl.READ, + foundation.AttributeAccessControl.READ + | foundation.AttributeAccessControl.WRITE, + foundation.AttributeAccessControl.READ, + ] + assert all(row.read_complete is False for row in rows.attributes) + assert all(row.read_status is None for row in rows.attributes) + assert all(row.last_error_code is None for row in rows.attributes) + assert all(row.value is None for row in rows.attributes) + + assert len(rows.progress) == 1 + progress = rows.progress[0] + assert progress.endpoint_id == 1 + assert progress.cluster_type == ClusterType.Server + assert progress.cluster_id == Basic.cluster_id + assert progress.attr_discovery_complete is True + assert progress.attr_discovery_next_id == 3 + + assert target.cluster.discover_attributes_extended.await_args_list[0].args == ( + 0, + 16, + ) + assert target.cluster.discover_attributes_extended.await_args_list[0].kwargs == { + "manufacturer": None + } + assert target.cluster.discover_attributes_extended.await_args_list[1].args == ( + 2, + 16, + ) + assert target.cluster.discover_attributes_extended.await_args_list[1].kwargs == { + "manufacturer": None + } + + await app.shutdown() + + +async def test_device_scanner_attribute_discovery_falls_back_to_standard_on_unsupported_extended_response( + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +): + app, dev, target = await _make_basic_scan_target(tmp_path) + discover_rsp = foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Discover_Attributes_rsp + ].schema + target.cluster.discover_attributes_extended = AsyncMock( + return_value=( + foundation.GeneralCommand.Default_Response, + foundation.Status.UNSUP_GENERAL_COMMAND, + ) + ) + target.cluster.discover_attributes = AsyncMock( + return_value=discover_rsp( + discovery_complete=True, + attribute_info=[ + foundation.DiscoverAttributesResponseRecord( + attrid=0x0000, + datatype=foundation.DataTypeId.uint8, + ) + ], + ) + ) + caplog.set_level(logging.WARNING, logger="zigpy.device_scanner") + + await app.device_scanner._discover_attributes_for_target(target) + + rows = await app._dblistener.get_device_scan_rows(dev.ieee) + assert [row.attr_id for row in rows.attributes] == [0x0000] + assert [row.access for row in rows.attributes] == [None] + assert rows.progress[0].attr_discovery_complete is True + target.cluster.discover_attributes_extended.assert_awaited_once() + target.cluster.discover_attributes.assert_awaited_once() + assert "falling back to discover_attributes" in caplog.text.lower() + + await app.shutdown() + + +async def test_device_scanner_attribute_discovery_continues_when_both_discovery_commands_are_unsupported( + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +): + app, dev, target = await _make_basic_scan_target(tmp_path) + unsupported_response = ( + foundation.GeneralCommand.Default_Response, + foundation.Status.UNSUP_GENERAL_COMMAND, + ) + target.cluster.discover_attributes_extended = AsyncMock( + return_value=unsupported_response + ) + target.cluster.discover_attributes = AsyncMock(return_value=unsupported_response) + caplog.set_level(logging.WARNING, logger="zigpy.device_scanner") + + await app.device_scanner._discover_attributes_for_target(target) + + rows = await app._dblistener.get_device_scan_rows(dev.ieee) + assert rows.attributes == [] + assert rows.progress[0].attr_discovery_complete is True + assert rows.progress[0].attr_discovery_next_id == 0 + assert rows.progress[0].last_error_code is None + assert "extended attribute discovery unsupported" in caplog.text.lower() + assert "standard attribute discovery unsupported" in caplog.text.lower() + + await app.shutdown() + + +async def test_device_scanner_attribute_discovery_continues_when_both_default_response_objects_are_unsupported( + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +): + app, dev, target = await _make_basic_scan_target(tmp_path) + target.cluster.discover_attributes_extended = AsyncMock( + return_value=foundation.DefaultResponse( + command_id=foundation.GeneralCommand.Discover_Attribute_Extended, + status=foundation.Status.UNSUP_GENERAL_COMMAND, + ) + ) + target.cluster.discover_attributes = AsyncMock( + return_value=foundation.DefaultResponse( + command_id=foundation.GeneralCommand.Discover_Attributes, + status=foundation.Status.UNSUP_GENERAL_COMMAND, + ) + ) + caplog.set_level(logging.WARNING, logger="zigpy.device_scanner") + + await app.device_scanner._discover_attributes_for_target(target) + + rows = await app._dblistener.get_device_scan_rows(dev.ieee) + assert rows.attributes == [] + assert rows.progress[0].attr_discovery_complete is True + assert rows.progress[0].attr_discovery_next_id == 0 + assert rows.progress[0].last_error_code is None + assert "extended attribute discovery unsupported" in caplog.text.lower() + assert "standard attribute discovery unsupported" in caplog.text.lower() + + await app.shutdown() + + +async def test_device_scanner_manufacturer_attribute_discovery_continues_when_both_default_response_objects_are_manufacturer_unsupported( + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +): + app, dev, _ = await _make_basic_scan_target(tmp_path) + targets = await app.device_scanner._build_scan_targets_for_node_descriptor( + dev, dev.node_desc + ) + manufacturer_target = next( + target + for target in targets + if target.cluster.cluster_id == Basic.cluster_id + and target.scope.manufacturer_code_scope == dev.node_desc.manufacturer_code + ) + + manufacturer_target.cluster.discover_attributes_extended = AsyncMock( + return_value=foundation.DefaultResponse( + command_id=foundation.GeneralCommand.Discover_Attribute_Extended, + status=foundation.Status.UNSUP_MANUF_GENERAL_COMMAND, + ) + ) + manufacturer_target.cluster.discover_attributes = AsyncMock( + return_value=foundation.DefaultResponse( + command_id=foundation.GeneralCommand.Discover_Attributes, + status=foundation.Status.UNSUP_MANUF_GENERAL_COMMAND, + ) + ) + caplog.set_level(logging.WARNING, logger="zigpy.device_scanner") + + await app.device_scanner._discover_attributes_for_target(manufacturer_target) + + rows = await app._dblistener.get_device_scan_rows(dev.ieee) + assert rows.attributes == [] + progress = next( + row + for row in rows.progress + if row.endpoint_id == manufacturer_target.endpoint_id + and row.cluster_type == manufacturer_target.cluster.cluster_type + and row.cluster_id == manufacturer_target.cluster.cluster_id + and row.manufacturer_code_scope == dev.node_desc.manufacturer_code + ) + assert progress.attr_discovery_complete is True + assert progress.attr_discovery_next_id == 0 + assert progress.last_error_code is None + assert "extended attribute discovery unsupported" in caplog.text.lower() + assert "standard attribute discovery unsupported" in caplog.text.lower() + + await app.shutdown() + + +async def test_device_scanner_attribute_discovery_non_unsupported_default_response_is_terminal( + tmp_path: Path, +): + app, _, target = await _make_basic_scan_target(tmp_path) + target.cluster.discover_attributes_extended = AsyncMock( + return_value=foundation.DefaultResponse( + command_id=foundation.GeneralCommand.Discover_Attribute_Extended, + status=foundation.Status.FAILURE, + ) + ) + + with pytest.raises(zigpy.device_scanner._TerminalStepFailure) as exc_info: + await app.device_scanner._discover_attributes_for_target(target) + + assert ( + exc_info.value.error_code + == zigpy.device_scanner.ERROR_CODE_UNSUPPORTED_DISCOVERY_COMMAND + ) + assert "failure" in str(exc_info.value).lower() + + await app.shutdown() + + +async def test_device_scanner_attribute_reads_use_raw_reads_and_preserve_live_cache( + tmp_path: Path, +): + app, dev, target = await _make_basic_scan_target(tmp_path) + await _seed_scan_progress(app, target) + await app._dblistener.upsert_device_scan_progress( + ieee=dev.ieee, + endpoint_id=1, + cluster_type=ClusterType.Server, + cluster_id=Basic.cluster_id, + manufacturer_code_scope=None, + attr_discovery_complete=True, + attr_discovery_next_id=2, + attr_reads_complete=False, + cmd_rx_complete=False, + cmd_rx_next_id=0, + cmd_tx_complete=False, + cmd_tx_next_id=0, + last_started=None, + last_finished=None, + last_error_code=None, + last_error=None, + last_success=None, + ) + await _seed_discovered_attribute( + app, + target, + attr_id=Basic.AttributeDefs.zcl_version.id, + attribute_name=Basic.AttributeDefs.zcl_version.name, + datatype=foundation.DataTypeId.uint8, + access=foundation.AttributeAccessControl.READ, + ) + await _seed_discovered_attribute( + app, + target, + attr_id=Basic.AttributeDefs.app_version.id, + attribute_name=Basic.AttributeDefs.app_version.name, + datatype=foundation.DataTypeId.uint8, + access=foundation.AttributeAccessControl.READ, + ) + + live_cluster = dev.endpoints[1].in_clusters[Basic.cluster_id] + live_cluster._update_attribute(Basic.AttributeDefs.zcl_version.id, 9) + + read_rsp = foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Read_Attributes_rsp + ].schema + + async def read_attributes_raw( + attributes, + *args, + manufacturer=None, + **kwargs, + ): + assert manufacturer is None + assert list(attributes) == [ + Basic.AttributeDefs.zcl_version.id, + Basic.AttributeDefs.app_version.id, + ] + return read_rsp( + status_records=[ + foundation.ReadAttributeRecord( + attrid=Basic.AttributeDefs.zcl_version.id, + status=foundation.Status.SUCCESS, + value=foundation.TypeValue( + type=foundation.DataTypeId.uint8, + value=t.uint8_t(3), + ), + ), + foundation.ReadAttributeRecord( + attrid=Basic.AttributeDefs.app_version.id, + status=foundation.Status.UNSUPPORTED_ATTRIBUTE, + ), + ] + ) + + target.cluster.read_attributes_raw = AsyncMock(side_effect=read_attributes_raw) + await app.device_scanner._read_attributes_for_target(target) + + rows = await app._dblistener.get_device_scan_rows(dev.ieee) + attrs = {row.attr_id: row for row in rows.attributes} + assert attrs[Basic.AttributeDefs.zcl_version.id].read_complete is True + assert attrs[Basic.AttributeDefs.zcl_version.id].read_status == "success" + assert attrs[Basic.AttributeDefs.zcl_version.id].last_error_code is None + assert attrs[Basic.AttributeDefs.zcl_version.id].value == b"\x03" + assert attrs[Basic.AttributeDefs.app_version.id].read_complete is True + assert ( + attrs[Basic.AttributeDefs.app_version.id].read_status == "unsupported_attribute" + ) + assert ( + attrs[Basic.AttributeDefs.app_version.id].last_error_code + == "attribute_unsupported" + ) + assert attrs[Basic.AttributeDefs.app_version.id].value is None + assert rows.progress[0].attr_reads_complete is True + assert target.cluster.read_attributes_raw.await_args.kwargs["manufacturer"] is None + assert live_cluster._attr_cache.get_value(Basic.AttributeDefs.zcl_version) == 9 + + await app.shutdown() + + +async def test_device_scanner_attribute_reads_include_unknown_access_rows( + tmp_path: Path, +): + app, dev, target = await _make_basic_scan_target(tmp_path) + await _seed_scan_progress(app, target) + await app._dblistener.upsert_device_scan_progress( + ieee=dev.ieee, + endpoint_id=1, + cluster_type=ClusterType.Server, + cluster_id=Basic.cluster_id, + manufacturer_code_scope=None, + attr_discovery_complete=True, + attr_discovery_next_id=1, + attr_reads_complete=False, + cmd_rx_complete=False, + cmd_rx_next_id=0, + cmd_tx_complete=False, + cmd_tx_next_id=0, + last_started=None, + last_finished=None, + last_error_code=None, + last_error=None, + last_success=None, + ) + await _seed_discovered_attribute( + app, + target, + attr_id=Basic.AttributeDefs.zcl_version.id, + attribute_name=Basic.AttributeDefs.zcl_version.name, + datatype=foundation.DataTypeId.uint8, + access=None, + ) + + read_rsp = foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Read_Attributes_rsp + ].schema + target.cluster.read_attributes_raw = AsyncMock( + return_value=read_rsp( + status_records=[ + foundation.ReadAttributeRecord( + attrid=Basic.AttributeDefs.zcl_version.id, + status=foundation.Status.SUCCESS, + value=foundation.TypeValue( + type=foundation.DataTypeId.uint8, + value=t.uint8_t(3), + ), + ) + ] + ) + ) + + await app.device_scanner._read_attributes_for_target(target) + + target.cluster.read_attributes_raw.assert_awaited_once() + rows = await app._dblistener.get_device_scan_rows(dev.ieee) + attr_row = next(row for row in rows.attributes if row.attr_id == 0x0000) + assert attr_row.read_complete is True + assert attr_row.read_status == "success" + + await app.shutdown() + + +async def test_device_scanner_attribute_reads_batch_in_groups_of_three( + tmp_path: Path, +): + app, dev, target = await _make_basic_scan_target(tmp_path) + await _seed_scan_progress(app, target) + await app._dblistener.upsert_device_scan_progress( + ieee=dev.ieee, + endpoint_id=1, + cluster_type=ClusterType.Server, + cluster_id=Basic.cluster_id, + manufacturer_code_scope=None, + attr_discovery_complete=True, + attr_discovery_next_id=4, + attr_reads_complete=False, + cmd_rx_complete=False, + cmd_rx_next_id=0, + cmd_tx_complete=False, + cmd_tx_next_id=0, + last_started=None, + last_finished=None, + last_error_code=None, + last_error=None, + last_success=None, + ) + for attr_def in ( + Basic.AttributeDefs.zcl_version, + Basic.AttributeDefs.app_version, + Basic.AttributeDefs.stack_version, + Basic.AttributeDefs.hw_version, + ): + await _seed_discovered_attribute( + app, + target, + attr_id=attr_def.id, + attribute_name=attr_def.name, + datatype=foundation.DataTypeId.uint8, + access=foundation.AttributeAccessControl.READ, + ) + + read_rsp = foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Read_Attributes_rsp + ].schema + + async def read_attributes_raw( + attributes, + *args, + manufacturer=None, + **kwargs, + ): + assert manufacturer is None + + if list(attributes) == [0, 1, 2]: + return read_rsp( + status_records=[ + foundation.ReadAttributeRecord( + attrid=0, + status=foundation.Status.SUCCESS, + value=foundation.TypeValue( + type=foundation.DataTypeId.uint8, + value=t.uint8_t(3), + ), + ), + foundation.ReadAttributeRecord( + attrid=1, + status=foundation.Status.SUCCESS, + value=foundation.TypeValue( + type=foundation.DataTypeId.uint8, + value=t.uint8_t(4), + ), + ), + foundation.ReadAttributeRecord( + attrid=2, + status=foundation.Status.SUCCESS, + value=foundation.TypeValue( + type=foundation.DataTypeId.uint8, + value=t.uint8_t(5), + ), + ), + ] + ) + + if list(attributes) == [3]: + return read_rsp( + status_records=[ + foundation.ReadAttributeRecord( + attrid=3, + status=foundation.Status.SUCCESS, + value=foundation.TypeValue( + type=foundation.DataTypeId.uint8, + value=t.uint8_t(6), + ), + ) + ] + ) + + raise AssertionError(f"Unexpected attribute batch: {attributes!r}") + + target.cluster.read_attributes_raw = AsyncMock(side_effect=read_attributes_raw) + + await app.device_scanner._read_attributes_for_target(target) + + assert [ + list(call.args[0]) + for call in target.cluster.read_attributes_raw.await_args_list + ] == [[0, 1, 2], [3]] + + await app.shutdown() + + +async def test_device_scanner_attribute_reads_missing_status_records_are_logged_and_do_not_abort_batches( + tmp_path: Path, caplog: pytest.LogCaptureFixture +): + app, dev, target = await _make_basic_scan_target(tmp_path) + await _seed_scan_progress(app, target) + await app._dblistener.upsert_device_scan_progress( + ieee=dev.ieee, + endpoint_id=1, + cluster_type=ClusterType.Server, + cluster_id=Basic.cluster_id, + manufacturer_code_scope=None, + attr_discovery_complete=True, + attr_discovery_next_id=4, + attr_reads_complete=False, + cmd_rx_complete=False, + cmd_rx_next_id=0, + cmd_tx_complete=False, + cmd_tx_next_id=0, + last_started=None, + last_finished=None, + last_error_code=None, + last_error=None, + last_success=None, + ) + for attr_def in ( + Basic.AttributeDefs.zcl_version, + Basic.AttributeDefs.app_version, + Basic.AttributeDefs.stack_version, + Basic.AttributeDefs.hw_version, + ): + await _seed_discovered_attribute( + app, + target, + attr_id=attr_def.id, + attribute_name=attr_def.name, + datatype=foundation.DataTypeId.uint8, + access=foundation.AttributeAccessControl.READ, + ) + + read_rsp = foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Read_Attributes_rsp + ].schema + + async def read_attributes_raw( + attributes, + *args, + manufacturer=None, + **kwargs, + ): + assert manufacturer is None + + if list(attributes) == [0, 1, 2]: + return read_rsp( + status_records=[ + foundation.ReadAttributeRecord( + attrid=0, + status=foundation.Status.SUCCESS, + value=foundation.TypeValue( + type=foundation.DataTypeId.uint8, + value=t.uint8_t(3), + ), + ), + foundation.ReadAttributeRecord( + attrid=1, + status=foundation.Status.SUCCESS, + value=foundation.TypeValue( + type=foundation.DataTypeId.uint8, + value=t.uint8_t(4), + ), + ), + ] + ) + + if list(attributes) == [3]: + return read_rsp( + status_records=[ + foundation.ReadAttributeRecord( + attrid=3, + status=foundation.Status.SUCCESS, + value=foundation.TypeValue( + type=foundation.DataTypeId.uint8, + value=t.uint8_t(6), + ), + ) + ] + ) + + if list(attributes) == [2]: + return read_rsp(status_records=[]) + + raise AssertionError(f"Unexpected attribute batch: {attributes!r}") + + target.cluster.read_attributes_raw = AsyncMock(side_effect=read_attributes_raw) + + with caplog.at_level(logging.WARNING): + with pytest.raises(zigpy.device_scanner._TerminalStepFailure) as exc_info: + await app.device_scanner._read_attributes_for_target(target) + + assert exc_info.value.error_code == "transport_failure" + assert "0x0002" in str(exc_info.value) + assert [ + list(call.args[0]) + for call in target.cluster.read_attributes_raw.await_args_list + ] == [[0, 1, 2], [2], [3]] + assert any( + "missing status records" in record.message and "0x0002" in record.message + for record in caplog.records + ) + + rows = await app._dblistener.get_device_scan_rows(dev.ieee) + attrs = {row.attr_id: row for row in rows.attributes} + assert attrs[0].read_complete is True + assert attrs[0].read_status == "success" + assert attrs[1].read_complete is True + assert attrs[1].read_status == "success" + assert attrs[2].read_complete is False + assert attrs[2].read_status == "transport_failure" + assert attrs[2].last_error_code == "transport_failure" + assert attrs[2].value is None + assert attrs[3].read_complete is True + assert attrs[3].read_status == "success" + assert rows.progress[0].attr_reads_complete is False + + await app.shutdown() + + +async def test_device_scanner_attribute_reads_retry_before_split( + tmp_path: Path, +): + app, dev, target = await _make_basic_scan_target(tmp_path) + await app._dblistener.upsert_device_scan_progress( + ieee=dev.ieee, + endpoint_id=1, + cluster_type=ClusterType.Server, + cluster_id=Basic.cluster_id, + manufacturer_code_scope=None, + attr_discovery_complete=True, + attr_discovery_next_id=2, + attr_reads_complete=False, + cmd_rx_complete=False, + cmd_rx_next_id=0, + cmd_tx_complete=False, + cmd_tx_next_id=0, + last_started=None, + last_finished=None, + last_error_code=None, + last_error=None, + last_success=None, + ) + await _seed_discovered_attribute( + app, + target, + attr_id=Basic.AttributeDefs.zcl_version.id, + attribute_name=Basic.AttributeDefs.zcl_version.name, + datatype=foundation.DataTypeId.uint8, + access=foundation.AttributeAccessControl.READ, + ) + await _seed_discovered_attribute( + app, + target, + attr_id=Basic.AttributeDefs.app_version.id, + attribute_name=Basic.AttributeDefs.app_version.name, + datatype=foundation.DataTypeId.uint8, + access=foundation.AttributeAccessControl.READ, + ) + + read_rsp = foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Read_Attributes_rsp + ].schema + read_attempts = 0 + + async def read_attributes_raw( + attributes, + *args, + manufacturer=None, + **kwargs, + ): + nonlocal read_attempts + read_attempts += 1 + assert manufacturer is None + assert list(attributes) == [ + Basic.AttributeDefs.zcl_version.id, + Basic.AttributeDefs.app_version.id, + ] + + if read_attempts == 1: + raise zigpy.exceptions.DeliveryError("boom") + + return read_rsp( + status_records=[ + foundation.ReadAttributeRecord( + attrid=Basic.AttributeDefs.zcl_version.id, + status=foundation.Status.SUCCESS, + value=foundation.TypeValue( + type=foundation.DataTypeId.uint8, + value=t.uint8_t(4), + ), + ), + foundation.ReadAttributeRecord( + attrid=Basic.AttributeDefs.app_version.id, + status=foundation.Status.SUCCESS, + value=foundation.TypeValue( + type=foundation.DataTypeId.uint8, + value=t.uint8_t(7), + ), + ), + ] + ) + + target.cluster.read_attributes_raw = AsyncMock(side_effect=read_attributes_raw) + + await app.device_scanner._read_attributes_for_target(target) + + rows = await app._dblistener.get_device_scan_rows(dev.ieee) + attrs = {row.attr_id: row for row in rows.attributes} + assert attrs[Basic.AttributeDefs.zcl_version.id].read_complete is True + assert attrs[Basic.AttributeDefs.zcl_version.id].value == b"\x04" + assert attrs[Basic.AttributeDefs.app_version.id].read_complete is True + assert attrs[Basic.AttributeDefs.app_version.id].value == b"\x07" + assert rows.progress[0].attr_reads_complete is True + assert [ + list(call.args[0]) + for call in target.cluster.read_attributes_raw.await_args_list + ] == [[0, 1], [0, 1]] + + await app.shutdown() + + +async def test_device_scanner_attribute_reads_treat_default_response_as_terminal_protocol_outcome( + tmp_path: Path, +): + app, dev, target = await _make_basic_scan_target(tmp_path) + await app._dblistener.upsert_device_scan_progress( + ieee=dev.ieee, + endpoint_id=1, + cluster_type=ClusterType.Server, + cluster_id=Basic.cluster_id, + manufacturer_code_scope=None, + attr_discovery_complete=True, + attr_discovery_next_id=2, + attr_reads_complete=False, + cmd_rx_complete=False, + cmd_rx_next_id=0, + cmd_tx_complete=False, + cmd_tx_next_id=0, + last_started=None, + last_finished=None, + last_error_code=None, + last_error=None, + last_success=None, + ) + await _seed_discovered_attribute( + app, + target, + attr_id=Basic.AttributeDefs.zcl_version.id, + attribute_name=Basic.AttributeDefs.zcl_version.name, + datatype=foundation.DataTypeId.uint8, + access=foundation.AttributeAccessControl.READ, + ) + await _seed_discovered_attribute( + app, + target, + attr_id=Basic.AttributeDefs.app_version.id, + attribute_name=Basic.AttributeDefs.app_version.name, + datatype=foundation.DataTypeId.uint8, + access=foundation.AttributeAccessControl.READ, + ) + + target.cluster.read_attributes_raw = AsyncMock( + return_value=( + foundation.GeneralCommand.Default_Response, + foundation.Status.UNSUP_GENERAL_COMMAND, + ) + ) + + await app.device_scanner._read_attributes_for_target(target) + + rows = await app._dblistener.get_device_scan_rows(dev.ieee) + attrs = {row.attr_id: row for row in rows.attributes} + assert attrs[Basic.AttributeDefs.zcl_version.id].read_complete is True + assert ( + attrs[Basic.AttributeDefs.zcl_version.id].read_status == "unsup_general_command" + ) + assert attrs[Basic.AttributeDefs.zcl_version.id].last_error_code is None + assert attrs[Basic.AttributeDefs.app_version.id].read_complete is True + assert ( + attrs[Basic.AttributeDefs.app_version.id].read_status == "unsup_general_command" + ) + assert attrs[Basic.AttributeDefs.app_version.id].last_error_code is None + assert rows.progress[0].attr_reads_complete is True + + await app.shutdown() + + +async def test_device_scanner_attribute_reads_treat_default_response_object_as_terminal_protocol_outcome( + tmp_path: Path, +): + app, dev, target = await _make_basic_scan_target(tmp_path) + await app._dblistener.upsert_device_scan_progress( + ieee=dev.ieee, + endpoint_id=1, + cluster_type=ClusterType.Server, + cluster_id=Basic.cluster_id, + manufacturer_code_scope=None, + attr_discovery_complete=True, + attr_discovery_next_id=2, + attr_reads_complete=False, + cmd_rx_complete=False, + cmd_rx_next_id=0, + cmd_tx_complete=False, + cmd_tx_next_id=0, + last_started=None, + last_finished=None, + last_error_code=None, + last_error=None, + last_success=None, + ) + await _seed_discovered_attribute( + app, + target, + attr_id=Basic.AttributeDefs.zcl_version.id, + attribute_name=Basic.AttributeDefs.zcl_version.name, + datatype=foundation.DataTypeId.uint8, + access=foundation.AttributeAccessControl.READ, + ) + await _seed_discovered_attribute( + app, + target, + attr_id=Basic.AttributeDefs.app_version.id, + attribute_name=Basic.AttributeDefs.app_version.name, + datatype=foundation.DataTypeId.uint8, + access=foundation.AttributeAccessControl.READ, + ) + + target.cluster.read_attributes_raw = AsyncMock( + return_value=foundation.DefaultResponse( + command_id=foundation.GeneralCommand.Read_Attributes, + status=foundation.Status.UNSUP_GENERAL_COMMAND, + ) + ) + + await app.device_scanner._read_attributes_for_target(target) + + rows = await app._dblistener.get_device_scan_rows(dev.ieee) + attrs = {row.attr_id: row for row in rows.attributes} + assert attrs[Basic.AttributeDefs.zcl_version.id].read_complete is True + assert ( + attrs[Basic.AttributeDefs.zcl_version.id].read_status == "unsup_general_command" + ) + assert attrs[Basic.AttributeDefs.zcl_version.id].last_error_code is None + assert attrs[Basic.AttributeDefs.app_version.id].read_complete is True + assert ( + attrs[Basic.AttributeDefs.app_version.id].read_status == "unsup_general_command" + ) + assert attrs[Basic.AttributeDefs.app_version.id].last_error_code is None + assert rows.progress[0].attr_reads_complete is True + + await app.shutdown() + + +async def test_device_scanner_attribute_reads_split_transport_failures_and_keep_retryable_rows_pending( + tmp_path: Path, +): + app, dev, target = await _make_basic_scan_target(tmp_path) + await app._dblistener.upsert_device_scan_progress( + ieee=dev.ieee, + endpoint_id=1, + cluster_type=ClusterType.Server, + cluster_id=Basic.cluster_id, + manufacturer_code_scope=None, + attr_discovery_complete=True, + attr_discovery_next_id=2, + attr_reads_complete=False, + cmd_rx_complete=False, + cmd_rx_next_id=0, + cmd_tx_complete=False, + cmd_tx_next_id=0, + last_started=None, + last_finished=None, + last_error_code=None, + last_error=None, + last_success=None, + ) + await _seed_discovered_attribute( + app, + target, + attr_id=Basic.AttributeDefs.zcl_version.id, + attribute_name=Basic.AttributeDefs.zcl_version.name, + datatype=foundation.DataTypeId.uint8, + access=foundation.AttributeAccessControl.READ, + ) + await _seed_discovered_attribute( + app, + target, + attr_id=Basic.AttributeDefs.app_version.id, + attribute_name=Basic.AttributeDefs.app_version.name, + datatype=foundation.DataTypeId.uint8, + access=foundation.AttributeAccessControl.READ, + ) + + read_rsp = foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Read_Attributes_rsp + ].schema + + async def read_attributes_raw( + attributes, + *args, + manufacturer=None, + **kwargs, + ): + if list(attributes) == [ + Basic.AttributeDefs.zcl_version.id, + Basic.AttributeDefs.app_version.id, + ]: + raise zigpy.exceptions.DeliveryError("boom") + if list(attributes) == [Basic.AttributeDefs.zcl_version.id]: + return read_rsp( + status_records=[ + foundation.ReadAttributeRecord( + attrid=Basic.AttributeDefs.zcl_version.id, + status=foundation.Status.SUCCESS, + value=foundation.TypeValue( + type=foundation.DataTypeId.uint8, + value=t.uint8_t(4), + ), + ) + ] + ) + if list(attributes) == [Basic.AttributeDefs.app_version.id]: + raise zigpy.exceptions.DeliveryError("still broken") + raise AssertionError(f"Unexpected attribute batch: {attributes!r}") + + target.cluster.read_attributes_raw = AsyncMock(side_effect=read_attributes_raw) + + with pytest.raises(zigpy.device_scanner._TerminalStepFailure) as exc_info: + await app.device_scanner._read_attributes_for_target(target) + + rows = await app._dblistener.get_device_scan_rows(dev.ieee) + attrs = {row.attr_id: row for row in rows.attributes} + assert exc_info.value.error_code == "transport_failure" + assert str(exc_info.value) == "still broken" + assert attrs[Basic.AttributeDefs.zcl_version.id].read_complete is True + assert attrs[Basic.AttributeDefs.zcl_version.id].read_status == "success" + assert attrs[Basic.AttributeDefs.zcl_version.id].value == b"\x04" + assert attrs[Basic.AttributeDefs.app_version.id].read_complete is False + assert attrs[Basic.AttributeDefs.app_version.id].read_status == "transport_failure" + assert ( + attrs[Basic.AttributeDefs.app_version.id].last_error_code == "transport_failure" + ) + assert attrs[Basic.AttributeDefs.app_version.id].value is None + assert rows.progress[0].attr_reads_complete is False + assert [ + list(call.args[0]) + for call in target.cluster.read_attributes_raw.await_args_list + ] == [[0, 1], [0, 1], [0], [1]] + + await app.shutdown() + + +async def test_device_scanner_scan_marks_attribute_read_transport_failures_partial( + tmp_path: Path, +): + app, dev, target = await _make_basic_scan_target(tmp_path) + raw_descriptors = zigpy.device_scanner._RawDeviceDescriptors( + node_descriptor=dev.node_desc, + endpoints=(), + ) + await app._dblistener.upsert_device_scan_progress( + ieee=dev.ieee, + endpoint_id=1, + cluster_type=ClusterType.Server, + cluster_id=Basic.cluster_id, + manufacturer_code_scope=None, + attr_discovery_complete=True, + attr_discovery_next_id=2, + attr_reads_complete=False, + cmd_rx_complete=False, + cmd_rx_next_id=0, + cmd_tx_complete=False, + cmd_tx_next_id=0, + last_started=None, + last_finished=None, + last_error_code=None, + last_error=None, + last_success=None, + ) + await _seed_discovered_attribute( + app, + target, + attr_id=Basic.AttributeDefs.zcl_version.id, + attribute_name=Basic.AttributeDefs.zcl_version.name, + datatype=foundation.DataTypeId.uint8, + access=foundation.AttributeAccessControl.READ, + ) + await _seed_discovered_attribute( + app, + target, + attr_id=Basic.AttributeDefs.app_version.id, + attribute_name=Basic.AttributeDefs.app_version.name, + datatype=foundation.DataTypeId.uint8, + access=foundation.AttributeAccessControl.READ, + ) + + read_rsp = foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Read_Attributes_rsp + ].schema + + async def read_attributes_raw( + attributes, + *args, + manufacturer=None, + **kwargs, + ): + if list(attributes) == [ + Basic.AttributeDefs.zcl_version.id, + Basic.AttributeDefs.app_version.id, + ]: + raise zigpy.exceptions.DeliveryError("boom") + if list(attributes) == [Basic.AttributeDefs.zcl_version.id]: + return read_rsp( + status_records=[ + foundation.ReadAttributeRecord( + attrid=Basic.AttributeDefs.zcl_version.id, + status=foundation.Status.SUCCESS, + value=foundation.TypeValue( + type=foundation.DataTypeId.uint8, + value=t.uint8_t(4), + ), + ) + ] + ) + if list(attributes) == [Basic.AttributeDefs.app_version.id]: + raise zigpy.exceptions.DeliveryError("still broken") + raise AssertionError(f"Unexpected attribute batch: {attributes!r}") + + target.cluster.read_attributes_raw = AsyncMock(side_effect=read_attributes_raw) + + with ( + patch.object( + app.device_scanner, + "_refresh_raw_descriptors", + new=AsyncMock(return_value=raw_descriptors), + ), + patch.object( + app.device_scanner, + "_build_scan_targets_for_node_descriptor", + new=AsyncMock(return_value=(target,)), + ), + patch.object( + app.device_scanner, + "_discover_attributes_for_target", + new=AsyncMock(), + ) as discover_attrs, + patch.object( + app.device_scanner, + "_discover_commands_received_for_target", + new=AsyncMock(), + ) as discover_rx, + patch.object( + app.device_scanner, + "_discover_commands_generated_for_target", + new=AsyncMock(), + ) as discover_tx, + patch.object(app.device_scanner, "_pace_requests", new=AsyncMock()), + ): + summary = await app.device_scanner._run_scan_body( + dev, resume=True, force_full=False + ) + + discover_attrs.assert_not_awaited() + discover_rx.assert_awaited_once() + discover_tx.assert_awaited_once() + assert summary.outcome == "partial" + assert summary.error_code == "transport_failure" + + rows = await app._dblistener.get_device_scan_rows(dev.ieee) + failed_progress = next( + row + for row in rows.progress + if row.endpoint_id == target.endpoint_id + and row.cluster_type == target.cluster.cluster_type + and row.cluster_id == target.cluster.cluster_id + and row.manufacturer_code_scope is None + ) + assert failed_progress.attr_reads_complete is False + assert failed_progress.last_error_code == "transport_failure" + assert failed_progress.last_error == "still broken" + + await app.shutdown() + + +async def test_device_scanner_attribute_discovery_persists_standard_and_manufacturer_scopes( + tmp_path: Path, +): + app, dev, standard_target = await _make_basic_scan_target(tmp_path) + targets = await app.device_scanner._build_scan_targets_for_node_descriptor( + dev, dev.node_desc + ) + standard_target = next( + target + for target in targets + if target.cluster.cluster_id == Basic.cluster_id + and target.scope.manufacturer_code_scope is None + ) + manufacturer_target = next( + target + for target in targets + if target.cluster.cluster_id == Basic.cluster_id + and target.scope.manufacturer_code_scope == dev.node_desc.manufacturer_code + ) + + discover_rsp = foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Discover_Attribute_Extended_rsp + ].schema + manufacturers = [] + + async def discover_attributes_extended( + start_attr_id, page_size, *, manufacturer=None + ): + manufacturers.append(manufacturer) + return discover_rsp( + discovery_complete=True, + extended_attr_info=[ + foundation.DiscoverAttributesExtendedResponseRecord( + attrid=Basic.AttributeDefs.zcl_version.id, + datatype=foundation.DataTypeId.uint8, + acl=foundation.AttributeAccessControl.READ, + ) + ], + ) + + standard_target.cluster.discover_attributes_extended = AsyncMock( + side_effect=discover_attributes_extended + ) + + await app.device_scanner._discover_attributes_for_target(standard_target) + await app.device_scanner._discover_attributes_for_target(manufacturer_target) + + rows = await app._dblistener.get_device_scan_rows(dev.ieee) + attr_rows = [ + row + for row in rows.attributes + if row.attr_id == Basic.AttributeDefs.zcl_version.id + ] + assert {row.manufacturer_code_scope for row in attr_rows} == { + None, + dev.node_desc.manufacturer_code, + } + assert manufacturers == [None, dev.node_desc.manufacturer_code] + + await app.shutdown() + + +async def test_device_scanner_command_discovery_persists_received_and_generated_pages( + tmp_path: Path, +): + app, dev, server_target, _ = await _make_onoff_scan_targets(tmp_path) + + cmd_rsp = foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Discover_Commands_Received_rsp + ].schema + server_target.cluster.discover_commands_received = AsyncMock( + side_effect=[ + cmd_rsp(discovery_complete=False, command_ids=[0x00, 0x01]), + cmd_rsp(discovery_complete=True, command_ids=[0x02]), + ] + ) + server_target.cluster.discover_commands_generated = AsyncMock( + return_value=cmd_rsp(discovery_complete=True, command_ids=[0x00]) + ) + + await app.device_scanner._discover_commands_received_for_target(server_target) + await app.device_scanner._discover_commands_generated_for_target(server_target) + + rows = await app._dblistener.get_device_scan_rows(dev.ieee) + rx_rows = [row for row in rows.commands if row.direction == "received"] + tx_rows = [row for row in rows.commands if row.direction == "generated"] + assert [row.command_id for row in rx_rows] == [0x00, 0x01, 0x02] + assert [row.command_id for row in tx_rows] == [0x00] + assert rows.progress[0].cmd_rx_complete is True + assert rows.progress[0].cmd_rx_next_id == 3 + assert rows.progress[0].cmd_tx_complete is True + assert rows.progress[0].cmd_tx_next_id == 1 + assert server_target.cluster.discover_commands_received.await_args_list[0].args == ( + 0, + 16, + ) + assert server_target.cluster.discover_commands_generated.await_args_list[ + 0 + ].args == ( + 0, + 16, + ) + + await app.shutdown() + + +async def test_device_scanner_command_discovery_logs_and_completes_on_unsupported_default_response( + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +): + app, dev, target, _ = await _make_onoff_scan_targets(tmp_path) + target.cluster.discover_commands_received = AsyncMock( + return_value=( + foundation.GeneralCommand.Default_Response, + foundation.Status.UNSUP_GENERAL_COMMAND, + ) + ) + caplog.set_level(logging.WARNING, logger="zigpy.device_scanner") + + await app.device_scanner._discover_commands_received_for_target(target) + + rows = await app._dblistener.get_device_scan_rows(dev.ieee) + assert rows.commands == [] + progress = next( + row + for row in rows.progress + if row.endpoint_id == target.endpoint_id + and row.cluster_type == target.cluster.cluster_type + and row.cluster_id == target.cluster.cluster_id + and row.manufacturer_code_scope is None + ) + assert progress.cmd_rx_complete is True + assert progress.cmd_rx_next_id == 0 + assert progress.last_error_code is None + assert "unsupported command discovery response" in caplog.text + + await app.shutdown() + + +async def test_device_scanner_command_discovery_persists_client_role_for_output_clusters( + tmp_path: Path, +): + app, dev, _, client_target = await _make_onoff_scan_targets(tmp_path) + + cmd_rsp = foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Discover_Commands_Generated_rsp + ].schema + client_target.cluster.discover_commands_generated = AsyncMock( + return_value=cmd_rsp(discovery_complete=True, command_ids=[0x00, 0x02]) + ) + + await app.device_scanner._discover_commands_generated_for_target(client_target) + + rows = await app._dblistener.get_device_scan_rows(dev.ieee) + assert [ + (row.cluster_type, row.direction, row.command_id) for row in rows.commands + ] == [ + (ClusterType.Client, "generated", 0x00), + (ClusterType.Client, "generated", 0x02), + ] + assert client_target.cluster.is_client is True + assert client_target.cluster.is_server is False + + await app.shutdown() + + +async def test_device_scanner_command_discovery_persists_manufacturer_scope( + tmp_path: Path, +): + app, dev, server_target, _ = await _make_onoff_scan_targets(tmp_path) + targets = await app.device_scanner._build_scan_targets_for_node_descriptor( + dev, dev.node_desc + ) + manufacturer_target = next( + target + for target in targets + if target.cluster.cluster_id == OnOff.cluster_id + and target.cluster.cluster_type == ClusterType.Server + and target.scope.manufacturer_code_scope == dev.node_desc.manufacturer_code + ) + + cmd_rsp = foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Discover_Commands_Received_rsp + ].schema + manufacturers = [] + + async def discover_commands_received( + start_command_id, page_size, *, manufacturer=None + ): + manufacturers.append(manufacturer) + return cmd_rsp(discovery_complete=True, command_ids=[0x00]) + + manufacturer_target.cluster.discover_commands_received = AsyncMock( + side_effect=discover_commands_received + ) + + await app.device_scanner._discover_commands_received_for_target(manufacturer_target) + + rows = await app._dblistener.get_device_scan_rows(dev.ieee) + rx_rows = [ + row + for row in rows.commands + if row.direction == "received" + and row.cluster_id == OnOff.cluster_id + and row.manufacturer_code_scope == dev.node_desc.manufacturer_code + ] + assert [row.command_id for row in rx_rows] == [0x00] + assert manufacturers == [dev.node_desc.manufacturer_code] + + await app.shutdown() + + +async def test_device_scanner_manufacturer_command_discovery_logs_and_completes_on_manufacturer_unsupported_default_response( + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +): + app, dev, _, _ = await _make_onoff_scan_targets(tmp_path) + targets = await app.device_scanner._build_scan_targets_for_node_descriptor( + dev, dev.node_desc + ) + manufacturer_target = next( + target + for target in targets + if target.cluster.cluster_id == OnOff.cluster_id + and target.cluster.cluster_type == ClusterType.Server + and target.scope.manufacturer_code_scope == dev.node_desc.manufacturer_code + ) + manufacturer_target.cluster.discover_commands_received = AsyncMock( + return_value=foundation.DefaultResponse( + command_id=foundation.GeneralCommand.Discover_Commands_Received, + status=foundation.Status.UNSUP_MANUF_GENERAL_COMMAND, + ) + ) + caplog.set_level(logging.WARNING, logger="zigpy.device_scanner") + + await app.device_scanner._discover_commands_received_for_target(manufacturer_target) + + rows = await app._dblistener.get_device_scan_rows(dev.ieee) + progress = next( + row + for row in rows.progress + if row.endpoint_id == manufacturer_target.endpoint_id + and row.cluster_type == manufacturer_target.cluster.cluster_type + and row.cluster_id == manufacturer_target.cluster.cluster_id + and row.manufacturer_code_scope == dev.node_desc.manufacturer_code + ) + assert progress.cmd_rx_complete is True + assert progress.cmd_rx_next_id == 0 + assert progress.last_error_code is None + assert "unsupported command discovery response" in caplog.text.lower() + + await app.shutdown() + + +async def test_device_scanner_retries_manufacturer_command_discovery_timeout_once( + tmp_path: Path, + caplog: pytest.LogCaptureFixture, +): + app, dev, _, _ = await _make_onoff_scan_targets(tmp_path) + targets = await app.device_scanner._build_scan_targets_for_node_descriptor( + dev, dev.node_desc + ) + manufacturer_target = next( + target + for target in targets + if target.cluster.cluster_id == OnOff.cluster_id + and target.cluster.cluster_type == ClusterType.Server + and target.scope.manufacturer_code_scope == dev.node_desc.manufacturer_code + ) + cmd_rsp = foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Discover_Commands_Generated_rsp + ].schema + manufacturer_target.cluster.discover_commands_generated = AsyncMock( + side_effect=[ + TimeoutError(), + cmd_rsp(discovery_complete=True, command_ids=[0x02]), + ] + ) + caplog.set_level(logging.WARNING, logger="zigpy.device_scanner") + + with patch("zigpy.device_scanner.asyncio.sleep", new=AsyncMock()): + await app.device_scanner._discover_commands_generated_for_target( + manufacturer_target + ) + + assert manufacturer_target.cluster.discover_commands_generated.await_count == 2 + assert "Retrying manufacturer-scoped generated command discovery" in caplog.text + + rows = await app._dblistener.get_device_scan_rows(dev.ieee) + progress = next( + row + for row in rows.progress + if row.endpoint_id == manufacturer_target.endpoint_id + and row.cluster_type == manufacturer_target.cluster.cluster_type + and row.cluster_id == manufacturer_target.cluster.cluster_id + and row.manufacturer_code_scope == dev.node_desc.manufacturer_code + ) + commands = [ + row + for row in rows.commands + if row.endpoint_id == manufacturer_target.endpoint_id + and row.cluster_type == manufacturer_target.cluster.cluster_type + and row.cluster_id == manufacturer_target.cluster.cluster_id + and row.manufacturer_code_scope == dev.node_desc.manufacturer_code + and row.direction == "generated" + ] + assert progress.cmd_tx_complete is True + assert [row.command_id for row in commands] == [0x02] + + await app.shutdown() + + +async def test_device_scanner_scan_reuses_duplicate_requests_and_rejects_conflicts( + tmp_path: Path, +): + app, dev, _ = await _make_basic_scan_target(tmp_path) + started = asyncio.Event() + finish = asyncio.Event() + calls = [] + + async def mock_run_scan_body(device, *, resume, force_full): + calls.append((device.ieee, resume, force_full)) + started.set() + await finish.wait() + return _make_scan_summary(device.ieee) + + with patch.object( + app.device_scanner, + "_run_scan_body", + new=AsyncMock(side_effect=mock_run_scan_body), + create=True, + ): + first = asyncio.create_task(app.device_scanner.scan(dev.ieee)) + await asyncio.wait_for(started.wait(), timeout=0.2) + + second = asyncio.create_task(app.device_scanner.scan(dev.ieee)) + + with pytest.raises(zigpy.device_scanner.ScanInProgressError): + await app.device_scanner.scan(dev.ieee, resume=False) + + finish.set() + + assert await first == await second + + assert calls == [(dev.ieee, True, False)] + + await app.shutdown() + + +async def test_device_scanner_scan_waits_fifo_for_different_devices(tmp_path: Path): + app, first_dev, _ = await _make_basic_scan_target(tmp_path) + second_dev = app.add_device( + nwk=0x2234, + ieee=t.EUI64.convert("aa:bb:cc:dd:ee:ff:00:33"), + ) + second_dev.node_desc = make_node_desc() + + ep1 = second_dev.add_endpoint(1) + ep1.status = zigpy.endpoint.Status.ZDO_INIT + ep1.profile_id = zha.PROFILE_ID + ep1.device_type = zha.DeviceType.PUMP + ep1.add_input_cluster(Basic.cluster_id) + + await app._dblistener._save_device(second_dev) + + first_started = asyncio.Event() + allow_first_finish = asyncio.Event() + start_order = [] + + async def mock_run_scan_body(device, *, resume, force_full): + start_order.append(device.ieee) + if device.ieee == first_dev.ieee: + first_started.set() + await allow_first_finish.wait() + return _make_scan_summary(device.ieee) + + with patch.object( + app.device_scanner, + "_run_scan_body", + new=AsyncMock(side_effect=mock_run_scan_body), + create=True, + ): + first = asyncio.create_task(app.device_scanner.scan(first_dev.ieee)) + await asyncio.wait_for(first_started.wait(), timeout=0.2) + + second = asyncio.create_task(app.device_scanner.scan(second_dev.ieee)) + await asyncio.sleep(0) + assert start_order == [first_dev.ieee] + + allow_first_finish.set() + await asyncio.gather(first, second) + + assert start_order == [first_dev.ieee, second_dev.ieee] + + await app.shutdown() + + +async def test_device_scanner_scan_queued_duplicate_and_conflict_match_running_behavior( + tmp_path: Path, +): + app, first_dev, _ = await _make_basic_scan_target(tmp_path) + second_dev = app.add_device( + nwk=0x3234, + ieee=t.EUI64.convert("aa:bb:cc:dd:ee:ff:00:99"), + ) + second_dev.node_desc = make_node_desc() + + ep1 = second_dev.add_endpoint(1) + ep1.status = zigpy.endpoint.Status.ZDO_INIT + ep1.profile_id = zha.PROFILE_ID + ep1.device_type = zha.DeviceType.PUMP + ep1.add_input_cluster(Basic.cluster_id) + + await app._dblistener._save_device(second_dev) + + first_started = asyncio.Event() + allow_first_finish = asyncio.Event() + second_started = asyncio.Event() + allow_second_finish = asyncio.Event() + calls = [] + + async def mock_run_scan_body(device, *, resume, force_full): + calls.append((device.ieee, resume, force_full)) + if device.ieee == first_dev.ieee: + first_started.set() + await allow_first_finish.wait() + else: + second_started.set() + await allow_second_finish.wait() + return _make_scan_summary(device.ieee) + + with patch.object( + app.device_scanner, + "_run_scan_body", + new=AsyncMock(side_effect=mock_run_scan_body), + create=True, + ): + first = asyncio.create_task(app.device_scanner.scan(first_dev.ieee)) + await asyncio.wait_for(first_started.wait(), timeout=0.2) + + queued = asyncio.create_task(app.device_scanner.scan(second_dev.ieee)) + duplicate = asyncio.create_task(app.device_scanner.scan(second_dev.ieee)) + await asyncio.sleep(0) + + with pytest.raises(zigpy.device_scanner.ScanInProgressError): + await app.device_scanner.scan(second_dev.ieee, resume=False) + + allow_first_finish.set() + await asyncio.wait_for(second_started.wait(), timeout=0.2) + allow_second_finish.set() + + queued_summary, duplicate_summary = await asyncio.gather(queued, duplicate) + await first + + assert queued_summary == duplicate_summary + assert calls == [ + (first_dev.ieee, True, False), + (second_dev.ieee, True, False), + ] + + await app.shutdown() + + +async def test_device_scanner_scan_caller_cancellation_does_not_cancel_shared_work( + tmp_path: Path, +): + app, dev, _ = await _make_basic_scan_target(tmp_path) + started = asyncio.Event() + finish = asyncio.Event() + + async def mock_run_scan_body(device, *, resume, force_full): + started.set() + await finish.wait() + return _make_scan_summary(device.ieee) + + with patch.object( + app.device_scanner, + "_run_scan_body", + new=AsyncMock(side_effect=mock_run_scan_body), + create=True, + ): + owner = asyncio.create_task(app.device_scanner.scan(dev.ieee)) + await asyncio.wait_for(started.wait(), timeout=0.2) + + detached = asyncio.create_task(app.device_scanner.scan(dev.ieee)) + await asyncio.sleep(0) + detached.cancel() + + with pytest.raises(asyncio.CancelledError): + await detached + + assert not owner.done() + + finish.set() + summary = await owner + + assert summary.outcome == "success" + + await app.shutdown() + + +async def test_device_scanner_scan_missing_target_before_execution_fails_cleanly( + tmp_path: Path, +): + app, first_dev, _ = await _make_basic_scan_target(tmp_path) + second_dev = app.add_device( + nwk=0x2234, + ieee=t.EUI64.convert("aa:bb:cc:dd:ee:ff:00:44"), + ) + second_dev.node_desc = make_node_desc() + + ep1 = second_dev.add_endpoint(1) + ep1.status = zigpy.endpoint.Status.ZDO_INIT + ep1.profile_id = zha.PROFILE_ID + ep1.device_type = zha.DeviceType.PUMP + ep1.add_input_cluster(Basic.cluster_id) + + await app._dblistener._save_device(second_dev) + + events = [] + + class Listener: + def scan_finished(self, event): + events.append(event) + + app.device_scanner.add_listener(Listener()) + + started = asyncio.Event() + finish = asyncio.Event() + + async def mock_run_scan_body(device, *, resume, force_full): + if device.ieee == first_dev.ieee: + started.set() + await finish.wait() + return _make_scan_summary(device.ieee) + + with patch.object( + app.device_scanner, + "_run_scan_body", + new=AsyncMock(side_effect=mock_run_scan_body), + create=True, + ): + first = asyncio.create_task(app.device_scanner.scan(first_dev.ieee)) + await asyncio.wait_for(started.wait(), timeout=0.2) + + second = asyncio.create_task(app.device_scanner.scan(second_dev.ieee)) + await asyncio.sleep(0) + app.devices.pop(second_dev.ieee) + + finish.set() + await first + + with pytest.raises(zigpy.device_scanner.DeviceScanTargetMissingError): + await second + + assert events[-1].status == "failed" + assert events[-1].outcome == "failed" + assert events[-1].error_code == "device_scan_target_missing" + + await app.shutdown() + + +async def test_device_scanner_scan_deadline_outcome_depends_on_committed_scan_rows( + tmp_path: Path, +): + app, dev, target = await _make_basic_scan_target(tmp_path) + + async def sleep_forever(*args, **kwargs): + await asyncio.sleep(60) + + with ( + patch("zigpy.device_scanner.SCAN_DEADLINE_S", 0.01), + patch.object( + app.device_scanner, + "_run_scan_body", + new=AsyncMock(side_effect=sleep_forever), + create=True, + ), + ): + failed_summary = await app.device_scanner.scan(dev.ieee) + + assert failed_summary.outcome == "failed" + assert failed_summary.error_code == "scan_deadline_exceeded" + + async def persist_then_sleep(device, *, resume, force_full): + await app._dblistener.upsert_device_scan_progress( + ieee=device.ieee, + endpoint_id=target.endpoint_id, + cluster_type=target.cluster.cluster_type, + cluster_id=target.cluster.cluster_id, + manufacturer_code_scope=None, + attr_discovery_complete=False, + attr_discovery_next_id=1, + attr_reads_complete=False, + cmd_rx_complete=False, + cmd_rx_next_id=0, + cmd_tx_complete=False, + cmd_tx_next_id=0, + last_started=1.0, + last_finished=None, + last_error_code=None, + last_error=None, + last_success=None, + ) + await asyncio.sleep(60) + + with ( + patch("zigpy.device_scanner.SCAN_DEADLINE_S", 0.01), + patch.object( + app.device_scanner, + "_run_scan_body", + new=AsyncMock(side_effect=persist_then_sleep), + create=True, + ), + ): + partial_summary = await app.device_scanner.scan(dev.ieee, force_full=False) + + assert partial_summary.outcome == "partial" + assert partial_summary.error_code == "scan_deadline_exceeded" + + await app.shutdown() + + +async def test_device_scanner_scan_continues_after_terminal_scope_failure( + tmp_path: Path, +): + app, dev, bad_target, good_target = await _make_onoff_scan_targets(tmp_path) + raw_descriptors = zigpy.device_scanner._RawDeviceDescriptors( + node_descriptor=dev.node_desc, + endpoints=(), + ) + + with ( + patch.object( + app.device_scanner, + "_refresh_raw_descriptors", + new=AsyncMock(return_value=raw_descriptors), + ), + patch.object( + app.device_scanner, + "_build_scan_targets_for_node_descriptor", + new=AsyncMock(return_value=(bad_target, good_target)), + ), + patch.object( + app.device_scanner, + "_discover_attributes_for_target", + new=AsyncMock( + side_effect=[ + zigpy.device_scanner._TerminalStepFailure( + "unsupported discovery", + error_code=zigpy.device_scanner.ERROR_CODE_UNSUPPORTED_DISCOVERY_COMMAND, + ), + None, + ] + ), + ) as discover_attrs, + patch.object( + app.device_scanner, + "_read_attributes_for_target", + new=AsyncMock(), + ) as read_attrs, + patch.object( + app.device_scanner, + "_discover_commands_received_for_target", + new=AsyncMock(), + ) as discover_rx, + patch.object( + app.device_scanner, + "_discover_commands_generated_for_target", + new=AsyncMock(), + ) as discover_tx, + ): + summary = await app.device_scanner._run_scan_body( + dev, resume=False, force_full=False + ) + + assert discover_attrs.await_count == 2 + assert read_attrs.await_count == 1 + assert discover_rx.await_count == 1 + assert discover_tx.await_count == 1 + assert summary.outcome == "partial" + assert summary.error_code == "unsupported_discovery_command" + assert summary.last_error == "unsupported discovery" + rows = await app._dblistener.get_device_scan_rows(dev.ieee) + failed_progress = next( + row + for row in rows.progress + if row.endpoint_id == bad_target.endpoint_id + and row.cluster_type == bad_target.cluster.cluster_type + and row.cluster_id == bad_target.cluster.cluster_id + and row.manufacturer_code_scope is None + ) + assert failed_progress.last_error_code == "unsupported_discovery_command" + + await app.shutdown() + + +async def test_device_scanner_scan_treats_scope_timeout_as_transport_failure_not_deadline( + tmp_path: Path, +): + app, dev, bad_target, good_target = await _make_onoff_scan_targets(tmp_path) + raw_descriptors = zigpy.device_scanner._RawDeviceDescriptors( + node_descriptor=dev.node_desc, + endpoints=(), + ) + + with ( + patch.object( + app.device_scanner, + "_refresh_raw_descriptors", + new=AsyncMock(return_value=raw_descriptors), + ), + patch.object( + app.device_scanner, + "_build_scan_targets_for_node_descriptor", + new=AsyncMock(return_value=(bad_target, good_target)), + ), + patch.object( + app.device_scanner, + "_discover_attributes_for_target", + new=AsyncMock(side_effect=[TimeoutError(), None]), + ) as discover_attrs, + patch.object( + app.device_scanner, + "_read_attributes_for_target", + new=AsyncMock(), + ) as read_attrs, + patch.object( + app.device_scanner, + "_discover_commands_received_for_target", + new=AsyncMock(), + ) as discover_rx, + patch.object( + app.device_scanner, + "_discover_commands_generated_for_target", + new=AsyncMock(), + ) as discover_tx, + ): + summary = await app.device_scanner.scan(dev.ieee, resume=False) + + assert discover_attrs.await_count == 2 + assert read_attrs.await_count == 1 + assert discover_rx.await_count == 1 + assert discover_tx.await_count == 1 + assert summary.outcome == "partial" + assert summary.error_code == "transport_failure" + assert summary.last_error == "request timed out" + rows = await app._dblistener.get_device_scan_rows(dev.ieee) + failed_progress = next( + row + for row in rows.progress + if row.endpoint_id == bad_target.endpoint_id + and row.cluster_type == bad_target.cluster.cluster_type + and row.cluster_id == bad_target.cluster.cluster_id + and row.manufacturer_code_scope is None + ) + assert failed_progress.last_error_code == "transport_failure" + assert failed_progress.last_error == "request timed out" + + await app.shutdown() + + +async def test_device_scanner_scan_continues_after_missing_attribute_read_status_records( + tmp_path: Path, +): + app, dev, target, _ = await _make_onoff_scan_targets(tmp_path) + raw_descriptors = zigpy.device_scanner._RawDeviceDescriptors( + node_descriptor=dev.node_desc, + endpoints=(), + ) + await _seed_scan_progress(app, target) + read_rsp = foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Read_Attributes_rsp + ].schema + + async def discover_attributes_for_target( + current_target: zigpy.device_scanner._RawScanTarget, + *, + start_attr_id: int, + ) -> None: + assert current_target is target + assert start_attr_id == 0 + await app._dblistener.upsert_device_scan_progress( + ieee=dev.ieee, + endpoint_id=target.endpoint_id, + cluster_type=target.cluster.cluster_type, + cluster_id=target.cluster.cluster_id, + manufacturer_code_scope=target.scope.manufacturer_code_scope, + attr_discovery_complete=True, + attr_discovery_next_id=4, + attr_reads_complete=False, + cmd_rx_complete=False, + cmd_rx_next_id=0, + cmd_tx_complete=False, + cmd_tx_next_id=0, + last_started=None, + last_finished=None, + last_error_code=None, + last_error=None, + last_success=None, + ) + for attr_def, datatype in ( + (OnOff.AttributeDefs.on_off, foundation.DataTypeId.bool_), + ( + OnOff.AttributeDefs.global_scene_control, + foundation.DataTypeId.bool_, + ), + (OnOff.AttributeDefs.on_time, foundation.DataTypeId.uint16), + (OnOff.AttributeDefs.off_wait_time, foundation.DataTypeId.uint16), + ): + await _seed_discovered_attribute( + app, + target, + attr_id=attr_def.id, + attribute_name=attr_def.name, + datatype=datatype, + access=foundation.AttributeAccessControl.READ, + ) + + async def read_attributes_raw( + attributes, + *args, + manufacturer=None, + **kwargs, + ): + assert manufacturer is None + + if list(attributes) == [0, 16384, 16385]: + return read_rsp( + status_records=[ + foundation.ReadAttributeRecord( + attrid=0, + status=foundation.Status.SUCCESS, + value=foundation.TypeValue( + type=foundation.DataTypeId.bool_, + value=t.Bool.true, + ), + ), + foundation.ReadAttributeRecord( + attrid=16384, + status=foundation.Status.SUCCESS, + value=foundation.TypeValue( + type=foundation.DataTypeId.bool_, + value=t.Bool.false, + ), + ), + ] + ) + + if list(attributes) == [16386]: + return read_rsp( + status_records=[ + foundation.ReadAttributeRecord( + attrid=16386, + status=foundation.Status.SUCCESS, + value=foundation.TypeValue( + type=foundation.DataTypeId.uint16, + value=t.uint16_t(9), + ), + ) + ] + ) + + if list(attributes) == [16385]: + return read_rsp(status_records=[]) + + raise AssertionError(f"Unexpected attribute batch: {attributes!r}") + + target.cluster.read_attributes_raw = AsyncMock(side_effect=read_attributes_raw) + + with ( + patch.object( + app.device_scanner, + "_refresh_raw_descriptors", + new=AsyncMock(return_value=raw_descriptors), + ), + patch.object( + app.device_scanner, + "_build_scan_targets_for_node_descriptor", + new=AsyncMock(return_value=(target,)), + ), + patch.object( + app.device_scanner, + "_discover_attributes_for_target", + new=AsyncMock(side_effect=discover_attributes_for_target), + ), + patch.object( + app.device_scanner, + "_discover_commands_received_for_target", + new=AsyncMock(), + ) as discover_rx, + patch.object( + app.device_scanner, + "_discover_commands_generated_for_target", + new=AsyncMock(), + ) as discover_tx, + patch.object(app.device_scanner, "_pace_requests", new=AsyncMock()), + ): + summary = await app.device_scanner._run_scan_body( + dev, resume=False, force_full=False + ) + + assert summary.outcome == "partial" + assert summary.error_code == "transport_failure" + assert "0x4001" in (summary.last_error or "") + discover_rx.assert_awaited_once() + discover_tx.assert_awaited_once() + + rows = await app._dblistener.get_device_scan_rows(dev.ieee) + failed_progress = next( + row + for row in rows.progress + if row.endpoint_id == target.endpoint_id + and row.cluster_type == target.cluster.cluster_type + and row.cluster_id == target.cluster.cluster_id + and row.manufacturer_code_scope is None + ) + assert failed_progress.last_error_code == "transport_failure" + assert "0x4001" in (failed_progress.last_error or "") + + attrs = {row.attr_id: row for row in rows.attributes} + assert attrs[0].read_complete is True + assert attrs[0].read_status == "success" + assert attrs[16384].read_complete is True + assert attrs[16384].read_status == "success" + assert attrs[16385].read_complete is False + assert attrs[16385].read_status == "transport_failure" + assert attrs[16385].last_error_code == "transport_failure" + assert attrs[16386].read_complete is True + assert attrs[16386].read_status == "success" + + await app.shutdown() + + +async def test_device_scanner_attribute_reads_retry_missing_records_individually( + tmp_path: Path, +): + app, _, target = await _make_basic_scan_target(tmp_path) + read_rsp = foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Read_Attributes_rsp + ].schema + + await _seed_scan_progress(app, target) + for attr_def, datatype in ( + (Basic.AttributeDefs.zcl_version, foundation.DataTypeId.uint8), + (Basic.AttributeDefs.app_version, foundation.DataTypeId.uint8), + (Basic.AttributeDefs.stack_version, foundation.DataTypeId.uint8), + (Basic.AttributeDefs.hw_version, foundation.DataTypeId.uint8), + ): + await _seed_discovered_attribute( + app, + target, + attr_id=attr_def.id, + attribute_name=attr_def.name, + datatype=datatype, + access=foundation.AttributeAccessControl.READ, + ) + + async def read_attributes_raw( + attributes, + *args, + manufacturer=None, + **kwargs, + ): + assert manufacturer is None + + if list(attributes) == [0, 1, 2]: + return read_rsp( + status_records=[ + foundation.ReadAttributeRecord( + attrid=0, + status=foundation.Status.SUCCESS, + value=foundation.TypeValue( + type=foundation.DataTypeId.uint8, + value=t.uint8_t(1), + ), + ), + foundation.ReadAttributeRecord( + attrid=1, + status=foundation.Status.SUCCESS, + value=foundation.TypeValue( + type=foundation.DataTypeId.uint8, + value=t.uint8_t(2), + ), + ), + ] + ) + + if list(attributes) == [2]: + return read_rsp( + status_records=[ + foundation.ReadAttributeRecord( + attrid=2, + status=foundation.Status.SUCCESS, + value=foundation.TypeValue( + type=foundation.DataTypeId.uint8, + value=t.uint8_t(3), + ), + ) + ] + ) + + if list(attributes) == [3]: + return read_rsp( + status_records=[ + foundation.ReadAttributeRecord( + attrid=3, + status=foundation.Status.SUCCESS, + value=foundation.TypeValue( + type=foundation.DataTypeId.uint8, + value=t.uint8_t(4), + ), + ) + ] + ) + + raise AssertionError(f"Unexpected attribute batch: {attributes!r}") + + target.cluster.read_attributes_raw = AsyncMock(side_effect=read_attributes_raw) + + with patch.object(app.device_scanner, "_pace_requests", new=AsyncMock()): + await app.device_scanner._read_attributes_for_target(target) + + assert target.cluster.read_attributes_raw.await_args_list == [ + call([0, 1, 2], manufacturer=None), + call([2], manufacturer=None), + call([3], manufacturer=None), + ] + + rows = await app._dblistener.get_device_scan_rows(target.endpoint.device.ieee) + progress = next( + row + for row in rows.progress + if row.endpoint_id == target.endpoint_id + and row.cluster_type == target.cluster.cluster_type + and row.cluster_id == target.cluster.cluster_id + and row.manufacturer_code_scope is None + ) + assert progress.attr_reads_complete is True + attrs = {row.attr_id: row for row in rows.attributes} + assert attrs[0].read_status == "success" + assert attrs[1].read_status == "success" + assert attrs[2].read_status == "success" + assert attrs[3].read_status == "success" + + await app.shutdown() + + +async def test_device_scanner_scan_ignores_unsupported_command_discovery_response( + tmp_path: Path, +): + app, dev, target, _ = await _make_onoff_scan_targets(tmp_path) + raw_descriptors = zigpy.device_scanner._RawDeviceDescriptors( + node_descriptor=dev.node_desc, + endpoints=(), + ) + cmd_tx_rsp = foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Discover_Commands_Generated_rsp + ].schema + + target.cluster.discover_commands_received = AsyncMock( + return_value=( + foundation.GeneralCommand.Default_Response, + foundation.Status.UNSUP_GENERAL_COMMAND, + ) + ) + target.cluster.discover_commands_generated = AsyncMock( + return_value=cmd_tx_rsp(discovery_complete=True, command_ids=[0x00]) + ) + + with ( + patch.object( + app.device_scanner, + "_refresh_raw_descriptors", + new=AsyncMock(return_value=raw_descriptors), + ), + patch.object( + app.device_scanner, + "_build_scan_targets_for_node_descriptor", + new=AsyncMock(return_value=(target,)), + ), + patch.object( + app.device_scanner, + "_discover_attributes_for_target", + new=AsyncMock(), + ), + patch.object( + app.device_scanner, + "_read_attributes_for_target", + new=AsyncMock(), + ), + patch.object(app.device_scanner, "_pace_requests", new=AsyncMock()), + ): + summary = await app.device_scanner._run_scan_body( + dev, resume=False, force_full=False + ) + + assert summary.outcome == "success" + assert summary.error_code is None + target.cluster.discover_commands_generated.assert_awaited_once() + rows = await app._dblistener.get_device_scan_rows(dev.ieee) + progress = next( + row + for row in rows.progress + if row.endpoint_id == target.endpoint_id + and row.cluster_type == target.cluster.cluster_type + and row.cluster_id == target.cluster.cluster_id + and row.manufacturer_code_scope is None + ) + assert progress.cmd_rx_complete is True + assert progress.cmd_tx_complete is True + assert progress.last_error_code is None + + await app.shutdown() + + +async def test_device_scanner_scan_ignores_unsupported_command_discovery_default_response_object( + tmp_path: Path, +): + app, dev, target, _ = await _make_onoff_scan_targets(tmp_path) + raw_descriptors = zigpy.device_scanner._RawDeviceDescriptors( + node_descriptor=dev.node_desc, + endpoints=(), + ) + cmd_tx_rsp = foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Discover_Commands_Generated_rsp + ].schema + + target.cluster.discover_commands_received = AsyncMock( + return_value=foundation.DefaultResponse( + command_id=foundation.GeneralCommand.Discover_Commands_Received, + status=foundation.Status.UNSUP_GENERAL_COMMAND, + ) + ) + target.cluster.discover_commands_generated = AsyncMock( + return_value=cmd_tx_rsp(discovery_complete=True, command_ids=[0x00]) + ) + + with ( + patch.object( + app.device_scanner, + "_refresh_raw_descriptors", + new=AsyncMock(return_value=raw_descriptors), + ), + patch.object( + app.device_scanner, + "_build_scan_targets_for_node_descriptor", + new=AsyncMock(return_value=(target,)), + ), + patch.object( + app.device_scanner, + "_discover_attributes_for_target", + new=AsyncMock(), + ), + patch.object( + app.device_scanner, + "_read_attributes_for_target", + new=AsyncMock(), + ), + patch.object(app.device_scanner, "_pace_requests", new=AsyncMock()), + ): + summary = await app.device_scanner._run_scan_body( + dev, resume=False, force_full=False + ) + + assert summary.outcome == "success" + assert summary.error_code is None + target.cluster.discover_commands_generated.assert_awaited_once() + rows = await app._dblistener.get_device_scan_rows(dev.ieee) + progress = next( + row + for row in rows.progress + if row.endpoint_id == target.endpoint_id + and row.cluster_type == target.cluster.cluster_type + and row.cluster_id == target.cluster.cluster_id + and row.manufacturer_code_scope is None + ) + assert progress.cmd_rx_complete is True + assert progress.cmd_tx_complete is True + assert progress.last_error_code is None + + await app.shutdown() + + +async def test_device_scanner_scan_treats_non_unsupported_command_discovery_default_response_as_terminal( + tmp_path: Path, +): + app, dev, target, _ = await _make_onoff_scan_targets(tmp_path) + raw_descriptors = zigpy.device_scanner._RawDeviceDescriptors( + node_descriptor=dev.node_desc, + endpoints=(), + ) + + target.cluster.discover_commands_received = AsyncMock( + return_value=( + foundation.GeneralCommand.Default_Response, + foundation.Status.FAILURE, + ) + ) + target.cluster.discover_commands_generated = AsyncMock() + + with ( + patch.object( + app.device_scanner, + "_refresh_raw_descriptors", + new=AsyncMock(return_value=raw_descriptors), + ), + patch.object( + app.device_scanner, + "_build_scan_targets_for_node_descriptor", + new=AsyncMock(return_value=(target,)), + ), + patch.object( + app.device_scanner, + "_discover_attributes_for_target", + new=AsyncMock(), + ), + patch.object( + app.device_scanner, + "_read_attributes_for_target", + new=AsyncMock(), + ), + patch.object(app.device_scanner, "_pace_requests", new=AsyncMock()), + ): + summary = await app.device_scanner._run_scan_body( + dev, resume=False, force_full=False + ) + + assert summary.outcome == "partial" + assert summary.error_code == "unsupported_discovery_command" + assert "failure" in (summary.last_error or "").lower() + target.cluster.discover_commands_generated.assert_not_awaited() + rows = await app._dblistener.get_device_scan_rows(dev.ieee) + progress = next( + row + for row in rows.progress + if row.endpoint_id == target.endpoint_id + and row.cluster_type == target.cluster.cluster_type + and row.cluster_id == target.cluster.cluster_id + and row.manufacturer_code_scope is None + ) + assert progress.last_error_code == "unsupported_discovery_command" + + await app.shutdown() + + +async def test_device_scanner_scan_resume_skips_completed_steps_and_force_full_restarts( + tmp_path: Path, +): + app, dev, target = await _make_basic_scan_target(tmp_path) + raw_descriptors = zigpy.device_scanner._RawDeviceDescriptors( + node_descriptor=dev.node_desc, + endpoints=(), + ) + + await app._dblistener.upsert_device_scan_progress( + ieee=dev.ieee, + endpoint_id=target.endpoint_id, + cluster_type=target.cluster.cluster_type, + cluster_id=target.cluster.cluster_id, + manufacturer_code_scope=None, + attr_discovery_complete=True, + attr_discovery_next_id=3, + attr_reads_complete=True, + cmd_rx_complete=True, + cmd_rx_next_id=2, + cmd_tx_complete=True, + cmd_tx_next_id=1, + last_started=1.0, + last_finished=2.0, + last_error_code=None, + last_error=None, + last_success=2.0, + ) + + with ( + patch.object( + app.device_scanner, + "_refresh_raw_descriptors", + new=AsyncMock(return_value=raw_descriptors), + ), + patch.object( + app.device_scanner, + "_build_scan_targets_for_node_descriptor", + new=AsyncMock(return_value=(target,)), + create=True, + ), + patch.object( + app.device_scanner, + "_discover_attributes_for_target", + new=AsyncMock(), + ) as discover_attrs, + patch.object( + app.device_scanner, + "_read_attributes_for_target", + new=AsyncMock(), + ) as read_attrs, + patch.object( + app.device_scanner, + "_discover_commands_received_for_target", + new=AsyncMock(), + ) as discover_rx, + patch.object( + app.device_scanner, + "_discover_commands_generated_for_target", + new=AsyncMock(), + ) as discover_tx, + ): + resume_summary = await app.device_scanner.scan(dev.ieee, resume=True) + assert resume_summary.used_resume is True + discover_attrs.assert_not_awaited() + read_attrs.assert_not_awaited() + discover_rx.assert_not_awaited() + discover_tx.assert_not_awaited() + + force_summary = await app.device_scanner.scan( + dev.ieee, resume=False, force_full=True + ) + assert force_summary.force_full is True + assert discover_attrs.await_count == 1 + assert read_attrs.await_count == 1 + assert discover_rx.await_count == 1 + assert discover_tx.await_count == 1 + + await app.shutdown() + + +async def test_device_scanner_force_full_clears_existing_scan_rows_before_rerun( + tmp_path: Path, +): + app, dev, target = await _make_basic_scan_target(tmp_path) + raw_descriptors = zigpy.device_scanner._RawDeviceDescriptors( + node_descriptor=dev.node_desc, + endpoints=(), + ) + + await _seed_discovered_attribute( + app, + target, + attr_id=Basic.AttributeDefs.zcl_version.id, + attribute_name=Basic.AttributeDefs.zcl_version.name, + datatype=foundation.DataTypeId.uint8, + access=int(foundation.AttributeAccessControl.READ), + read_complete=True, + read_status="success", + value=b"\x04", + ) + + async def check_rows_cleared(*args, **kwargs): + rows = await app._dblistener.get_device_scan_rows(dev.ieee) + assert rows.attributes == [] + + with ( + patch.object( + app.device_scanner, + "_refresh_raw_descriptors", + new=AsyncMock(return_value=raw_descriptors), + ), + patch.object( + app.device_scanner, + "_build_scan_targets_for_node_descriptor", + new=AsyncMock(return_value=(target,)), + ), + patch.object( + app.device_scanner, + "_discover_attributes_for_target", + new=AsyncMock(side_effect=check_rows_cleared), + ), + patch.object( + app.device_scanner, + "_read_attributes_for_target", + new=AsyncMock(), + ), + patch.object( + app.device_scanner, + "_discover_commands_received_for_target", + new=AsyncMock(), + ), + patch.object( + app.device_scanner, + "_discover_commands_generated_for_target", + new=AsyncMock(), + ), + ): + await app.device_scanner.scan(dev.ieee, resume=False, force_full=True) + + await app.shutdown() + + +async def test_device_scanner_get_snapshot_raises_when_raw_rows_are_missing( + tmp_path: Path, +): + app = await make_app_with_db(tmp_path / "test.db") + ieee = t.EUI64.convert("aa:bb:cc:dd:11:22:33:55") + + with pytest.raises(zigpy.device_scanner.DeviceScanSnapshotNotFoundError): + await app.device_scanner.get_snapshot(ieee) + + await app.shutdown() + + +async def test_device_scanner_scan_preserves_error_code_from_scan_failure( + tmp_path: Path, +): + app = await make_app_with_db(tmp_path / "test.db") + dev = app.add_device( + nwk=0x1234, + ieee=t.EUI64.convert("aa:bb:cc:dd:ee:ff:00:67"), + ) + dev.node_desc = make_node_desc() + ep1 = dev.add_endpoint(1) + ep1.status = zigpy.endpoint.Status.ZDO_INIT + ep1.profile_id = zha.PROFILE_ID + ep1.device_type = zha.DeviceType.PUMP + ep1.add_input_cluster(Basic.cluster_id) + await app._dblistener._save_device(dev) + + with patch.object( + app.device_scanner, + "_run_scan_body", + new=AsyncMock( + side_effect=zigpy.device_scanner._ScanFailure( + "descriptor refresh failed", + error_code=zigpy.device_scanner.ERROR_CODE_DESCRIPTOR_REFRESH_FAILED, + ) + ), + ): + summary = await app.device_scanner.scan(dev.ieee) + + assert summary.outcome == "failed" + assert summary.error_code == "descriptor_refresh_failed" + assert summary.last_error == "descriptor refresh failed" + + await app.shutdown() + + +async def test_device_scanner_get_snapshot_returns_valid_empty_hierarchy( + tmp_path: Path, +): + app = await make_app_with_db(tmp_path / "test.db") + dev = app.add_device( + nwk=0x1234, + ieee=t.EUI64.convert("aa:bb:cc:dd:ee:ff:00:66"), + ) + dev.node_desc = make_node_desc(manufacturer_code=None) + + ep1 = dev.add_endpoint(1) + ep1.status = zigpy.endpoint.Status.ZDO_INIT + ep1.profile_id = zha.PROFILE_ID + ep1.device_type = zha.DeviceType.PUMP + ep1.add_input_cluster(Basic.cluster_id) + + await app._dblistener._save_device(dev) + + snapshot = await app.device_scanner.get_snapshot(dev.ieee) + + assert isinstance(snapshot, zigpy.device_scanner.DeviceScanSnapshot) + assert snapshot.ieee == dev.ieee + assert snapshot.raw_node_descriptor == dev.node_desc + assert len(snapshot.endpoints) == 1 + + endpoint = snapshot.endpoints[0] + assert endpoint.endpoint_id == 1 + assert len(endpoint.clusters) == 1 + + cluster = endpoint.clusters[0] + assert cluster.cluster_id == Basic.cluster_id + assert cluster.standard.progress.status == "pending" + assert cluster.standard.attributes == [] + assert cluster.standard.commands == [] + assert cluster.manufacturer_specific.progress.status == "skipped" + assert ( + cluster.manufacturer_specific.progress.error_code + == "missing_raw_manufacturer_code" + ) + assert cluster.manufacturer_specific.manufacturer_code is None + + await app.shutdown() + + +async def test_device_scanner_get_snapshot_skips_endpoint_242( + tmp_path: Path, +): + app = await make_app_with_db(tmp_path / "test.db") + dev = app.add_device( + nwk=0x1234, + ieee=t.EUI64.convert("aa:bb:cc:dd:ee:ff:00:67"), + ) + dev.node_desc = make_node_desc(manufacturer_code=0x1234) + + ep1 = dev.add_endpoint(1) + ep1.status = zigpy.endpoint.Status.ZDO_INIT + ep1.profile_id = zha.PROFILE_ID + ep1.device_type = zha.DeviceType.PUMP + ep1.add_input_cluster(Basic.cluster_id) + + ep242 = dev.add_endpoint(242) + ep242.status = zigpy.endpoint.Status.ZDO_INIT + ep242.profile_id = 0xA1E0 + ep242.device_type = 0x0061 + ep242.add_output_cluster(0x0021) + + await app._dblistener._save_device(dev) + + snapshot = await app.device_scanner.get_snapshot(dev.ieee) + + assert [endpoint.endpoint_id for endpoint in snapshot.endpoints] == [1] + + await app.shutdown() + + +async def test_device_scanner_get_snapshot_exposes_raw_and_decoded_values_and_ordering( + tmp_path: Path, +): + app, dev, target = await _make_basic_scan_target(tmp_path) + + await app._dblistener.upsert_device_scan_progress( + ieee=dev.ieee, + endpoint_id=target.endpoint_id, + cluster_type=target.cluster.cluster_type, + cluster_id=target.cluster.cluster_id, + manufacturer_code_scope=None, + attr_discovery_complete=True, + attr_discovery_next_id=2, + attr_reads_complete=True, + cmd_rx_complete=True, + cmd_rx_next_id=1, + cmd_tx_complete=False, + cmd_tx_next_id=0, + last_started=2.0, + last_finished=4.0, + last_error_code=None, + last_error=None, + last_success=4.0, + ) + await _seed_discovered_attribute( + app, + target, + attr_id=Basic.AttributeDefs.zcl_version.id, + attribute_name="zcl_version", + datatype=foundation.DataTypeId.uint8, + access=int(foundation.AttributeAccessControl.READ), + read_complete=True, + read_status="success", + value=b"\x04", + ) + await _seed_discovered_attribute( + app, + target, + attr_id=Basic.AttributeDefs.app_version.id, + attribute_name="app_version", + datatype=foundation.DataTypeId.uint8, + access=int(foundation.AttributeAccessControl.READ), + read_complete=True, + read_status="unsupported_attribute", + value=None, + last_error_code="attribute_unsupported", + ) + await app._dblistener.upsert_device_scan_command( + ieee=dev.ieee, + endpoint_id=target.endpoint_id, + cluster_type=target.cluster.cluster_type, + cluster_id=target.cluster.cluster_id, + manufacturer_code_scope=None, + direction="received", + command_id=1, + command_name="reset_to_factory_defaults", + command_schema=None, + discovered_at=3.0, + ) + + snapshot = await app.device_scanner.get_snapshot(dev.ieee) + + cluster = snapshot.endpoints[0].clusters[0] + attrs = cluster.standard.attributes + assert [attr.attr_id for attr in attrs] == [ + Basic.AttributeDefs.zcl_version.id, + Basic.AttributeDefs.app_version.id, + ] + assert attrs[0].datatype == foundation.DataTypeId.uint8 + assert attrs[0].raw_value == b"\x04" + assert attrs[0].decoded_value == 4 + assert attrs[1].raw_value is None + assert attrs[1].decoded_value is None + assert attrs[1].last_error_code == "attribute_unsupported" + assert cluster.standard.commands[0].direction == "received" + assert snapshot.last_snapshot_at is not None + + await app.shutdown() + + +async def test_device_scanner_get_snapshot_uses_max_persisted_timestamp_and_skipped_scope_is_empty( + tmp_path: Path, +): + app = await make_app_with_db(tmp_path / "test.db") + dev = app.add_device( + nwk=0x1234, + ieee=t.EUI64.convert("aa:bb:cc:dd:ee:ff:00:aa"), + ) + dev.node_desc = make_node_desc(manufacturer_code=None) + + ep1 = dev.add_endpoint(1) + ep1.status = zigpy.endpoint.Status.ZDO_INIT + ep1.profile_id = zha.PROFILE_ID + ep1.device_type = zha.DeviceType.PUMP + ep1.add_input_cluster(Basic.cluster_id) + + await app._dblistener._save_device(dev) + target = next( + target + for target in await app.device_scanner._build_raw_scan_targets(dev) + if target.cluster.cluster_id == Basic.cluster_id + ) + await app._dblistener.upsert_device_scan_progress( + ieee=dev.ieee, + endpoint_id=target.endpoint_id, + cluster_type=target.cluster.cluster_type, + cluster_id=target.cluster.cluster_id, + manufacturer_code_scope=None, + attr_discovery_complete=True, + attr_discovery_next_id=1, + attr_reads_complete=False, + cmd_rx_complete=False, + cmd_rx_next_id=0, + cmd_tx_complete=False, + cmd_tx_next_id=0, + last_started=2.0, + last_finished=4.0, + last_error_code=None, + last_error=None, + last_success=4.0, + ) + await _seed_discovered_attribute( + app, + target, + attr_id=Basic.AttributeDefs.zcl_version.id, + attribute_name="zcl_version", + datatype=foundation.DataTypeId.uint8, + access=int(foundation.AttributeAccessControl.READ), + read_complete=True, + read_status="success", + value=b"\x04", + ) + + snapshot = await app.device_scanner.get_snapshot(dev.ieee) + + assert snapshot.last_snapshot_at == datetime.fromtimestamp(4.0, UTC) + skipped_scope = snapshot.endpoints[0].clusters[0].manufacturer_specific + assert skipped_scope.attributes == [] + assert skipped_scope.commands == [] + + await app.shutdown() + + +async def test_device_scanner_snapshot_marks_partial_discovery_as_started( + tmp_path: Path, +): + app, dev, target = await _make_basic_scan_target(tmp_path) + + with patch("zigpy.appdb.datetime") as datetime_mock: + now = datetime(2026, 3, 15, tzinfo=UTC) + datetime_mock.now.return_value = now + datetime_mock.fromtimestamp.side_effect = datetime.fromtimestamp + + await app._dblistener.persist_device_scan_attribute_discovery_page( + ieee=dev.ieee, + endpoint_id=target.endpoint_id, + cluster_type=target.cluster.cluster_type, + cluster_id=target.cluster.cluster_id, + manufacturer_code_scope=None, + attributes=[], + next_attr_id=5, + complete=False, + ) + + snapshot = await app.device_scanner.get_snapshot(dev.ieee) + progress = snapshot.endpoints[0].clusters[0].standard.progress + + assert progress.status == "started" + assert progress.last_finished == now + assert progress.last_success == now + assert snapshot.last_snapshot_at == now + + await app.shutdown() + + +async def test_device_scanner_resume_uses_stored_discovery_cursors(tmp_path: Path): + app, dev, target, _ = await _make_onoff_scan_targets(tmp_path) + raw_descriptors = zigpy.device_scanner._RawDeviceDescriptors( + node_descriptor=dev.node_desc, + endpoints=(), + ) + + await app._dblistener.upsert_device_scan_progress( + ieee=dev.ieee, + endpoint_id=target.endpoint_id, + cluster_type=target.cluster.cluster_type, + cluster_id=target.cluster.cluster_id, + manufacturer_code_scope=None, + attr_discovery_complete=False, + attr_discovery_next_id=3, + attr_reads_complete=True, + cmd_rx_complete=False, + cmd_rx_next_id=7, + cmd_tx_complete=False, + cmd_tx_next_id=9, + last_started=1.0, + last_finished=None, + last_error_code=None, + last_error=None, + last_success=None, + ) + + discover_rsp = foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Discover_Attribute_Extended_rsp + ].schema + cmd_rx_rsp = foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Discover_Commands_Received_rsp + ].schema + cmd_tx_rsp = foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Discover_Commands_Generated_rsp + ].schema + + target.cluster.discover_attributes_extended = AsyncMock( + return_value=discover_rsp(discovery_complete=True, extended_attr_info=[]) + ) + target.cluster.discover_commands_received = AsyncMock( + return_value=cmd_rx_rsp(discovery_complete=True, command_ids=[]) + ) + target.cluster.discover_commands_generated = AsyncMock( + return_value=cmd_tx_rsp(discovery_complete=True, command_ids=[]) + ) + + with ( + patch.object( + app.device_scanner, + "_refresh_raw_descriptors", + new=AsyncMock(return_value=raw_descriptors), + ), + patch.object( + app.device_scanner, + "_build_scan_targets_for_node_descriptor", + new=AsyncMock(return_value=(target,)), + ), + patch.object( + app.device_scanner, + "_read_attributes_for_target", + new=AsyncMock(), + ), + patch.object(app.device_scanner, "_pace_requests", new=AsyncMock()), + ): + await app.device_scanner._run_scan_body(dev, resume=True, force_full=False) + + assert target.cluster.discover_attributes_extended.await_args_list[0].args == ( + 3, + 16, + ) + assert target.cluster.discover_commands_received.await_args_list[0].args == (7, 16) + assert target.cluster.discover_commands_generated.await_args_list[0].args == (9, 16) + + await app.shutdown() + + +async def test_device_scanner_attribute_discovery_keeps_first_page_when_later_page_write_fails( + tmp_path: Path, +): + app, dev, target = await _make_basic_scan_target(tmp_path) + discover_rsp = foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Discover_Attribute_Extended_rsp + ].schema + target.cluster.discover_attributes_extended = AsyncMock( + side_effect=[ + discover_rsp( + discovery_complete=False, + extended_attr_info=[ + foundation.DiscoverAttributesExtendedResponseRecord( + attrid=0x0000, + datatype=foundation.DataTypeId.uint8, + acl=foundation.AttributeAccessControl.READ, + ) + ], + ), + discover_rsp( + discovery_complete=True, + extended_attr_info=[ + foundation.DiscoverAttributesExtendedResponseRecord( + attrid=0x0001, + datatype=foundation.DataTypeId.uint8, + acl=foundation.AttributeAccessControl.READ, + ) + ], + ), + ] + ) + + original_persist = app._dblistener.persist_device_scan_attribute_discovery_page + calls = 0 + + async def persist_then_fail(*args, **kwargs): + nonlocal calls + calls += 1 + if calls == 2: + raise RuntimeError("page write failed") + return await original_persist(*args, **kwargs) + + with patch.object( + app._dblistener, + "persist_device_scan_attribute_discovery_page", + new=AsyncMock(side_effect=persist_then_fail), + ): + with pytest.raises(RuntimeError): + await app.device_scanner._discover_attributes_for_target(target) + + rows = await app._dblistener.get_device_scan_rows(dev.ieee) + assert [row.attr_id for row in rows.attributes] == [0x0000] + assert rows.progress[0].attr_discovery_next_id == 1 + assert rows.progress[0].attr_discovery_complete is False + + await app.shutdown() + + +async def test_device_scanner_attribute_reads_do_not_requery_pending_rows_during_split_fallback( + tmp_path: Path, +): + app, dev, target = await _make_basic_scan_target(tmp_path) + await app._dblistener.upsert_device_scan_progress( + ieee=dev.ieee, + endpoint_id=1, + cluster_type=ClusterType.Server, + cluster_id=Basic.cluster_id, + manufacturer_code_scope=None, + attr_discovery_complete=True, + attr_discovery_next_id=2, + attr_reads_complete=False, + cmd_rx_complete=False, + cmd_rx_next_id=0, + cmd_tx_complete=False, + cmd_tx_next_id=0, + last_started=None, + last_finished=None, + last_error_code=None, + last_error=None, + last_success=None, + ) + await _seed_discovered_attribute( + app, + target, + attr_id=Basic.AttributeDefs.zcl_version.id, + attribute_name=Basic.AttributeDefs.zcl_version.name, + datatype=foundation.DataTypeId.uint8, + access=foundation.AttributeAccessControl.READ, + ) + await _seed_discovered_attribute( + app, + target, + attr_id=Basic.AttributeDefs.app_version.id, + attribute_name=Basic.AttributeDefs.app_version.name, + datatype=foundation.DataTypeId.uint8, + access=foundation.AttributeAccessControl.READ, + ) + + read_rsp = foundation.GENERAL_COMMANDS[ + foundation.GeneralCommand.Read_Attributes_rsp + ].schema + + async def read_attributes_raw(attributes, *args, manufacturer=None, **kwargs): + if list(attributes) == [0, 1]: + raise zigpy.exceptions.DeliveryError("boom") + return read_rsp( + status_records=[ + foundation.ReadAttributeRecord( + attrid=attributes[0], + status=foundation.Status.SUCCESS, + value=foundation.TypeValue( + type=foundation.DataTypeId.uint8, + value=t.uint8_t(4), + ), + ) + ] + ) + + target.cluster.read_attributes_raw = AsyncMock(side_effect=read_attributes_raw) + + original_get_pending = app._dblistener.get_pending_device_scan_attributes + + with patch.object( + app._dblistener, + "get_pending_device_scan_attributes", + new=AsyncMock(wraps=original_get_pending), + ) as get_pending: + await app.device_scanner._read_attributes_for_target(target) + + assert get_pending.await_count == 2 + + await app.shutdown() + + +async def test_device_scanner_scan_emits_step_cardinality_and_missing_manufacturer_skip( + tmp_path: Path, +): + app = await make_app_with_db(tmp_path / "test.db") + dev = app.add_device( + nwk=0x1234, + ieee=t.EUI64.convert("aa:bb:cc:dd:ee:ff:00:77"), + ) + dev.node_desc = make_node_desc(manufacturer_code=None) + + ep1 = dev.add_endpoint(1) + ep1.status = zigpy.endpoint.Status.ZDO_INIT + ep1.profile_id = zha.PROFILE_ID + ep1.device_type = zha.DeviceType.PUMP + ep1.add_input_cluster(Basic.cluster_id) + + await app._dblistener._save_device(dev) + + events = [] + + class Listener: + def scan_queued(self, event): + events.append(("scan_queued", event)) + + def scan_started(self, event): + events.append(("scan_started", event)) + + def step_started(self, event): + events.append(("step_started", event)) + + def step_finished(self, event): + events.append(("step_finished", event)) + + def scan_finished(self, event): + events.append(("scan_finished", event)) + + app.device_scanner.add_listener(Listener()) + + async def mock_active_ep_req(nwk): + return [zdo_t.Status.SUCCESS, None, [1]] + + async def mock_simple_desc_req(nwk, endpoint_id): + sd = zdo_t.SimpleDescriptor() + sd.endpoint = endpoint_id + sd.profile = zha.PROFILE_ID + sd.device_type = zha.DeviceType.PUMP + sd.input_clusters = [Basic.cluster_id] + sd.output_clusters = [] + return [zdo_t.Status.SUCCESS, None, sd] + + dev.zdo.Node_Desc_req = AsyncMock( + return_value=( + zdo_t.Status.SUCCESS, + dev.nwk, + make_node_desc(manufacturer_code=None), + ) + ) + dev.zdo.Active_EP_req = AsyncMock(side_effect=mock_active_ep_req) + dev.zdo.Simple_Desc_req = AsyncMock(side_effect=mock_simple_desc_req) + + with ( + patch.object( + app.device_scanner, + "_discover_attributes_for_target", + new=AsyncMock(), + ), + patch.object( + app.device_scanner, + "_read_attributes_for_target", + new=AsyncMock(), + ), + patch.object( + app.device_scanner, + "_discover_commands_received_for_target", + new=AsyncMock(), + ), + patch.object( + app.device_scanner, + "_discover_commands_generated_for_target", + new=AsyncMock(), + ), + patch.object(app.device_scanner, "_pace_requests", new=AsyncMock()), + ): + summary = await app.device_scanner.scan(dev.ieee, resume=False) + + assert [name for name, _ in events[:2]] == ["scan_queued", "scan_started"] + assert events[-1][0] == "scan_finished" + assert events[-1][1].outcome == "success" + assert summary.outcome == "success" + + descriptor_events = [ + event for _, event in events if event.step == "descriptor_refresh" + ] + assert descriptor_events + assert all(event.endpoint_id is None for event in descriptor_events) + assert all(event.cluster_id is None for event in descriptor_events) + + attr_events = [event for _, event in events if event.step == "attribute_discovery"] + assert any( + event.status == "started" and event.endpoint_id == 1 for event in attr_events + ) + assert any( + event.status == "skipped" + and event.error_code == "missing_raw_manufacturer_code" + for event in attr_events + ) + + await app.shutdown() + + +async def test_device_scanner_scan_ignores_live_manufacturer_override_when_raw_code_missing( + tmp_path: Path, +): + app = await make_app_with_db(tmp_path / "test.db") + dev = app.add_device( + nwk=0x1234, + ieee=t.EUI64.convert("aa:bb:cc:dd:ee:ff:00:88"), + ) + dev.node_desc = make_node_desc(manufacturer_code=None) + dev.manufacturer_id_override = 0x9999 + + ep1 = dev.add_endpoint(1) + ep1.status = zigpy.endpoint.Status.ZDO_INIT + ep1.profile_id = zha.PROFILE_ID + ep1.device_type = zha.DeviceType.PUMP + ep1.add_input_cluster(Basic.cluster_id) + + await app._dblistener._save_device(dev) + + raw_descriptors = zigpy.device_scanner._RawDeviceDescriptors( + node_descriptor=dev.node_desc, + endpoints=(), + ) + target = next( + target + for target in await app.device_scanner._build_raw_scan_targets(dev) + if target.cluster.cluster_id == Basic.cluster_id + ) + events = [] + + class Listener: + def step_finished(self, event): + events.append(event) + + app.device_scanner.add_listener(Listener()) + + with ( + patch.object( + app.device_scanner, + "_refresh_raw_descriptors", + new=AsyncMock(return_value=raw_descriptors), + ), + patch.object( + app.device_scanner, + "_build_scan_targets_for_node_descriptor", + new=AsyncMock(return_value=(target,)), + ), + patch.object( + app.device_scanner, + "_discover_attributes_for_target", + new=AsyncMock(), + ), + patch.object( + app.device_scanner, + "_read_attributes_for_target", + new=AsyncMock(), + ), + patch.object( + app.device_scanner, + "_discover_commands_received_for_target", + new=AsyncMock(), + ), + patch.object( + app.device_scanner, + "_discover_commands_generated_for_target", + new=AsyncMock(), + ), + ): + await app.device_scanner._run_scan_body(dev, resume=False, force_full=False) + + assert any( + event.status == "skipped" + and event.error_code == "missing_raw_manufacturer_code" + and event.manufacturer_code_scope is None + for event in events + ) + + await app.shutdown() diff --git a/zigpy/appdb.py b/zigpy/appdb.py index 45987cbf2..821877159 100644 --- a/zigpy/appdb.py +++ b/zigpy/appdb.py @@ -2,7 +2,7 @@ import asyncio import contextlib -from datetime import UTC, datetime, timedelta +from datetime import datetime, timedelta, timezone import json import logging import re @@ -34,6 +34,7 @@ AttributeUpdatedEvent, AttributeWrittenEvent, ClusterType, + foundation, ) from zigpy.zcl.clusters.general import Basic from zigpy.zcl.foundation import Status @@ -45,6 +46,7 @@ MIN_SQLITE_VERSION = (3, 24, 0) +UTC_TZ = timezone(timedelta(0)) if sqlite3.sqlite_version_info < MIN_SQLITE_VERSION: raise RuntimeError( @@ -54,10 +56,10 @@ LOGGER = logging.getLogger(__name__) -DB_VERSION = 14 +DB_VERSION = 15 DB_V = f"_v{DB_VERSION}" -UNIX_EPOCH = datetime.fromtimestamp(0, tz=UTC) +UNIX_EPOCH = datetime.fromtimestamp(0, tz=UTC_TZ) DB_V_REGEX = re.compile(r"(?:_v\d+)?$") MIN_UPDATE_DELTA = timedelta(seconds=30).total_seconds() @@ -84,6 +86,83 @@ class AttributeCacheRow(NamedTuple): last_updated: float +class DeviceScanProgressRow(NamedTuple): + ieee: t.EUI64 + endpoint_id: int + cluster_type: ClusterType + cluster_id: int + manufacturer_code_scope: int | None + attr_discovery_complete: bool + attr_discovery_next_id: int + attr_reads_complete: bool + cmd_rx_complete: bool + cmd_rx_next_id: int + cmd_tx_complete: bool + cmd_tx_next_id: int + last_started: float | None + last_finished: float | None + last_error_code: str | None + last_error: str | None + last_success: float | None + + +class DeviceScanAttributeRow(NamedTuple): + ieee: t.EUI64 + endpoint_id: int + cluster_type: ClusterType + cluster_id: int + manufacturer_code_scope: int | None + attr_id: int + attribute_name: str | None + datatype: int | None + access: int | None + discovered_at: float + read_complete: bool + read_status: str | None + value: bytes | None + last_read: float | None + last_error_code: str | None + last_error: str | None + + +class DeviceScanCommandRow(NamedTuple): + ieee: t.EUI64 + endpoint_id: int + cluster_type: ClusterType + cluster_id: int + manufacturer_code_scope: int | None + direction: str + command_id: int + command_name: str | None + command_schema: str | None + discovered_at: float + + +class DeviceScanRows(NamedTuple): + progress: list[DeviceScanProgressRow] + attributes: list[DeviceScanAttributeRow] + commands: list[DeviceScanCommandRow] + + +class RawEndpointRow(NamedTuple): + endpoint_id: int + profile_id: int + device_type: int + status: int + + +class RawClusterRow(NamedTuple): + endpoint_id: int + cluster_type: int + cluster_id: int + + +class RawTopologyRows(NamedTuple): + node_descriptor: zdo_t.NodeDescriptor | None + endpoints: list[RawEndpointRow] + clusters: list[RawClusterRow] + + def _register_sqlite_adapters(): def adapt_ieee(eui64): return str(eui64) @@ -515,7 +594,7 @@ async def _save_unsupported_attributes(self, ep: Endpoint) -> None: manufacturer_code, Status.UNSUPPORTED_ATTRIBUTE, None, - datetime.now(UTC).timestamp(), + datetime.now(UTC_TZ).timestamp(), ) for cluster in ep.clusters for (attrid, manufacturer_code) in cluster._attr_cache._unsupported @@ -569,7 +648,7 @@ async def _save_attribute( "manufacturer_code": event.manufacturer_code, "status": Status.SUCCESS, "value": event.value, - "timestamp": datetime.now(UTC).timestamp(), + "timestamp": datetime.now(UTC_TZ).timestamp(), "min_update_delta": MIN_UPDATE_DELTA, }, ) @@ -626,7 +705,7 @@ async def _unsupported_attribute_added( "manufacturer_code": event.manufacturer_code, "status": Status.UNSUPPORTED_ATTRIBUTE, "value": None, - "timestamp": datetime.now(UTC).timestamp(), + "timestamp": datetime.now(UTC_TZ).timestamp(), }, ) await self._db.commit() @@ -666,6 +745,1020 @@ async def _read_all_attributes( ) as cursor: return [AttributeCacheRow(*row) for row in await cursor.fetchall()] + async def get_raw_topology_rows(self, ieee: t.EUI64) -> RawTopologyRows: + async with self.execute( + f""" + SELECT logical_type, complex_descriptor_available, user_descriptor_available, + reserved, aps_flags, frequency_band, mac_capability_flags, + manufacturer_code, maximum_buffer_size, + maximum_incoming_transfer_size, server_mask, + maximum_outgoing_transfer_size, descriptor_capability_field + FROM node_descriptors{DB_V} + WHERE ieee = ? + """, + (ieee,), + ) as cursor: + row = await cursor.fetchone() + node_descriptor = zdo_t.NodeDescriptor(*row) if row is not None else None + + async with self.execute( + f""" + SELECT endpoint_id, profile_id, device_type, status + FROM endpoints{DB_V} + WHERE ieee = ? + ORDER BY endpoint_id + """, + (ieee,), + ) as cursor: + endpoints = [RawEndpointRow(*row) for row in await cursor.fetchall()] + + async with self.execute( + f""" + SELECT endpoint_id, cluster_type, cluster_id + FROM clusters{DB_V} + WHERE ieee = ? + ORDER BY endpoint_id, cluster_type, cluster_id + """, + (ieee,), + ) as cursor: + clusters = [RawClusterRow(*row) for row in await cursor.fetchall()] + + return RawTopologyRows( + node_descriptor=node_descriptor, + endpoints=endpoints, + clusters=clusters, + ) + + def _build_valid_scan_scope_keys( + self, + *, + endpoints: tuple[zigpy.endpoint.DiscoveredEndpointDescriptor, ...], + manufacturer_code: int | None, + ) -> set[tuple[int, int, int, int | None]]: + valid_scope_keys: set[tuple[int, int, int, int | None]] = set() + + for endpoint in endpoints: + if ( + endpoint.status == EndpointStatus.ENDPOINT_INACTIVE + or endpoint.profile_id is None + or endpoint.device_type is None + ): + continue + + for cluster_id in endpoint.input_clusters: + valid_scope_keys.add( + ( + endpoint.endpoint_id, + int(ClusterType.Server), + int(cluster_id), + None, + ) + ) + + if manufacturer_code is not None: + valid_scope_keys.add( + ( + endpoint.endpoint_id, + int(ClusterType.Server), + int(cluster_id), + manufacturer_code, + ) + ) + + for cluster_id in endpoint.output_clusters: + valid_scope_keys.add( + ( + endpoint.endpoint_id, + int(ClusterType.Client), + int(cluster_id), + None, + ) + ) + + if manufacturer_code is not None: + valid_scope_keys.add( + ( + endpoint.endpoint_id, + int(ClusterType.Client), + int(cluster_id), + manufacturer_code, + ) + ) + + return valid_scope_keys + + async def _clear_invalid_device_scan_scope_rows( + self, + ieee: t.EUI64, + valid_scope_keys: set[tuple[int, int, int, int | None]], + ) -> None: + async with self.execute( + f""" + SELECT endpoint_id, cluster_type, cluster_id, manufacturer_code_scope + FROM device_scan_progress{DB_V} + WHERE ieee = ? + UNION + SELECT endpoint_id, cluster_type, cluster_id, manufacturer_code_scope + FROM device_scan_attributes{DB_V} + WHERE ieee = ? + UNION + SELECT endpoint_id, cluster_type, cluster_id, manufacturer_code_scope + FROM device_scan_commands{DB_V} + WHERE ieee = ? + """, + (ieee, ieee, ieee), + ) as cursor: + existing_scope_keys = { + (endpoint_id, cluster_type, cluster_id, manufacturer_code_scope) + for endpoint_id, cluster_type, cluster_id, manufacturer_code_scope in ( + await cursor.fetchall() + ) + } + + stale_scope_keys = existing_scope_keys - valid_scope_keys + + if not stale_scope_keys: + return + + delete_rows = [ + ( + ieee, + endpoint_id, + cluster_type, + cluster_id, + -2 if manufacturer_code_scope is None else manufacturer_code_scope, + ) + for endpoint_id, cluster_type, cluster_id, manufacturer_code_scope in ( + stale_scope_keys + ) + ] + + for table_name in ( + f"device_scan_progress{DB_V}", + f"device_scan_attributes{DB_V}", + f"device_scan_commands{DB_V}", + ): + await self._db.executemany( + f""" + DELETE FROM {table_name} + WHERE ieee = ? + AND endpoint_id = ? + AND cluster_type = ? + AND cluster_id = ? + AND manufacturer_code_scope_idx = ? + """, + delete_rows, + ) + + async def replace_device_raw_descriptors( + self, + device: Device, + *, + node_descriptor: zdo_t.NodeDescriptor, + endpoints: tuple[zigpy.endpoint.DiscoveredEndpointDescriptor, ...], + ) -> None: + endpoint_rows = [ + ( + device.ieee, + endpoint.endpoint_id, + endpoint.profile_id, + endpoint.device_type, + endpoint.status, + ) + for endpoint in endpoints + if endpoint.status != EndpointStatus.ENDPOINT_INACTIVE + and endpoint.profile_id is not None + and endpoint.device_type is not None + ] + cluster_rows = [ + ( + device.ieee, + endpoint.endpoint_id, + cluster_type, + cluster_id, + ) + for endpoint in endpoints + if endpoint.status != EndpointStatus.ENDPOINT_INACTIVE + and endpoint.profile_id is not None + and endpoint.device_type is not None + for cluster_type, cluster_ids in ( + (ClusterType.Server, endpoint.input_clusters), + (ClusterType.Client, endpoint.output_clusters), + ) + for cluster_id in cluster_ids + ] + valid_scope_keys = self._build_valid_scan_scope_keys( + endpoints=endpoints, + manufacturer_code=node_descriptor.manufacturer_code, + ) + + try: + await self.execute("BEGIN") + await self.execute( + f"""INSERT INTO devices{DB_V} (ieee, nwk, status, last_seen) + VALUES (?, ?, ?, ?) + ON CONFLICT (ieee) + DO UPDATE SET + nwk=excluded.nwk, + status=excluded.status, + last_seen=excluded.last_seen""", + ( + device.ieee, + device.nwk, + device.status, + (device._last_seen or UNIX_EPOCH).timestamp(), + ), + ) + await self.execute( + f"""INSERT INTO node_descriptors{DB_V} + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT (ieee) + DO UPDATE SET + logical_type=excluded.logical_type, + complex_descriptor_available=excluded.complex_descriptor_available, + user_descriptor_available=excluded.user_descriptor_available, + reserved=excluded.reserved, + aps_flags=excluded.aps_flags, + frequency_band=excluded.frequency_band, + mac_capability_flags=excluded.mac_capability_flags, + manufacturer_code=excluded.manufacturer_code, + maximum_buffer_size=excluded.maximum_buffer_size, + maximum_incoming_transfer_size=excluded.maximum_incoming_transfer_size, + server_mask=excluded.server_mask, + maximum_outgoing_transfer_size=excluded.maximum_outgoing_transfer_size, + descriptor_capability_field=excluded.descriptor_capability_field""", + (device.ieee, *node_descriptor.as_tuple()), + ) + if endpoint_rows: + await self._db.executemany( + f"""INSERT INTO endpoints{DB_V} VALUES (?, ?, ?, ?, ?) + ON CONFLICT (ieee, endpoint_id) + DO UPDATE SET + profile_id=excluded.profile_id, + device_type=excluded.device_type, + status=excluded.status""", + endpoint_rows, + ) + + await self.execute( + f"DELETE FROM clusters{DB_V} WHERE ieee = ?", (device.ieee,) + ) + + if cluster_rows: + await self._db.executemany( + f"""INSERT INTO clusters{DB_V} VALUES (?, ?, ?, ?) + ON CONFLICT (ieee, endpoint_id, cluster_type, cluster_id) + DO NOTHING""", + cluster_rows, + ) + + endpoint_ids = [endpoint_id for _, endpoint_id, *_ in endpoint_rows] + + if endpoint_ids: + placeholders = ",".join("?" for _ in endpoint_ids) + await self.execute( + f""" + DELETE FROM endpoints{DB_V} + WHERE ieee = ? + AND endpoint_id NOT IN ({placeholders}) + """, + (device.ieee, *endpoint_ids), + ) + else: + await self.execute( + f"DELETE FROM endpoints{DB_V} WHERE ieee = ?", (device.ieee,) + ) + + await self._clear_invalid_device_scan_scope_rows( + device.ieee, valid_scope_keys + ) + await self._db.commit() + except Exception: # noqa: BLE001 + await self._db.rollback() + raise + + async def persist_device_scan_attribute_discovery_page( + self, + *, + ieee: t.EUI64, + endpoint_id: int, + cluster_type: ClusterType, + cluster_id: int, + manufacturer_code_scope: int | None, + attributes: list[tuple[int, str | None, int, int | None]], + next_attr_id: int, + complete: bool, + ) -> None: + discovered_at = datetime.now(UTC_TZ).timestamp() + + try: + await self.execute("BEGIN") + + if attributes: + await self._db.executemany( + f""" + INSERT INTO device_scan_attributes{DB_V} ( + ieee, endpoint_id, cluster_type, cluster_id, + manufacturer_code_scope, attr_id, attribute_name, datatype, + access, discovered_at, read_complete, read_status, value, + last_read, last_error_code, last_error + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, NULL, NULL, NULL, NULL, NULL) + ON CONFLICT ( + ieee, endpoint_id, cluster_type, cluster_id, + manufacturer_code_scope_idx, attr_id + ) DO UPDATE SET + attribute_name=excluded.attribute_name, + datatype=excluded.datatype, + access=excluded.access, + discovered_at=excluded.discovered_at + """, + [ + ( + ieee, + endpoint_id, + cluster_type, + cluster_id, + manufacturer_code_scope, + attr_id, + attribute_name, + datatype, + access, + discovered_at, + ) + for attr_id, attribute_name, datatype, access in attributes + ], + ) + + await self.execute( + f""" + INSERT INTO device_scan_progress{DB_V} ( + ieee, endpoint_id, cluster_type, cluster_id, + manufacturer_code_scope, attr_discovery_complete, + attr_discovery_next_id, attr_reads_complete, cmd_rx_complete, + cmd_rx_next_id, cmd_tx_complete, cmd_tx_next_id, last_started, + last_finished, last_error_code, last_error, last_success + ) + VALUES (?, ?, ?, ?, ?, ?, ?, 0, 0, 0, 0, 0, ?, ?, NULL, NULL, ?) + ON CONFLICT ( + ieee, endpoint_id, cluster_type, cluster_id, + manufacturer_code_scope_idx + ) DO UPDATE SET + attr_discovery_complete=excluded.attr_discovery_complete, + attr_discovery_next_id=excluded.attr_discovery_next_id, + last_started=COALESCE(last_started, excluded.last_started), + last_finished=excluded.last_finished, + last_error_code=NULL, + last_error=NULL, + last_success=excluded.last_success + """, + ( + ieee, + endpoint_id, + cluster_type, + cluster_id, + manufacturer_code_scope, + complete, + next_attr_id, + discovered_at, + discovered_at, + discovered_at, + ), + ) + + await self._db.commit() + except Exception: # noqa: BLE001 + await self._db.rollback() + raise + + async def get_pending_device_scan_attributes( + self, + *, + ieee: t.EUI64, + endpoint_id: int, + cluster_type: ClusterType, + cluster_id: int, + manufacturer_code_scope: int | None, + ) -> list[DeviceScanAttributeRow]: + async with self.execute( + f""" + SELECT ieee, endpoint_id, cluster_type, cluster_id, manufacturer_code_scope, + attr_id, attribute_name, datatype, access, discovered_at, + read_complete, read_status, value, last_read, last_error_code, + last_error + FROM device_scan_attributes{DB_V} + WHERE ieee = ? + AND endpoint_id = ? + AND cluster_type = ? + AND cluster_id = ? + AND manufacturer_code_scope_idx = ? + AND read_complete = 0 + AND (access IS NULL OR (access & ?) != 0) + ORDER BY attr_id + """, + ( + ieee, + endpoint_id, + cluster_type, + cluster_id, + -2 if manufacturer_code_scope is None else manufacturer_code_scope, + int(foundation.AttributeAccessControl.READ), + ), + ) as cursor: + return [DeviceScanAttributeRow(*row) for row in await cursor.fetchall()] + + async def persist_device_scan_attribute_read_results( + self, + *, + ieee: t.EUI64, + endpoint_id: int, + cluster_type: ClusterType, + cluster_id: int, + manufacturer_code_scope: int | None, + results: list[ + tuple[ + int, + int | None, + bool, + str | None, + bytes | None, + float | None, + str | None, + str | None, + ] + ], + ) -> None: + if not results: + return + + try: + await self.execute("BEGIN") + await self._db.executemany( + f""" + UPDATE device_scan_attributes{DB_V} + SET datatype=COALESCE(?, datatype), + read_complete=?, + read_status=?, + value=?, + last_read=?, + last_error_code=?, + last_error=? + WHERE ieee = ? + AND endpoint_id = ? + AND cluster_type = ? + AND cluster_id = ? + AND manufacturer_code_scope_idx = ? + AND attr_id = ? + """, + [ + ( + datatype, + read_complete, + read_status, + value, + last_read, + last_error_code, + last_error, + ieee, + endpoint_id, + cluster_type, + cluster_id, + -2 + if manufacturer_code_scope is None + else manufacturer_code_scope, + attr_id, + ) + for ( + attr_id, + datatype, + read_complete, + read_status, + value, + last_read, + last_error_code, + last_error, + ) in results + ], + ) + await self._db.commit() + except Exception: # noqa: BLE001 + await self._db.rollback() + raise + + async def set_device_scan_attr_reads_complete( + self, + *, + ieee: t.EUI64, + endpoint_id: int, + cluster_type: ClusterType, + cluster_id: int, + manufacturer_code_scope: int | None, + complete: bool, + ) -> None: + completed_at = datetime.now(UTC_TZ).timestamp() + + await self.execute( + f""" + INSERT INTO device_scan_progress{DB_V} ( + ieee, endpoint_id, cluster_type, cluster_id, + manufacturer_code_scope, attr_discovery_complete, + attr_discovery_next_id, attr_reads_complete, cmd_rx_complete, + cmd_rx_next_id, cmd_tx_complete, cmd_tx_next_id, last_started, + last_finished, last_error_code, last_error, last_success + ) + VALUES (?, ?, ?, ?, ?, 0, 0, ?, 0, 0, 0, 0, ?, ?, NULL, NULL, ?) + ON CONFLICT ( + ieee, endpoint_id, cluster_type, cluster_id, + manufacturer_code_scope_idx + ) DO UPDATE SET + attr_reads_complete=excluded.attr_reads_complete, + last_started=COALESCE(last_started, excluded.last_started), + last_finished=excluded.last_finished, + last_error_code=NULL, + last_error=NULL, + last_success=excluded.last_success + """, + ( + ieee, + endpoint_id, + cluster_type, + cluster_id, + manufacturer_code_scope, + complete, + completed_at, + completed_at, + completed_at, + ), + ) + await self._db.commit() + + async def persist_device_scan_command_discovery_page( + self, + *, + ieee: t.EUI64, + endpoint_id: int, + cluster_type: ClusterType, + cluster_id: int, + manufacturer_code_scope: int | None, + direction: str, + commands: list[tuple[int, str | None, str | None]], + next_command_id: int, + complete: bool, + ) -> None: + discovered_at = datetime.now(UTC_TZ).timestamp() + complete_column = ( + "cmd_rx_complete" if direction == "received" else "cmd_tx_complete" + ) + next_column = "cmd_rx_next_id" if direction == "received" else "cmd_tx_next_id" + + try: + await self.execute("BEGIN") + + if commands: + await self._db.executemany( + f""" + INSERT INTO device_scan_commands{DB_V} ( + ieee, endpoint_id, cluster_type, cluster_id, + manufacturer_code_scope, direction, command_id, + command_name, command_schema, discovered_at + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT ( + ieee, endpoint_id, cluster_type, cluster_id, + manufacturer_code_scope_idx, direction, command_id + ) DO UPDATE SET + command_name=excluded.command_name, + command_schema=excluded.command_schema, + discovered_at=excluded.discovered_at + """, + [ + ( + ieee, + endpoint_id, + cluster_type, + cluster_id, + manufacturer_code_scope, + direction, + command_id, + command_name, + command_schema, + discovered_at, + ) + for command_id, command_name, command_schema in commands + ], + ) + + await self.execute( + f""" + INSERT INTO device_scan_progress{DB_V} ( + ieee, endpoint_id, cluster_type, cluster_id, + manufacturer_code_scope, attr_discovery_complete, + attr_discovery_next_id, attr_reads_complete, cmd_rx_complete, + cmd_rx_next_id, cmd_tx_complete, cmd_tx_next_id, last_started, + last_finished, last_error_code, last_error, last_success + ) + VALUES (?, ?, ?, ?, ?, 0, 0, 0, ?, ?, ?, ?, ?, ?, NULL, NULL, ?) + ON CONFLICT ( + ieee, endpoint_id, cluster_type, cluster_id, + manufacturer_code_scope_idx + ) DO UPDATE SET + {complete_column}=excluded.{complete_column}, + {next_column}=excluded.{next_column}, + last_started=COALESCE(last_started, excluded.last_started), + last_finished=excluded.last_finished, + last_error_code=NULL, + last_error=NULL, + last_success=excluded.last_success + """, + ( + ieee, + endpoint_id, + cluster_type, + cluster_id, + manufacturer_code_scope, + complete if direction == "received" else False, + next_command_id if direction == "received" else 0, + complete if direction == "generated" else False, + next_command_id if direction == "generated" else 0, + discovered_at, + discovered_at, + discovered_at, + ), + ) + + await self._db.commit() + except Exception: # noqa: BLE001 + await self._db.rollback() + raise + + async def set_device_scan_progress_error( + self, + *, + ieee: t.EUI64, + endpoint_id: int, + cluster_type: ClusterType, + cluster_id: int, + manufacturer_code_scope: int | None, + error_code: str, + error: str, + ) -> None: + failed_at = datetime.now(UTC_TZ).timestamp() + + await self.execute( + f""" + INSERT INTO device_scan_progress{DB_V} ( + ieee, endpoint_id, cluster_type, cluster_id, + manufacturer_code_scope, attr_discovery_complete, + attr_discovery_next_id, attr_reads_complete, cmd_rx_complete, + cmd_rx_next_id, cmd_tx_complete, cmd_tx_next_id, last_started, + last_finished, last_error_code, last_error, last_success + ) + VALUES (?, ?, ?, ?, ?, 0, 0, 0, 0, 0, 0, 0, ?, ?, ?, ?, NULL) + ON CONFLICT ( + ieee, endpoint_id, cluster_type, cluster_id, + manufacturer_code_scope_idx + ) DO UPDATE SET + last_started=COALESCE(last_started, excluded.last_started), + last_finished=excluded.last_finished, + last_error_code=excluded.last_error_code, + last_error=excluded.last_error + """, + ( + ieee, + endpoint_id, + cluster_type, + cluster_id, + manufacturer_code_scope, + failed_at, + failed_at, + error_code, + error, + ), + ) + await self._db.commit() + + async def upsert_device_scan_progress( + self, + *, + ieee: t.EUI64, + endpoint_id: int, + cluster_type: ClusterType, + cluster_id: int, + manufacturer_code_scope: int | None, + attr_discovery_complete: bool, + attr_discovery_next_id: int, + attr_reads_complete: bool, + cmd_rx_complete: bool, + cmd_rx_next_id: int, + cmd_tx_complete: bool, + cmd_tx_next_id: int, + last_started: float | None, + last_finished: float | None, + last_error_code: str | None, + last_error: str | None, + last_success: float | None, + ) -> None: + await self.execute( + f""" + INSERT INTO device_scan_progress{DB_V} ( + ieee, endpoint_id, cluster_type, cluster_id, manufacturer_code_scope, + attr_discovery_complete, attr_discovery_next_id, attr_reads_complete, + cmd_rx_complete, cmd_rx_next_id, cmd_tx_complete, cmd_tx_next_id, + last_started, last_finished, last_error_code, last_error, last_success + ) + VALUES ( + :ieee, :endpoint_id, :cluster_type, :cluster_id, :manufacturer_code_scope, + :attr_discovery_complete, :attr_discovery_next_id, :attr_reads_complete, + :cmd_rx_complete, :cmd_rx_next_id, :cmd_tx_complete, :cmd_tx_next_id, + :last_started, :last_finished, :last_error_code, :last_error, :last_success + ) + ON CONFLICT ( + ieee, endpoint_id, cluster_type, cluster_id, manufacturer_code_scope_idx + ) DO UPDATE SET + attr_discovery_complete=excluded.attr_discovery_complete, + attr_discovery_next_id=excluded.attr_discovery_next_id, + attr_reads_complete=excluded.attr_reads_complete, + cmd_rx_complete=excluded.cmd_rx_complete, + cmd_rx_next_id=excluded.cmd_rx_next_id, + cmd_tx_complete=excluded.cmd_tx_complete, + cmd_tx_next_id=excluded.cmd_tx_next_id, + last_started=excluded.last_started, + last_finished=excluded.last_finished, + last_error_code=excluded.last_error_code, + last_error=excluded.last_error, + last_success=excluded.last_success + """, + { + "ieee": ieee, + "endpoint_id": endpoint_id, + "cluster_type": cluster_type, + "cluster_id": cluster_id, + "manufacturer_code_scope": manufacturer_code_scope, + "attr_discovery_complete": attr_discovery_complete, + "attr_discovery_next_id": attr_discovery_next_id, + "attr_reads_complete": attr_reads_complete, + "cmd_rx_complete": cmd_rx_complete, + "cmd_rx_next_id": cmd_rx_next_id, + "cmd_tx_complete": cmd_tx_complete, + "cmd_tx_next_id": cmd_tx_next_id, + "last_started": last_started, + "last_finished": last_finished, + "last_error_code": last_error_code, + "last_error": last_error, + "last_success": last_success, + }, + ) + await self._db.commit() + + async def upsert_device_scan_attribute( + self, + *, + ieee: t.EUI64, + endpoint_id: int, + cluster_type: ClusterType, + cluster_id: int, + manufacturer_code_scope: int | None, + attr_id: int, + attribute_name: str | None, + datatype: int | None, + access: int | None, + discovered_at: float, + read_complete: bool, + read_status: str | None, + value: bytes | None, + last_read: float | None, + last_error_code: str | None, + last_error: str | None, + ) -> None: + await self.execute( + f""" + INSERT INTO device_scan_attributes{DB_V} ( + ieee, endpoint_id, cluster_type, cluster_id, manufacturer_code_scope, + attr_id, attribute_name, datatype, access, discovered_at, read_complete, + read_status, value, last_read, last_error_code, last_error + ) + VALUES ( + :ieee, :endpoint_id, :cluster_type, :cluster_id, :manufacturer_code_scope, + :attr_id, :attribute_name, :datatype, :access, :discovered_at, + :read_complete, :read_status, :value, :last_read, :last_error_code, + :last_error + ) + ON CONFLICT ( + ieee, endpoint_id, cluster_type, cluster_id, manufacturer_code_scope_idx, attr_id + ) DO UPDATE SET + attribute_name=excluded.attribute_name, + datatype=excluded.datatype, + access=excluded.access, + discovered_at=excluded.discovered_at, + read_complete=excluded.read_complete, + read_status=excluded.read_status, + value=excluded.value, + last_read=excluded.last_read, + last_error_code=excluded.last_error_code, + last_error=excluded.last_error + """, + { + "ieee": ieee, + "endpoint_id": endpoint_id, + "cluster_type": cluster_type, + "cluster_id": cluster_id, + "manufacturer_code_scope": manufacturer_code_scope, + "attr_id": attr_id, + "attribute_name": attribute_name, + "datatype": datatype, + "access": access, + "discovered_at": discovered_at, + "read_complete": read_complete, + "read_status": read_status, + "value": value, + "last_read": last_read, + "last_error_code": last_error_code, + "last_error": last_error, + }, + ) + await self._db.commit() + + async def upsert_device_scan_command( + self, + *, + ieee: t.EUI64, + endpoint_id: int, + cluster_type: ClusterType, + cluster_id: int, + manufacturer_code_scope: int | None, + direction: str, + command_id: int, + command_name: str | None, + command_schema: str | None, + discovered_at: float, + ) -> None: + await self.execute( + f""" + INSERT INTO device_scan_commands{DB_V} ( + ieee, endpoint_id, cluster_type, cluster_id, manufacturer_code_scope, + direction, command_id, command_name, command_schema, discovered_at + ) + VALUES ( + :ieee, :endpoint_id, :cluster_type, :cluster_id, :manufacturer_code_scope, + :direction, :command_id, :command_name, :command_schema, :discovered_at + ) + ON CONFLICT ( + ieee, endpoint_id, cluster_type, cluster_id, manufacturer_code_scope_idx, direction, command_id + ) DO UPDATE SET + command_name=excluded.command_name, + command_schema=excluded.command_schema, + discovered_at=excluded.discovered_at + """, + { + "ieee": ieee, + "endpoint_id": endpoint_id, + "cluster_type": cluster_type, + "cluster_id": cluster_id, + "manufacturer_code_scope": manufacturer_code_scope, + "direction": direction, + "command_id": command_id, + "command_name": command_name, + "command_schema": command_schema, + "discovered_at": discovered_at, + }, + ) + await self._db.commit() + + async def clear_device_scan_data(self, ieee: t.EUI64) -> None: + await self.execute( + f"DELETE FROM device_scan_progress{DB_V} WHERE ieee = ?", (ieee,) + ) + await self.execute( + f"DELETE FROM device_scan_attributes{DB_V} WHERE ieee = ?", (ieee,) + ) + await self.execute( + f"DELETE FROM device_scan_commands{DB_V} WHERE ieee = ?", (ieee,) + ) + await self._db.commit() + + async def get_device_scan_rows(self, ieee: t.EUI64) -> DeviceScanRows: + async with self.execute( + f""" + SELECT ieee, endpoint_id, cluster_type, cluster_id, manufacturer_code_scope, + attr_discovery_complete, attr_discovery_next_id, attr_reads_complete, + cmd_rx_complete, cmd_rx_next_id, cmd_tx_complete, cmd_tx_next_id, + last_started, last_finished, last_error_code, last_error, last_success + FROM device_scan_progress{DB_V} + WHERE ieee = ? + ORDER BY endpoint_id, cluster_type, cluster_id, manufacturer_code_scope_idx + """, + (ieee,), + ) as cursor: + progress = [ + DeviceScanProgressRow( + ieee, + endpoint_id, + cluster_type, + cluster_id, + manufacturer_code_scope, + bool(attr_discovery_complete), + attr_discovery_next_id, + bool(attr_reads_complete), + bool(cmd_rx_complete), + cmd_rx_next_id, + bool(cmd_tx_complete), + cmd_tx_next_id, + last_started, + last_finished, + last_error_code, + last_error, + last_success, + ) + for ( + ieee, + endpoint_id, + cluster_type, + cluster_id, + manufacturer_code_scope, + attr_discovery_complete, + attr_discovery_next_id, + attr_reads_complete, + cmd_rx_complete, + cmd_rx_next_id, + cmd_tx_complete, + cmd_tx_next_id, + last_started, + last_finished, + last_error_code, + last_error, + last_success, + ) in (await cursor.fetchall()) + ] + + async with self.execute( + f""" + SELECT ieee, endpoint_id, cluster_type, cluster_id, manufacturer_code_scope, + attr_id, attribute_name, datatype, access, discovered_at, + read_complete, read_status, value, last_read, last_error_code, + last_error + FROM device_scan_attributes{DB_V} + WHERE ieee = ? + ORDER BY endpoint_id, cluster_type, cluster_id, manufacturer_code_scope_idx, attr_id + """, + (ieee,), + ) as cursor: + attributes = [ + DeviceScanAttributeRow( + ieee, + endpoint_id, + cluster_type, + cluster_id, + manufacturer_code_scope, + attr_id, + attribute_name, + datatype, + access, + discovered_at, + bool(read_complete), + read_status, + value, + last_read, + last_error_code, + last_error, + ) + for ( + ieee, + endpoint_id, + cluster_type, + cluster_id, + manufacturer_code_scope, + attr_id, + attribute_name, + datatype, + access, + discovered_at, + read_complete, + read_status, + value, + last_read, + last_error_code, + last_error, + ) in (await cursor.fetchall()) + ] + + async with self.execute( + f""" + SELECT ieee, endpoint_id, cluster_type, cluster_id, manufacturer_code_scope, + direction, command_id, command_name, command_schema, discovered_at + FROM device_scan_commands{DB_V} + WHERE ieee = ? + ORDER BY endpoint_id, cluster_type, cluster_id, manufacturer_code_scope_idx, direction, command_id + """, + (ieee,), + ) as cursor: + commands = [DeviceScanCommandRow(*row) for row in await cursor.fetchall()] + + return DeviceScanRows( + progress=progress, + attributes=attributes, + commands=commands, + ) + async def load(self) -> None: LOGGER.debug("Loading application state") await self._load_devices() @@ -842,7 +1935,7 @@ async def _populate_attribute_cache( cluster._attr_cache.set_legacy_value( row.attr_id, row.value, - last_updated=datetime.fromtimestamp(row.last_updated, UTC), + last_updated=datetime.fromtimestamp(row.last_updated, UTC_TZ), ) continue @@ -851,7 +1944,7 @@ async def _populate_attribute_cache( cluster._attr_cache.set_value( attr_def, row.value, - last_updated=datetime.fromtimestamp(row.last_updated, UTC), + last_updated=datetime.fromtimestamp(row.last_updated, UTC_TZ), ) else: cluster._attr_cache.mark_unsupported(attr_def) @@ -1105,6 +2198,7 @@ async def _run_migrations(self) -> bool: (self._migrate_to_v12, 12), (self._migrate_to_v13, 13), (self._migrate_to_v14, 14), + (self._migrate_to_v15, 15), ]: if db_version >= min(to_db_version, DB_VERSION): continue @@ -1515,7 +2609,7 @@ async def _migrate_to_v14(self) -> None: UNMIGRATED_MANUFACTURER_CODE, Status.UNSUPPORTED_ATTRIBUTE, None, - datetime.fromtimestamp(0, UTC).timestamp(), + datetime.fromtimestamp(0, UTC_TZ).timestamp(), ), ) @@ -1546,3 +2640,35 @@ async def _migrate_to_v14(self) -> None: last_updated, ), ) + + async def _migrate_to_v15(self) -> None: + """Schema v15 adds raw device scan tables.""" + + await self._migrate_tables( + { + "devices_v14": "devices_v15", + "endpoints_v14": "endpoints_v15", + "clusters_v14": "clusters_v15", + "attributes_cache_v14": None, + "neighbors_v14": "neighbors_v15", + "routes_v14": "routes_v15", + "node_descriptors_v14": "node_descriptors_v15", + "groups_v14": "groups_v15", + "group_members_v14": "group_members_v15", + "relays_v14": "relays_v15", + "network_backups_v14": "network_backups_v15", + } + ) + + await self.execute( + """ + INSERT INTO attributes_cache_v15 ( + ieee, endpoint_id, cluster_type, cluster_id, attr_id, + manufacturer_code, status, value, last_updated + ) + SELECT + ieee, endpoint_id, cluster_type, cluster_id, attr_id, + manufacturer_code, status, value, last_updated + FROM attributes_cache_v14 + """ + ) diff --git a/zigpy/appdb_schemas/schema_v15.sql b/zigpy/appdb_schemas/schema_v15.sql new file mode 100644 index 000000000..0ba669103 --- /dev/null +++ b/zigpy/appdb_schemas/schema_v15.sql @@ -0,0 +1,295 @@ +PRAGMA user_version = 15; + +-- devices +DROP TABLE IF EXISTS devices_v15; +CREATE TABLE devices_v15 ( + ieee ieee NOT NULL, + nwk INTEGER NOT NULL, + status INTEGER NOT NULL, + last_seen REAL NOT NULL +); + +CREATE UNIQUE INDEX devices_idx_v15 + ON devices_v15(ieee); + + +-- endpoints +DROP TABLE IF EXISTS endpoints_v15; +CREATE TABLE endpoints_v15 ( + ieee ieee NOT NULL, + endpoint_id INTEGER NOT NULL, + profile_id INTEGER NOT NULL, + device_type INTEGER NOT NULL, + status INTEGER NOT NULL, + + FOREIGN KEY(ieee) + REFERENCES devices_v15(ieee) + ON DELETE CASCADE +); + +CREATE UNIQUE INDEX endpoint_idx_v15 + ON endpoints_v15(ieee, endpoint_id); + + +-- clusters +DROP TABLE IF EXISTS clusters_v15; +CREATE TABLE clusters_v15 ( + ieee ieee NOT NULL, + endpoint_id INTEGER NOT NULL, + cluster_type INTEGER NOT NULL, + cluster_id INTEGER NOT NULL, + + FOREIGN KEY(ieee, endpoint_id) + REFERENCES endpoints_v15(ieee, endpoint_id) + ON DELETE CASCADE +); + +CREATE UNIQUE INDEX clusters_idx_v15 + ON clusters_v15(ieee, endpoint_id, cluster_type, cluster_id); + + +-- attributes +DROP TABLE IF EXISTS attributes_cache_v15; +CREATE TABLE attributes_cache_v15 ( + ieee ieee NOT NULL, + endpoint_id INTEGER NOT NULL, + cluster_type INTEGER NOT NULL, + cluster_id INTEGER NOT NULL, + attr_id INTEGER NOT NULL, + manufacturer_code INTEGER, + -- NULL is not considered equal to itself in unique indexes, so we need a non-NULL + -- column to use in the unique index + manufacturer_code_idx INTEGER NOT NULL GENERATED ALWAYS AS (IFNULL(manufacturer_code, -2)) STORED, + status INTEGER, + value BLOB, + last_updated REAL NOT NULL, + + -- Quirks can create "virtual" clusters and endpoints that won't be present in the + -- DB but whose values still need to be cached + FOREIGN KEY(ieee) + REFERENCES devices_v15(ieee) + ON DELETE CASCADE +); + +CREATE UNIQUE INDEX attributes_cache_idx_v15 + ON attributes_cache_v15(ieee, endpoint_id, cluster_type, cluster_id, attr_id, manufacturer_code_idx); + + +-- neighbors +DROP TABLE IF EXISTS neighbors_v15; +CREATE TABLE neighbors_v15 ( + device_ieee ieee NOT NULL, + extended_pan_id ieee NOT NULL, + ieee ieee NOT NULL, + nwk INTEGER NOT NULL, + device_type INTEGER NOT NULL, + rx_on_when_idle INTEGER NOT NULL, + relationship INTEGER NOT NULL, + reserved1 INTEGER NOT NULL, + permit_joining INTEGER NOT NULL, + reserved2 INTEGER NOT NULL, + depth INTEGER NOT NULL, + lqi INTEGER NOT NULL, + + FOREIGN KEY(device_ieee) + REFERENCES devices_v15(ieee) + ON DELETE CASCADE +); + +CREATE INDEX neighbors_idx_v15 + ON neighbors_v15(device_ieee); + + +-- routes +DROP TABLE IF EXISTS routes_v15; +CREATE TABLE routes_v15 ( + device_ieee ieee NOT NULL, + dst_nwk INTEGER NOT NULL, + route_status INTEGER NOT NULL, + memory_constrained INTEGER NOT NULL, + many_to_one INTEGER NOT NULL, + route_record_required INTEGER NOT NULL, + reserved INTEGER NOT NULL, + next_hop INTEGER NOT NULL +); + +CREATE INDEX routes_idx_v15 + ON routes_v15(device_ieee); + + +-- node descriptors +DROP TABLE IF EXISTS node_descriptors_v15; +CREATE TABLE node_descriptors_v15 ( + ieee ieee NOT NULL, + + logical_type INTEGER NOT NULL, + complex_descriptor_available INTEGER NOT NULL, + user_descriptor_available INTEGER NOT NULL, + reserved INTEGER NOT NULL, + aps_flags INTEGER NOT NULL, + frequency_band INTEGER NOT NULL, + mac_capability_flags INTEGER NOT NULL, + manufacturer_code INTEGER, + maximum_buffer_size INTEGER NOT NULL, + maximum_incoming_transfer_size INTEGER NOT NULL, + server_mask INTEGER NOT NULL, + maximum_outgoing_transfer_size INTEGER NOT NULL, + descriptor_capability_field INTEGER NOT NULL, + + FOREIGN KEY(ieee) + REFERENCES devices_v15(ieee) + ON DELETE CASCADE +); + +CREATE UNIQUE INDEX node_descriptors_idx_v15 + ON node_descriptors_v15(ieee); + + +-- groups +DROP TABLE IF EXISTS groups_v15; +CREATE TABLE groups_v15 ( + group_id INTEGER NOT NULL, + name TEXT NOT NULL +); + +CREATE UNIQUE INDEX groups_idx_v15 + ON groups_v15(group_id); + + +-- group members +DROP TABLE IF EXISTS group_members_v15; +CREATE TABLE group_members_v15 ( + group_id INTEGER NOT NULL, + ieee ieee NOT NULL, + endpoint_id INTEGER NOT NULL, + + FOREIGN KEY(group_id) + REFERENCES groups_v15(group_id) + ON DELETE CASCADE, + FOREIGN KEY(ieee, endpoint_id) + REFERENCES endpoints_v15(ieee, endpoint_id) + ON DELETE CASCADE +); + +CREATE UNIQUE INDEX group_members_idx_v15 + ON group_members_v15(group_id, ieee, endpoint_id); + + +-- relays +DROP TABLE IF EXISTS relays_v15; +CREATE TABLE relays_v15 ( + ieee ieee NOT NULL, + relays BLOB NOT NULL, + + FOREIGN KEY(ieee) + REFERENCES devices_v15(ieee) + ON DELETE CASCADE +); + +CREATE UNIQUE INDEX relays_idx_v15 + ON relays_v15(ieee); + + +-- device scan progress +DROP TABLE IF EXISTS device_scan_progress_v15; +CREATE TABLE device_scan_progress_v15 ( + ieee ieee NOT NULL, + endpoint_id INTEGER NOT NULL, + cluster_type INTEGER NOT NULL, + cluster_id INTEGER NOT NULL, + manufacturer_code_scope INTEGER, + manufacturer_code_scope_idx INTEGER NOT NULL GENERATED ALWAYS AS (IFNULL(manufacturer_code_scope, -2)) STORED, + attr_discovery_complete INTEGER NOT NULL, + attr_discovery_next_id INTEGER NOT NULL, + attr_reads_complete INTEGER NOT NULL, + cmd_rx_complete INTEGER NOT NULL, + cmd_rx_next_id INTEGER NOT NULL, + cmd_tx_complete INTEGER NOT NULL, + cmd_tx_next_id INTEGER NOT NULL, + last_started REAL, + last_finished REAL, + last_error_code TEXT, + last_error TEXT, + last_success REAL, + + FOREIGN KEY(ieee) + REFERENCES devices_v15(ieee) + ON DELETE CASCADE +); + +CREATE UNIQUE INDEX device_scan_progress_idx_v15 + ON device_scan_progress_v15(ieee, endpoint_id, cluster_type, cluster_id, manufacturer_code_scope_idx); + +CREATE INDEX idx_device_scan_progress_v15_ieee + ON device_scan_progress_v15(ieee); + + +-- device scan attributes +DROP TABLE IF EXISTS device_scan_attributes_v15; +CREATE TABLE device_scan_attributes_v15 ( + ieee ieee NOT NULL, + endpoint_id INTEGER NOT NULL, + cluster_type INTEGER NOT NULL, + cluster_id INTEGER NOT NULL, + manufacturer_code_scope INTEGER, + manufacturer_code_scope_idx INTEGER NOT NULL GENERATED ALWAYS AS (IFNULL(manufacturer_code_scope, -2)) STORED, + attr_id INTEGER NOT NULL, + attribute_name TEXT, + datatype INTEGER, + access INTEGER, + discovered_at REAL NOT NULL, + read_complete INTEGER NOT NULL, + read_status TEXT, + value BLOB, + last_read REAL, + last_error_code TEXT, + last_error TEXT, + + FOREIGN KEY(ieee) + REFERENCES devices_v15(ieee) + ON DELETE CASCADE +); + +CREATE UNIQUE INDEX device_scan_attributes_idx_v15 + ON device_scan_attributes_v15(ieee, endpoint_id, cluster_type, cluster_id, manufacturer_code_scope_idx, attr_id); + +CREATE INDEX idx_device_scan_attributes_v15_ieee + ON device_scan_attributes_v15(ieee); + +CREATE INDEX idx_device_scan_attributes_v15_pending_reads + ON device_scan_attributes_v15(ieee, endpoint_id, cluster_type, cluster_id, manufacturer_code_scope_idx, read_complete, attr_id); + + +-- device scan commands +DROP TABLE IF EXISTS device_scan_commands_v15; +CREATE TABLE device_scan_commands_v15 ( + ieee ieee NOT NULL, + endpoint_id INTEGER NOT NULL, + cluster_type INTEGER NOT NULL, + cluster_id INTEGER NOT NULL, + manufacturer_code_scope INTEGER, + manufacturer_code_scope_idx INTEGER NOT NULL GENERATED ALWAYS AS (IFNULL(manufacturer_code_scope, -2)) STORED, + direction TEXT NOT NULL, + command_id INTEGER NOT NULL, + command_name TEXT, + command_schema TEXT, + discovered_at REAL NOT NULL, + + FOREIGN KEY(ieee) + REFERENCES devices_v15(ieee) + ON DELETE CASCADE +); + +CREATE UNIQUE INDEX device_scan_commands_idx_v15 + ON device_scan_commands_v15(ieee, endpoint_id, cluster_type, cluster_id, manufacturer_code_scope_idx, direction, command_id); + +CREATE INDEX idx_device_scan_commands_v15_ieee + ON device_scan_commands_v15(ieee); + + +-- network backups +DROP TABLE IF EXISTS network_backups_v15; +CREATE TABLE network_backups_v15 ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + backup_json TEXT NOT NULL +); diff --git a/zigpy/application.py b/zigpy/application.py index 5e4be1c23..98d9bbdc8 100644 --- a/zigpy/application.py +++ b/zigpy/application.py @@ -24,6 +24,7 @@ from zigpy.const import INTERFERENCE_MESSAGE from zigpy.datastructures import RequestLimiter import zigpy.device +import zigpy.device_scanner import zigpy.endpoint import zigpy.exceptions import zigpy.group @@ -65,7 +66,7 @@ def __init__(self, config: dict) -> None: self.state: zigpy.state.State = zigpy.state.State() self._listeners = {} self._config = self.SCHEMA(config) - self._dblistener = None + self._dblistener: zigpy.appdb.PersistingListener | None = None self._groups = zigpy.group.Groups(self) self._send_sequence = 0 self._tasks: set[asyncio.Future[Any]] = set() @@ -80,6 +81,9 @@ def __init__(self, config: dict) -> None: self.ota = zigpy.ota.OTA(self._config[conf.CONF_OTA], self) self.backups: zigpy.backups.BackupManager = zigpy.backups.BackupManager(self) self.topology: zigpy.topology.Topology = zigpy.topology.Topology(self) + self.device_scanner: zigpy.device_scanner.DeviceScanner = ( + zigpy.device_scanner.DeviceScanner(self) + ) self._req_listeners: collections.defaultdict[ zigpy.device.Device | zigpy.listeners.AnyDeviceType, diff --git a/zigpy/device.py b/zigpy/device.py index dff8200ec..f23475ec4 100644 --- a/zigpy/device.py +++ b/zigpy/device.py @@ -292,6 +292,15 @@ def schedule_initialize(self) -> asyncio.Task | None: return self._initialize_task async def get_node_descriptor(self) -> zdo_t.NodeDescriptor: + self.node_desc = await self.discover_node_descriptor(refresh=True) + return self.node_desc + + async def discover_node_descriptor( + self, *, refresh: bool = False + ) -> zdo_t.NodeDescriptor: + if self.node_desc is not None and not refresh: + return self.node_desc + self.info("Requesting 'Node Descriptor'") status, _, node_desc = await self.zdo.Node_Desc_req(self.nwk) @@ -301,11 +310,25 @@ async def get_node_descriptor(self) -> zdo_t.NodeDescriptor: f"Requesting Node Descriptor failed: {status}" ) - self.node_desc = node_desc self.info("Got Node Descriptor: %s", node_desc) return node_desc + async def discover_active_endpoints(self, *, refresh: bool = False) -> list[int]: + if self.has_non_zdo_endpoints and not refresh: + self.info("Already have endpoints: %s", self.endpoints) + return [ep.endpoint_id for ep in self.non_zdo_endpoints] + + self.info("Discovering endpoints") + + status, _, endpoints = await self.zdo.Active_EP_req(self.nwk) + + if status != zdo_t.Status.SUCCESS: + raise zigpy.exceptions.InvalidResponse(f"Endpoint request failed: {status}") + + self.info("Discovered endpoints: %s", endpoints) + return [endpoint_id for endpoint_id in endpoints if endpoint_id != 0] + async def initialize(self) -> None: try: # Perform initialization with critical priority @@ -430,26 +453,11 @@ async def _initialize(self) -> None: # Some devices are improperly initialized and are missing a node descriptor if self.node_desc is None: - await self.get_node_descriptor() + self.node_desc = await self.discover_node_descriptor() - # Devices should have endpoints other than ZDO - if self.has_non_zdo_endpoints: - self.info("Already have endpoints: %s", self.endpoints) - else: - self.info("Discovering endpoints") - - status, _, endpoints = await self.zdo.Active_EP_req(self.nwk) - - if status != zdo_t.Status.SUCCESS: - raise zigpy.exceptions.InvalidResponse( - f"Endpoint request failed: {status}" - ) - - self.info("Discovered endpoints: %s", endpoints) - - for endpoint_id in endpoints: - if endpoint_id != 0: - self.add_endpoint(endpoint_id) + for endpoint_id in await self.discover_active_endpoints(): + if endpoint_id not in self.endpoints: + self.add_endpoint(endpoint_id) self.status = Status.ZDO_INIT @@ -758,6 +766,53 @@ def _match_packet_endpoint_cluster( else: return endpoint, zcl_cluster + def _get_direction_mismatch_response_key( + self, + rsp_key: ResponseKey, + hdr: zdo_t.ZDOHeader | foundation.ZCLHeader, + zcl_cluster: Cluster | None, + cmd: foundation.CommandSchema | list[typing.Any] | None, + ) -> ResponseKey | None: + """Return an alternate response key for packets with the wrong ZCL direction.""" + if not isinstance(hdr, foundation.ZCLHeader): + return None + + if ( + hdr.frame_control.frame_type != foundation.FrameType.GLOBAL_COMMAND + or cmd is None + ): + return None + + try: + general_command = foundation.GeneralCommand(hdr.command_id) + except ValueError: + return None + + if ( + general_command != foundation.GeneralCommand.Default_Response + and not general_command.name.endswith("_rsp") + ): + return None + + if zcl_cluster is None or rsp_key.direction is None: + return None + + expected_cluster_type = ( + ClusterType.Client + if hdr.frame_control.direction == foundation.Direction.Client_to_Server + else ClusterType.Server + ) + + if zcl_cluster.cluster_type == expected_cluster_type: + return None + + return ResponseKey( + endpoint_id=rsp_key.endpoint_id, + cluster_id=rsp_key.cluster_id, + direction=rsp_key.direction.flip(), + tsn=rsp_key.tsn, + ) + def _parse_packet_command( self, packet: t.ZigbeePacket, endpoint: typing.Any, zcl_cluster: Cluster | None ) -> typing.Any: @@ -844,6 +899,14 @@ def packet_received(self, packet: t.ZigbeePacket) -> None: if self._maybe_match_response(rsp_key, cmd, error): return + alt_rsp_key = self._get_direction_mismatch_response_key( + rsp_key, hdr, zcl_cluster, cmd + ) + if alt_rsp_key is not None and self._maybe_match_response( + alt_rsp_key, cmd, error + ): + return + # Skip further processing if there was a parsing error if error is not None: return diff --git a/zigpy/device_scanner.py b/zigpy/device_scanner.py new file mode 100644 index 000000000..64cc16cdc --- /dev/null +++ b/zigpy/device_scanner.py @@ -0,0 +1,1962 @@ +from __future__ import annotations + +import asyncio +import collections +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +import functools +import logging +import typing + +import zigpy.datastructures +import zigpy.device +import zigpy.endpoint +import zigpy.exceptions +import zigpy.profiles +import zigpy.types as t +import zigpy.util +import zigpy.zcl +from zigpy.zcl import ClusterType, foundation +import zigpy.zdo.types as zdo_t + +if typing.TYPE_CHECKING: + from collections.abc import Awaitable, Mapping + + import zigpy.appdb + import zigpy.application + from zigpy.zcl import Cluster + + asyncio_timeout: typing.Callable[[float | None], asyncio.Timeout] +else: + from asyncio import timeout as asyncio_timeout + + +LOGGER = logging.getLogger(__name__) + + +ScopeEventPayload: typing.TypeAlias = dict[str, int | ClusterType | None] +SnapshotRowValue: typing.TypeAlias = t.EUI64 | int | float | str | bytes | None +_DefaultResponseTuple: typing.TypeAlias = tuple[ + foundation.GeneralCommand, foundation.Status | int +] +_DefaultResponseResult: typing.TypeAlias = ( + foundation.DefaultResponse | _DefaultResponseTuple +) + + +class _AttributeValueDeserializer(typing.Protocol): + @classmethod + def deserialize(cls, data: bytes) -> tuple[object, bytes]: ... + + +class _StandardAttributeDiscoveryResponse(typing.Protocol): + discovery_complete: bool + attribute_info: list[foundation.DiscoverAttributesResponseRecord] + + +class _ExtendedAttributeDiscoveryResponse(typing.Protocol): + discovery_complete: bool + extended_attr_info: list[foundation.DiscoverAttributesExtendedResponseRecord] + + +_AttributeDiscoveryResponse: typing.TypeAlias = ( + _StandardAttributeDiscoveryResponse | _ExtendedAttributeDiscoveryResponse +) + + +class _CommandDiscoveryResponse(typing.Protocol): + discovery_complete: bool + command_ids: list[t.uint8_t] + + +class _ReadAttributesRawResponse(typing.Protocol): + status_records: list[foundation.ReadAttributeRecord] + + +class _DiscoverCommandsCallable(typing.Protocol): + def __call__( + self, + start_command_id: int, + max_command_ids: int, + *, + manufacturer: int | None = None, + ) -> Awaitable[_CommandDiscoveryResponse | _DefaultResponseResult]: ... + + +SCAN_EVENT_NAMES = ( + "scan_queued", + "scan_started", + "step_started", + "step_finished", + "scan_finished", +) + +SCAN_EVENT_QUEUED = SCAN_EVENT_NAMES[0] +SCAN_EVENT_STARTED = SCAN_EVENT_NAMES[1] +SCAN_EVENT_STEP_STARTED = SCAN_EVENT_NAMES[2] +SCAN_EVENT_STEP_FINISHED = SCAN_EVENT_NAMES[3] +SCAN_EVENT_FINISHED = SCAN_EVENT_NAMES[4] + +SCAN_STATUSES = ( + "queued", + "started", + "success", + "failed", + "skipped", +) + +SCAN_STATUS_QUEUED = SCAN_STATUSES[0] +SCAN_STATUS_STARTED = SCAN_STATUSES[1] +SCAN_STATUS_SUCCESS = SCAN_STATUSES[2] +SCAN_STATUS_FAILED = SCAN_STATUSES[3] +SCAN_STATUS_SKIPPED = SCAN_STATUSES[4] + +SCAN_OUTCOMES = ( + "success", + "partial", + "failed", +) + +SCAN_OUTCOME_SUCCESS = SCAN_OUTCOMES[0] +SCAN_OUTCOME_PARTIAL = SCAN_OUTCOMES[1] +SCAN_OUTCOME_FAILED = SCAN_OUTCOMES[2] + +SCAN_STEPS = ( + "descriptor_refresh", + "attribute_discovery", + "attribute_reads", + "command_discovery_received", + "command_discovery_generated", +) + +SCAN_STEP_DESCRIPTOR_REFRESH = SCAN_STEPS[0] + +STEP_STATUSES = SCAN_STATUSES + +SCAN_ERROR_CODES = ( + "invalid_scan_options", + "scan_in_progress", + "device_scan_target_missing", + "descriptor_refresh_failed", + "scan_deadline_exceeded", + "unsupported_discovery_command", + "transport_failure", + "attribute_unsupported", + "missing_raw_manufacturer_code", +) + +ERROR_CODE_INVALID_SCAN_OPTIONS = SCAN_ERROR_CODES[0] +ERROR_CODE_SCAN_IN_PROGRESS = SCAN_ERROR_CODES[1] +ERROR_CODE_DEVICE_SCAN_TARGET_MISSING = SCAN_ERROR_CODES[2] +ERROR_CODE_DESCRIPTOR_REFRESH_FAILED = SCAN_ERROR_CODES[3] +ERROR_CODE_SCAN_DEADLINE_EXCEEDED = SCAN_ERROR_CODES[4] +ERROR_CODE_UNSUPPORTED_DISCOVERY_COMMAND = SCAN_ERROR_CODES[5] +ERROR_CODE_TRANSPORT_FAILURE = SCAN_ERROR_CODES[6] +ERROR_CODE_ATTRIBUTE_UNSUPPORTED = SCAN_ERROR_CODES[7] +ERROR_CODE_MISSING_RAW_MANUFACTURER_CODE = SCAN_ERROR_CODES[8] + +REQUEST_PACING_DELAY_S = 0.01 +SCAN_DEADLINE_S = 120.0 +ATTRIBUTE_DISCOVERY_PAGE_SIZE = 16 +ATTRIBUTE_READ_BATCH_SIZE = 3 +ATTRIBUTE_READ_RETRY_BACKOFF_S = 0.05 +COMMAND_DISCOVERY_RETRY_BACKOFF_S = 0.05 +UTC_TZ = timezone(timedelta(0)) + +ATTRIBUTE_READ_STATUS_SUCCESS = "success" +ATTRIBUTE_READ_STATUS_UNSUPPORTED = "unsupported_attribute" +ATTRIBUTE_READ_STATUS_TRANSPORT_FAILURE = "transport_failure" + +COMMAND_DISCOVERY_DIRECTION_RECEIVED = "received" +COMMAND_DISCOVERY_DIRECTION_GENERATED = "generated" + + +class InvalidScanOptionsError(ValueError): + """Raised when scan options are invalid.""" + + +class ScanInProgressError(RuntimeError): + """Raised when a conflicting scan is already queued or running.""" + + +class DeviceScanTargetMissingError(LookupError): + """Raised when a queued scan target no longer exists at execution time.""" + + +class DeviceScanSnapshotNotFoundError(LookupError): + """Raised when no persisted raw rows exist for a snapshot read.""" + + +@dataclass(frozen=True, slots=True) +class DeviceScanSummary: + ieee: t.EUI64 + completed: bool + outcome: str + used_resume: bool + force_full: bool + descriptor_refresh_performed: bool + last_finished: datetime | None + error_code: str | None + last_error: str | None + + +@dataclass(frozen=True, slots=True) +class DeviceScanSnapshot: + ieee: t.EUI64 + raw_node_descriptor: zdo_t.NodeDescriptor | None + last_snapshot_at: datetime | None + endpoints: list[DeviceScanSnapshotEndpoint] + + +@dataclass(frozen=True, slots=True) +class DeviceScanProgressEvent: + ieee: t.EUI64 + status: str + outcome: str | None = None + step: str | None = None + endpoint_id: int | None = None + cluster_id: int | None = None + cluster_type: ClusterType | None = None + manufacturer_code_scope: int | None = None + error_code: str | None = None + error: str | None = None + + +@dataclass(frozen=True, slots=True) +class DeviceScanSnapshotCommand: + direction: str + command_id: int + command_name: str | None + command_schema: str | None + + +@dataclass(frozen=True, slots=True) +class DeviceScanSnapshotAttribute: + attr_id: int + attribute_name: str | None + datatype: int | None + raw_value: bytes | None + decoded_value: object + read_complete: bool + read_status: str | None + last_error_code: str | None + last_error: str | None + + +@dataclass(frozen=True, slots=True) +class DeviceScanSnapshotProgress: + status: str + error_code: str | None + error: str | None + last_started: datetime | None + last_finished: datetime | None + last_success: datetime | None + attr_discovery_complete: bool + attr_discovery_next_id: int + attr_reads_complete: bool + cmd_rx_complete: bool + cmd_rx_next_id: int + cmd_tx_complete: bool + cmd_tx_next_id: int + + +@dataclass(frozen=True, slots=True) +class DeviceScanSnapshotScope: + manufacturer_code: int | None + progress: DeviceScanSnapshotProgress + attributes: list[DeviceScanSnapshotAttribute] + commands: list[DeviceScanSnapshotCommand] + + +@dataclass(frozen=True, slots=True) +class DeviceScanSnapshotCluster: + cluster_id: int + cluster_type: ClusterType + standard: DeviceScanSnapshotScope + manufacturer_specific: DeviceScanSnapshotScope + + +@dataclass(frozen=True, slots=True) +class DeviceScanSnapshotEndpoint: + endpoint_id: int + profile_id: int + device_type: int | None + status: int + clusters: list[DeviceScanSnapshotCluster] + + +@dataclass(frozen=True, slots=True) +class _RawDeviceDescriptors: + node_descriptor: zdo_t.NodeDescriptor + endpoints: tuple[zigpy.endpoint.DiscoveredEndpointDescriptor, ...] + + +@dataclass(frozen=True, slots=True) +class _ScanScopeKey: + endpoint_id: int + cluster_type: ClusterType + cluster_id: int + manufacturer_code_scope: int | None = None + + +@dataclass(frozen=True, slots=True) +class _RawScanTarget: + endpoint: zigpy.endpoint.Endpoint + cluster: Cluster + scope: _ScanScopeKey + + @property + def endpoint_id(self) -> int: + return self.scope.endpoint_id + + +@dataclass(slots=True) +class _SharedScanRequest: + ieee: t.EUI64 + resume: bool + force_full: bool + task: asyncio.Task[DeviceScanSummary] | None = None + + +class _ScanFailure(RuntimeError): + def __init__(self, message: str, *, error_code: str | None): + super().__init__(message) + self.error_code = error_code + + +class _TerminalStepFailure(_ScanFailure): + """A known scope-local failure that should not abort the whole scan.""" + + +@dataclass(frozen=True, slots=True) +class _StepResult: + status: str + error_code: str | None = None + error: str | None = None + + +class DeviceScanner(zigpy.util.ListenableMixin): + """On-demand raw device scan service.""" + + def __init__(self, app: zigpy.application.ControllerApplication) -> None: + super().__init__() + self._app = app + self._active_scans: dict[t.EUI64, _SharedScanRequest] = {} + self._scan_slot_limiter = zigpy.datastructures.RequestLimiter( + max_concurrency=1, capacities={0: 1} + ) + self._current_scan_deadline: asyncio.Timeout | None = None + + def _emit_progress(self, event_name: str, **payload) -> DeviceScanProgressEvent: + if event_name not in SCAN_EVENT_NAMES: + raise ValueError(f"Unknown device scan event name: {event_name!r}") + + status = payload.get("status") + if status not in SCAN_STATUSES: + raise ValueError(f"Unknown device scan status: {status!r}") + + outcome = payload.get("outcome") + if outcome is not None and outcome not in SCAN_OUTCOMES: + raise ValueError(f"Unknown device scan outcome: {outcome!r}") + + error_code = payload.get("error_code") + if error_code is not None and error_code not in SCAN_ERROR_CODES: + raise ValueError(f"Unknown device scan error code: {error_code!r}") + + event = DeviceScanProgressEvent(**payload) + self.listener_event(event_name, event) + return event + + def _get_dblistener(self) -> zigpy.appdb.PersistingListener: + dblistener = self._app._dblistener + if dblistener is None: + raise RuntimeError( + "DeviceScanner requires an initialized database listener" + ) + return dblistener + + def _scan_deadline_expired(self) -> bool: + return bool( + self._current_scan_deadline is not None + and self._current_scan_deadline.expired() + ) + + async def _noop_action(self) -> None: + await asyncio.sleep(0) + + async def scan( + self, + ieee: t.EUI64, + *, + resume: bool = True, + force_full: bool = False, + ) -> DeviceScanSummary: + if resume and force_full: + raise InvalidScanOptionsError( + "resume=True and force_full=True is invalid " + f"({ERROR_CODE_INVALID_SCAN_OPTIONS})" + ) + existing = self._active_scans.get(ieee) + if existing is not None: + if existing.resume == resume and existing.force_full == force_full: + existing_task = existing.task + if existing_task is None: + raise RuntimeError("Active device scan request is missing its task") + return await asyncio.shield(existing_task) + + raise ScanInProgressError( + f"Conflicting scan already queued or running ({ERROR_CODE_SCAN_IN_PROGRESS})" + ) + + request = _SharedScanRequest(ieee=ieee, resume=resume, force_full=force_full) + self._active_scans[ieee] = request + self._emit_progress( + SCAN_EVENT_QUEUED, + ieee=ieee, + status=SCAN_STATUS_QUEUED, + ) + + task = self._app.create_task( + self._run_shared_scan(request), + name=f"device-scanner:{ieee}", + ) + request.task = task + + def _remove_active_scan(_task: asyncio.Task[DeviceScanSummary]) -> None: + self._active_scans.pop(ieee, None) + + task.add_done_callback(_remove_active_scan) + + return await asyncio.shield(task) + + async def get_snapshot(self, ieee: t.EUI64) -> DeviceScanSnapshot: + dblistener = self._get_dblistener() + topology = await dblistener.get_raw_topology_rows(ieee) + + if ( + topology.node_descriptor is None + and not topology.endpoints + and not topology.clusters + ): + raise DeviceScanSnapshotNotFoundError( + f"No persisted raw descriptor rows for {ieee}" + ) + + rows = await dblistener.get_device_scan_rows(ieee) + return self._assemble_snapshot(ieee, topology, rows) + + def _scope_event_payload(self, target: _RawScanTarget) -> ScopeEventPayload: + return { + "endpoint_id": target.scope.endpoint_id, + "cluster_id": target.scope.cluster_id, + "cluster_type": target.scope.cluster_type, + "manufacturer_code_scope": target.scope.manufacturer_code_scope, + } + + def _progress_row_key( + self, + row: ( + zigpy.appdb.DeviceScanProgressRow + | zigpy.appdb.DeviceScanAttributeRow + | zigpy.appdb.DeviceScanCommandRow + ), + ) -> _ScanScopeKey: + return _ScanScopeKey( + endpoint_id=row.endpoint_id, + cluster_type=ClusterType(row.cluster_type), + cluster_id=row.cluster_id, + manufacturer_code_scope=row.manufacturer_code_scope, + ) + + async def _load_progress_by_scope( + self, ieee: t.EUI64 + ) -> dict[_ScanScopeKey, zigpy.appdb.DeviceScanProgressRow]: + rows = await self._get_dblistener().get_device_scan_rows(ieee) + return {self._progress_row_key(row): row for row in rows.progress} + + def _rows_snapshot( + self, rows: zigpy.appdb.DeviceScanRows + ) -> set[tuple[str, tuple[SnapshotRowValue, ...]]]: + return ( + {("progress", tuple(row)) for row in rows.progress} + | {("attribute", tuple(row)) for row in rows.attributes} + | {("command", tuple(row)) for row in rows.commands} + ) + + def _scan_rows_have_new_commits( + self, + before: zigpy.appdb.DeviceScanRows, + after: zigpy.appdb.DeviceScanRows, + ) -> bool: + return bool(self._rows_snapshot(after) - self._rows_snapshot(before)) + + async def _run_shared_scan(self, request: _SharedScanRequest) -> DeviceScanSummary: + dblistener = self._get_dblistener() + before_rows = await dblistener.get_device_scan_rows(request.ieee) + + async with self._scan_slot_limiter(priority=0): + try: + device = self._app.get_device(ieee=request.ieee) + except KeyError as exc: + error = DeviceScanTargetMissingError( + f"Queued scan target disappeared before execution ({ERROR_CODE_DEVICE_SCAN_TARGET_MISSING})" + ) + self._emit_progress( + SCAN_EVENT_FINISHED, + ieee=request.ieee, + status=SCAN_STATUS_FAILED, + outcome=SCAN_OUTCOME_FAILED, + error_code=ERROR_CODE_DEVICE_SCAN_TARGET_MISSING, + error=str(error), + ) + raise error from exc + + self._emit_progress( + SCAN_EVENT_STARTED, + ieee=device.ieee, + status=SCAN_STATUS_STARTED, + ) + + deadline = asyncio_timeout(SCAN_DEADLINE_S) + self._current_scan_deadline = deadline + + try: + try: + async with deadline: + summary = await self._run_scan_body( + device, + resume=request.resume, + force_full=request.force_full, + ) + except TimeoutError: + if not deadline.expired(): + raise + + after_rows = await dblistener.get_device_scan_rows(device.ieee) + outcome = ( + SCAN_OUTCOME_PARTIAL + if self._scan_rows_have_new_commits(before_rows, after_rows) + else SCAN_OUTCOME_FAILED + ) + summary = DeviceScanSummary( + ieee=device.ieee, + completed=True, + outcome=outcome, + used_resume=request.resume, + force_full=request.force_full, + descriptor_refresh_performed=True, + last_finished=datetime.now(UTC_TZ), + error_code=ERROR_CODE_SCAN_DEADLINE_EXCEEDED, + last_error="scan deadline exceeded", + ) + except _ScanFailure as exc: + summary = DeviceScanSummary( + ieee=device.ieee, + completed=True, + outcome=SCAN_OUTCOME_FAILED, + used_resume=request.resume, + force_full=request.force_full, + descriptor_refresh_performed=True, + last_finished=datetime.now(UTC_TZ), + error_code=exc.error_code, + last_error=str(exc), + ) + except Exception as exc: # noqa: BLE001 + summary = DeviceScanSummary( + ieee=device.ieee, + completed=True, + outcome=SCAN_OUTCOME_FAILED, + used_resume=request.resume, + force_full=request.force_full, + descriptor_refresh_performed=True, + last_finished=datetime.now(UTC_TZ), + error_code=getattr(exc, "device_scan_error_code", None), + last_error=str(exc), + ) + finally: + if self._current_scan_deadline is deadline: + self._current_scan_deadline = None + self._emit_progress( + SCAN_EVENT_FINISHED, + ieee=summary.ieee, + status=( + SCAN_STATUS_SUCCESS + if summary.outcome == SCAN_OUTCOME_SUCCESS + else SCAN_STATUS_FAILED + ), + outcome=summary.outcome, + error_code=summary.error_code, + error=summary.last_error, + ) + return summary + + async def _run_scope_step( + self, + target: _RawScanTarget, + *, + step: str, + action: typing.Callable[[], typing.Awaitable[None]], + skipped: bool = False, + error_code: str | None = None, + ) -> _StepResult: + payload = self._scope_event_payload(target) + + if skipped: + self._emit_progress( + SCAN_EVENT_STEP_FINISHED, + ieee=target.endpoint.device.ieee, + status=SCAN_STATUS_SKIPPED, + step=step, + error_code=error_code, + **payload, + ) + return _StepResult(status=SCAN_STATUS_SKIPPED, error_code=error_code) + + self._emit_progress( + SCAN_EVENT_STEP_STARTED, + ieee=target.endpoint.device.ieee, + status=SCAN_STATUS_STARTED, + step=step, + **payload, + ) + + try: + await action() + except _TerminalStepFailure as exc: + await self._get_dblistener().set_device_scan_progress_error( + ieee=target.endpoint.device.ieee, + endpoint_id=target.endpoint_id, + cluster_type=target.cluster.cluster_type, + cluster_id=target.cluster.cluster_id, + manufacturer_code_scope=target.scope.manufacturer_code_scope, + error_code=exc.error_code or ERROR_CODE_UNSUPPORTED_DISCOVERY_COMMAND, + error=str(exc), + ) + self._emit_progress( + SCAN_EVENT_STEP_FINISHED, + ieee=target.endpoint.device.ieee, + status=SCAN_STATUS_FAILED, + step=step, + error_code=exc.error_code, + error=str(exc), + **payload, + ) + return _StepResult( + status=SCAN_STATUS_FAILED, + error_code=exc.error_code, + error=str(exc), + ) + except TimeoutError as exc: + if self._scan_deadline_expired(): + raise + + error = str(exc) or "request timed out" + await self._get_dblistener().set_device_scan_progress_error( + ieee=target.endpoint.device.ieee, + endpoint_id=target.endpoint_id, + cluster_type=target.cluster.cluster_type, + cluster_id=target.cluster.cluster_id, + manufacturer_code_scope=target.scope.manufacturer_code_scope, + error_code=ERROR_CODE_TRANSPORT_FAILURE, + error=error, + ) + self._emit_progress( + SCAN_EVENT_STEP_FINISHED, + ieee=target.endpoint.device.ieee, + status=SCAN_STATUS_FAILED, + step=step, + error_code=ERROR_CODE_TRANSPORT_FAILURE, + error=error, + **payload, + ) + return _StepResult( + status=SCAN_STATUS_FAILED, + error_code=ERROR_CODE_TRANSPORT_FAILURE, + error=error, + ) + except Exception as exc: # noqa: BLE001 + self._emit_progress( + SCAN_EVENT_STEP_FINISHED, + ieee=target.endpoint.device.ieee, + status=SCAN_STATUS_FAILED, + step=step, + error=str(exc), + **payload, + ) + raise + + self._emit_progress( + SCAN_EVENT_STEP_FINISHED, + ieee=target.endpoint.device.ieee, + status=SCAN_STATUS_SUCCESS, + step=step, + **payload, + ) + return _StepResult(status=SCAN_STATUS_SUCCESS) + + async def _build_scan_targets_for_node_descriptor( + self, + device: zigpy.device.Device, + node_descriptor: zdo_t.NodeDescriptor, + ) -> tuple[_RawScanTarget, ...]: + raw_targets = await self._build_raw_scan_targets(device) + targets = list(raw_targets) + + if node_descriptor.manufacturer_code is not None: + targets.extend( + _RawScanTarget( + endpoint=target.endpoint, + cluster=target.cluster, + scope=_ScanScopeKey( + endpoint_id=target.scope.endpoint_id, + cluster_type=target.scope.cluster_type, + cluster_id=target.scope.cluster_id, + manufacturer_code_scope=node_descriptor.manufacturer_code, + ), + ) + for target in raw_targets + ) + + return tuple(targets) + + async def _run_scan_body( + self, + device: zigpy.device.Device, + *, + resume: bool, + force_full: bool, + ) -> DeviceScanSummary: + dblistener = self._get_dblistener() + + if force_full: + await dblistener.clear_device_scan_data(device.ieee) + + raw_descriptors = await self._refresh_raw_descriptors(device) + targets = await self._build_scan_targets_for_node_descriptor( + device, raw_descriptors.node_descriptor + ) + has_manufacturer_scope = ( + raw_descriptors.node_descriptor.manufacturer_code is not None + ) + progress_by_scope = await self._load_progress_by_scope(device.ieee) + outcome = SCAN_OUTCOME_SUCCESS + summary_error_code: str | None = None + summary_error: str | None = None + + def record_scope_failure(step_result: _StepResult) -> None: + nonlocal outcome, summary_error_code, summary_error + + if step_result.status != SCAN_STATUS_FAILED: + return + + outcome = SCAN_OUTCOME_PARTIAL + if summary_error_code is None: + summary_error_code = step_result.error_code + if summary_error is None: + summary_error = step_result.error + + for target in targets: + progress = progress_by_scope.get(target.scope) + attr_start_id = ( + 0 + if progress is None or not resume or progress.attr_discovery_complete + else progress.attr_discovery_next_id + ) + cmd_rx_start_id = ( + 0 + if progress is None or not resume or progress.cmd_rx_complete + else progress.cmd_rx_next_id + ) + cmd_tx_start_id = ( + 0 + if progress is None or not resume or progress.cmd_tx_complete + else progress.cmd_tx_next_id + ) + scope_terminal_failure = False + + attr_discovery_result = await self._run_scope_step( + target, + step=SCAN_STEPS[1], + skipped=bool(resume and progress and progress.attr_discovery_complete), + action=functools.partial( + self._discover_attributes_for_target, + target, + start_attr_id=attr_start_id, + ), + ) + record_scope_failure(attr_discovery_result) + scope_terminal_failure = attr_discovery_result.status == SCAN_STATUS_FAILED + + if not scope_terminal_failure: + progress_by_scope = await self._load_progress_by_scope(device.ieee) + progress = progress_by_scope.get(target.scope) + + attr_reads_result = await self._run_scope_step( + target, + step=SCAN_STEPS[2], + skipped=bool(resume and progress and progress.attr_reads_complete), + action=functools.partial(self._read_attributes_for_target, target), + ) + record_scope_failure(attr_reads_result) + + progress_by_scope = await self._load_progress_by_scope(device.ieee) + progress = progress_by_scope.get(target.scope) + + cmd_rx_result = await self._run_scope_step( + target, + step=SCAN_STEPS[3], + skipped=bool(resume and progress and progress.cmd_rx_complete), + action=functools.partial( + self._discover_commands_received_for_target, + target, + start_command_id=cmd_rx_start_id, + ), + ) + record_scope_failure(cmd_rx_result) + scope_terminal_failure = cmd_rx_result.status == SCAN_STATUS_FAILED + + if not scope_terminal_failure: + progress_by_scope = await self._load_progress_by_scope(device.ieee) + progress = progress_by_scope.get(target.scope) + + cmd_tx_result = await self._run_scope_step( + target, + step=SCAN_STEPS[4], + skipped=bool(resume and progress and progress.cmd_tx_complete), + action=functools.partial( + self._discover_commands_generated_for_target, + target, + start_command_id=cmd_tx_start_id, + ), + ) + record_scope_failure(cmd_tx_result) + + if ( + not has_manufacturer_scope + and target.scope.manufacturer_code_scope is None + ): + for step in SCAN_STEPS[1:]: + await self._run_scope_step( + target, + step=step, + skipped=True, + error_code=ERROR_CODE_MISSING_RAW_MANUFACTURER_CODE, + action=self._noop_action, + ) + + return DeviceScanSummary( + ieee=device.ieee, + completed=True, + outcome=outcome, + used_resume=resume, + force_full=force_full, + descriptor_refresh_performed=True, + last_finished=datetime.now(UTC_TZ), + error_code=summary_error_code, + last_error=summary_error, + ) + + def _to_datetime(self, timestamp: float | None) -> datetime | None: + if timestamp is None: + return None + return datetime.fromtimestamp(timestamp, UTC_TZ) + + def _decode_attribute_value( + self, + *, + datatype: int | None, + raw_value: bytes | None, + decode_cache: dict[int, type[_AttributeValueDeserializer]], + ) -> object: + if datatype is None or raw_value is None: + return None + + try: + python_type = decode_cache.get(datatype) + if python_type is None: + python_type = foundation.DataType.from_type_id( + foundation.DataTypeId(datatype) + ).python_type + decode_cache[datatype] = python_type + + value, _ = python_type.deserialize(raw_value) + return int(value) if isinstance(value, int) else value + except Exception: # noqa: BLE001 + return None + + def _make_snapshot_progress( + self, + row: zigpy.appdb.DeviceScanProgressRow | None, + *, + synthetic_skipped: bool, + ) -> DeviceScanSnapshotProgress: + if synthetic_skipped: + return DeviceScanSnapshotProgress( + status="skipped", + error_code=ERROR_CODE_MISSING_RAW_MANUFACTURER_CODE, + error=None, + last_started=None, + last_finished=None, + last_success=None, + attr_discovery_complete=False, + attr_discovery_next_id=0, + attr_reads_complete=False, + cmd_rx_complete=False, + cmd_rx_next_id=0, + cmd_tx_complete=False, + cmd_tx_next_id=0, + ) + + if row is None: + return DeviceScanSnapshotProgress( + status="pending", + error_code=None, + error=None, + last_started=None, + last_finished=None, + last_success=None, + attr_discovery_complete=False, + attr_discovery_next_id=0, + attr_reads_complete=False, + cmd_rx_complete=False, + cmd_rx_next_id=0, + cmd_tx_complete=False, + cmd_tx_next_id=0, + ) + + if ( + row.attr_discovery_complete + and row.attr_reads_complete + and row.cmd_rx_complete + and row.cmd_tx_complete + ): + status = "success" + elif row.last_error_code is not None: + status = "failed" + elif row.last_started is not None or row.last_finished is not None: + status = "started" + else: + status = "pending" + + return DeviceScanSnapshotProgress( + status=status, + error_code=row.last_error_code, + error=row.last_error, + last_started=self._to_datetime(row.last_started), + last_finished=self._to_datetime(row.last_finished), + last_success=self._to_datetime(row.last_success), + attr_discovery_complete=row.attr_discovery_complete, + attr_discovery_next_id=row.attr_discovery_next_id, + attr_reads_complete=row.attr_reads_complete, + cmd_rx_complete=row.cmd_rx_complete, + cmd_rx_next_id=row.cmd_rx_next_id, + cmd_tx_complete=row.cmd_tx_complete, + cmd_tx_next_id=row.cmd_tx_next_id, + ) + + def _assemble_snapshot( + self, + ieee: t.EUI64, + topology: zigpy.appdb.RawTopologyRows, + rows: zigpy.appdb.DeviceScanRows, + ) -> DeviceScanSnapshot: + progress_by_scope = {self._progress_row_key(row): row for row in rows.progress} + attrs_by_scope: dict[_ScanScopeKey, list[DeviceScanSnapshotAttribute]] = ( + collections.defaultdict(list) + ) + commands_by_scope: dict[_ScanScopeKey, list[DeviceScanSnapshotCommand]] = ( + collections.defaultdict(list) + ) + decode_cache: dict[int, type[_AttributeValueDeserializer]] = {} + timestamps: list[float] = [] + + for row in rows.progress: + timestamps.extend( + ts + for ts in (row.last_started, row.last_finished, row.last_success) + if ts is not None + ) + + for attribute_row in rows.attributes: + attrs_by_scope[self._progress_row_key(attribute_row)].append( + DeviceScanSnapshotAttribute( + attr_id=attribute_row.attr_id, + attribute_name=attribute_row.attribute_name, + datatype=attribute_row.datatype, + raw_value=attribute_row.value, + decoded_value=self._decode_attribute_value( + datatype=attribute_row.datatype, + raw_value=attribute_row.value, + decode_cache=decode_cache, + ), + read_complete=attribute_row.read_complete, + read_status=attribute_row.read_status, + last_error_code=attribute_row.last_error_code, + last_error=attribute_row.last_error, + ) + ) + timestamps.extend( + ts + for ts in (attribute_row.discovered_at, attribute_row.last_read) + if ts is not None + ) + + for command_row in rows.commands: + commands_by_scope[self._progress_row_key(command_row)].append( + DeviceScanSnapshotCommand( + direction=command_row.direction, + command_id=command_row.command_id, + command_name=command_row.command_name, + command_schema=command_row.command_schema, + ) + ) + timestamps.append(command_row.discovered_at) + + clusters_by_endpoint: dict[int, list[zigpy.appdb.RawClusterRow]] = ( + collections.defaultdict(list) + ) + for cluster_row in topology.clusters: + clusters_by_endpoint[cluster_row.endpoint_id].append(cluster_row) + + endpoints: list[DeviceScanSnapshotEndpoint] = [] + manufacturer_code = ( + None + if topology.node_descriptor is None + else topology.node_descriptor.manufacturer_code + ) + + for endpoint_row in topology.endpoints: + if endpoint_row.endpoint_id in (0, 242): + continue + + clusters: list[DeviceScanSnapshotCluster] = [] + for cluster_row in clusters_by_endpoint.get(endpoint_row.endpoint_id, ()): + cluster_type = ClusterType(cluster_row.cluster_type) + standard_scope_key = _ScanScopeKey( + endpoint_id=endpoint_row.endpoint_id, + cluster_type=cluster_type, + cluster_id=cluster_row.cluster_id, + manufacturer_code_scope=None, + ) + manufacturer_scope_key = _ScanScopeKey( + endpoint_id=endpoint_row.endpoint_id, + cluster_type=cluster_type, + cluster_id=cluster_row.cluster_id, + manufacturer_code_scope=manufacturer_code, + ) + synthetic_skipped = manufacturer_code is None + + clusters.append( + DeviceScanSnapshotCluster( + cluster_id=cluster_row.cluster_id, + cluster_type=cluster_type, + standard=DeviceScanSnapshotScope( + manufacturer_code=None, + progress=self._make_snapshot_progress( + progress_by_scope.get(standard_scope_key), + synthetic_skipped=False, + ), + attributes=attrs_by_scope.get(standard_scope_key, []), + commands=commands_by_scope.get(standard_scope_key, []), + ), + manufacturer_specific=DeviceScanSnapshotScope( + manufacturer_code=manufacturer_code, + progress=self._make_snapshot_progress( + progress_by_scope.get(manufacturer_scope_key), + synthetic_skipped=synthetic_skipped, + ), + attributes=( + [] + if synthetic_skipped + else attrs_by_scope.get(manufacturer_scope_key, []) + ), + commands=( + [] + if synthetic_skipped + else commands_by_scope.get(manufacturer_scope_key, []) + ), + ), + ) + ) + + endpoints.append( + DeviceScanSnapshotEndpoint( + endpoint_id=endpoint_row.endpoint_id, + profile_id=endpoint_row.profile_id, + device_type=self._coerce_device_type( + endpoint_row.profile_id, endpoint_row.device_type + ), + status=endpoint_row.status, + clusters=clusters, + ) + ) + + last_snapshot_at = ( + None if not timestamps else self._to_datetime(max(timestamps)) + ) + + return DeviceScanSnapshot( + ieee=ieee, + raw_node_descriptor=topology.node_descriptor, + last_snapshot_at=last_snapshot_at, + endpoints=endpoints, + ) + + async def _pace_requests(self) -> None: + await asyncio.sleep(REQUEST_PACING_DELAY_S) + + async def _discover_raw_descriptors( + self, device: zigpy.device.Device + ) -> _RawDeviceDescriptors: + node_descriptor = await device.discover_node_descriptor(refresh=True) + await self._pace_requests() + + endpoint_ids = await device.discover_active_endpoints(refresh=True) + await self._pace_requests() + + endpoints = [] + + for endpoint_id in endpoint_ids: + ephemeral_endpoint = zigpy.endpoint.Endpoint(device, endpoint_id) + endpoints.append(await ephemeral_endpoint.discover_descriptor(refresh=True)) + await self._pace_requests() + + return _RawDeviceDescriptors( + node_descriptor=node_descriptor, + endpoints=tuple(endpoints), + ) + + async def _refresh_raw_descriptors( + self, device: zigpy.device.Device + ) -> _RawDeviceDescriptors: + self._emit_progress( + SCAN_EVENT_STEP_STARTED, + ieee=device.ieee, + status=SCAN_STATUS_STARTED, + step=SCAN_STEP_DESCRIPTOR_REFRESH, + ) + + try: + raw_descriptors = await self._discover_raw_descriptors(device) + await self._get_dblistener().replace_device_raw_descriptors( + device, + node_descriptor=raw_descriptors.node_descriptor, + endpoints=raw_descriptors.endpoints, + ) + except Exception as exc: # noqa: BLE001 + setattr(exc, "device_scan_error_code", ERROR_CODE_DESCRIPTOR_REFRESH_FAILED) + self._emit_progress( + SCAN_EVENT_STEP_FINISHED, + ieee=device.ieee, + status=SCAN_STATUS_FAILED, + step=SCAN_STEP_DESCRIPTOR_REFRESH, + error_code=ERROR_CODE_DESCRIPTOR_REFRESH_FAILED, + error=str(exc), + ) + raise + + self._emit_progress( + SCAN_EVENT_STEP_FINISHED, + ieee=device.ieee, + status=SCAN_STATUS_SUCCESS, + step=SCAN_STEP_DESCRIPTOR_REFRESH, + ) + return raw_descriptors + + def _coerce_device_type( + self, profile_id: int | None, device_type: int | None + ) -> zigpy.profiles.zha.DeviceType | zigpy.profiles.zll.DeviceType | int | None: + if device_type is None: + return None + + if profile_id == zigpy.profiles.zha.PROFILE_ID: + return zigpy.profiles.zha.DeviceType(device_type) + if profile_id == zigpy.profiles.zll.PROFILE_ID: + return zigpy.profiles.zll.DeviceType(device_type) + return device_type + + def _make_ephemeral_cluster( + self, + endpoint: zigpy.endpoint.Endpoint, + *, + cluster_id: int, + cluster_type: ClusterType, + ) -> Cluster: + cluster = zigpy.zcl.Cluster.from_id( + endpoint, + cluster_id, + is_server=cluster_type == ClusterType.Server, + ) + + if cluster_type == ClusterType.Server: + endpoint.in_clusters[cluster_id] = cluster + else: + endpoint.out_clusters[cluster_id] = cluster + + if cluster.ep_attribute is not None: + endpoint._cluster_attr[cluster.ep_attribute] = cluster + + return cluster + + async def _build_raw_scan_targets( + self, device: zigpy.device.Device + ) -> tuple[_RawScanTarget, ...]: + topology = await self._get_dblistener().get_raw_topology_rows(device.ieee) + clusters_by_endpoint: dict[int, list[zigpy.appdb.RawClusterRow]] = ( + collections.defaultdict(list) + ) + + for cluster_row in topology.clusters: + clusters_by_endpoint[cluster_row.endpoint_id].append(cluster_row) + + targets = [] + + for endpoint_row in topology.endpoints: + # Match the reference diagnostic walker and avoid Green Power false timeouts. + if endpoint_row.endpoint_id in (0, 242): + continue + + endpoint = zigpy.endpoint.Endpoint(device, endpoint_row.endpoint_id) + endpoint.status = zigpy.endpoint.Status(endpoint_row.status) + endpoint.profile_id = endpoint_row.profile_id + endpoint.device_type = self._coerce_device_type( + endpoint_row.profile_id, endpoint_row.device_type + ) + + for cluster_row in clusters_by_endpoint.get(endpoint_row.endpoint_id, ()): + cluster_type = ClusterType(cluster_row.cluster_type) + cluster = self._make_ephemeral_cluster( + endpoint, + cluster_id=cluster_row.cluster_id, + cluster_type=cluster_type, + ) + + targets.append( + _RawScanTarget( + endpoint=endpoint, + cluster=cluster, + scope=_ScanScopeKey( + endpoint_id=endpoint_row.endpoint_id, + cluster_type=cluster.cluster_type, + cluster_id=cluster.cluster_id, + ), + ) + ) + + return tuple(targets) + + def _resolve_attribute_name( + self, target: _RawScanTarget, attr_id: int + ) -> str | None: + try: + return target.cluster.find_attribute( + attr_id, + manufacturer_code=target.scope.manufacturer_code_scope, + ).name + except KeyError: + return None + + def _canonicalize_attribute_value( + self, + value: foundation.TypeValue + | foundation.Array + | foundation.Bag + | foundation.Set, + ) -> tuple[int, bytes]: + if isinstance(value, foundation.Array | foundation.Bag | foundation.Set): + datatype = int(foundation.DataType.from_python_type(type(value)).type_id) + return datatype, value.serialize() + + datatype = int(value.type) + python_type = foundation.DataType.from_type_id( + foundation.DataTypeId(datatype) + ).python_type + return datatype, python_type(value.value).serialize() + + def _translate_discovery_failure( + self, exc: Exception + ) -> _TerminalStepFailure | None: + unsupported_statuses = { + foundation.Status.UNSUP_CLUSTER_COMMAND, + foundation.Status.UNSUP_GENERAL_COMMAND, + foundation.Status.UNSUP_MANUF_CLUSTER_COMMAND, + foundation.Status.UNSUP_MANUF_GENERAL_COMMAND, + foundation.Status.UNSUPPORTED_CLUSTER, + } + + if isinstance(exc, zigpy.exceptions.DeliveryError) and exc.status is not None: + try: + status = foundation.Status(exc.status) + except ValueError: + status = None + + if status in unsupported_statuses: + return _TerminalStepFailure( + str(exc), + error_code=ERROR_CODE_UNSUPPORTED_DISCOVERY_COMMAND, + ) + + lowered_error = str(exc).lower() + if "unsupported" in lowered_error or "not supported" in lowered_error: + return _TerminalStepFailure( + str(exc), + error_code=ERROR_CODE_UNSUPPORTED_DISCOVERY_COMMAND, + ) + + return None + + def _translate_discovery_response_failure( + self, + response: ( + _AttributeDiscoveryResponse + | _CommandDiscoveryResponse + | _DefaultResponseResult + ), + ) -> _TerminalStepFailure | None: + status = self._extract_default_response_status(response) + + if status is None: + return None + + return _TerminalStepFailure( + f"discovery command rejected with {status.name.lower()}", + error_code=ERROR_CODE_UNSUPPORTED_DISCOVERY_COMMAND, + ) + + def _extract_default_response_status( + self, + response: ( + _AttributeDiscoveryResponse + | _CommandDiscoveryResponse + | _ReadAttributesRawResponse + | _DefaultResponseResult + ), + ) -> foundation.Status | None: + status: foundation.Status | int + + if isinstance(response, foundation.DefaultResponse): + status = response.status + elif ( + isinstance(response, tuple) + and len(response) == 2 + and response[0] == foundation.GeneralCommand.Default_Response + ): + status = response[1] + else: + return None + + try: + return foundation.Status(status) + except ValueError: + return None + + def _normalize_attribute_read_status_records( + self, + response: _ReadAttributesRawResponse | _DefaultResponseResult, + attr_ids: list[int], + ) -> list[foundation.ReadAttributeRecord]: + status = self._extract_default_response_status(response) + + if status is None: + raw_response = typing.cast(_ReadAttributesRawResponse, response) + return list(raw_response.status_records) + + return [ + foundation.ReadAttributeRecord(attrid=t.uint16_t(attr_id), status=status) + for attr_id in attr_ids + ] + + def _get_unsupported_discovery_status( + self, + response: ( + _StandardAttributeDiscoveryResponse + | _ExtendedAttributeDiscoveryResponse + | _CommandDiscoveryResponse + | _DefaultResponseResult + ), + ) -> foundation.Status | None: + status = self._extract_default_response_status(response) + + if status not in ( + foundation.Status.UNSUP_GENERAL_COMMAND, + foundation.Status.UNSUP_MANUF_GENERAL_COMMAND, + ): + return None + + return status + + def _extract_discovered_attributes( + self, + target: _RawScanTarget, + response: _AttributeDiscoveryResponse, + *, + start_attr_id: int, + ) -> tuple[list[tuple[int, str | None, int, int | None]], int]: + if hasattr(response, "extended_attr_info"): + extended_response = typing.cast( + _ExtendedAttributeDiscoveryResponse, response + ) + discovered_attributes: list[tuple[int, str | None, int, int | None]] = [ + ( + int(attribute_info.attrid), + self._resolve_attribute_name(target, int(attribute_info.attrid)), + int(attribute_info.datatype), + int(attribute_info.acl), + ) + for attribute_info in extended_response.extended_attr_info + ] + next_attr_id = ( + start_attr_id + if not extended_response.extended_attr_info + else int(extended_response.extended_attr_info[-1].attrid) + 1 + ) + return discovered_attributes, next_attr_id + + standard_response = typing.cast(_StandardAttributeDiscoveryResponse, response) + discovered_attributes = [ + ( + int(attribute_info.attrid), + self._resolve_attribute_name(target, int(attribute_info.attrid)), + int(attribute_info.datatype), + None, + ) + for attribute_info in standard_response.attribute_info + ] + next_attr_id = ( + start_attr_id + if not standard_response.attribute_info + else int(standard_response.attribute_info[-1].attrid) + 1 + ) + return discovered_attributes, next_attr_id + + async def _skip_unsupported_command_discovery( + self, + target: _RawScanTarget, + *, + direction: str, + start_command_id: int, + status: foundation.Status, + ) -> None: + LOGGER.warning( + "Skipping %s command discovery for %s endpoint %s cluster 0x%04x (%s): unsupported command discovery response %s", + direction, + target.endpoint.device.ieee, + target.endpoint_id, + target.cluster.cluster_id, + target.cluster.cluster_type.name.lower(), + status.name, + ) + await self._pace_requests() + await self._get_dblistener().persist_device_scan_command_discovery_page( + ieee=target.endpoint.device.ieee, + endpoint_id=target.endpoint_id, + cluster_type=target.cluster.cluster_type, + cluster_id=target.cluster.cluster_id, + manufacturer_code_scope=target.scope.manufacturer_code_scope, + direction=direction, + commands=[], + next_command_id=start_command_id, + complete=True, + ) + + async def _discover_attributes_for_target( + self, target: _RawScanTarget, *, start_attr_id: int = 0 + ) -> None: + use_extended_discovery = True + + while True: + try: + if use_extended_discovery: + response: ( + _AttributeDiscoveryResponse | _DefaultResponseResult + ) = await target.cluster.discover_attributes_extended( + start_attr_id, + ATTRIBUTE_DISCOVERY_PAGE_SIZE, + manufacturer=target.scope.manufacturer_code_scope, + ) + else: + response = await target.cluster.discover_attributes( + start_attr_id, + ATTRIBUTE_DISCOVERY_PAGE_SIZE, + manufacturer=target.scope.manufacturer_code_scope, + ) + except (TimeoutError, zigpy.exceptions.ZigbeeException) as exc: + terminal_failure = self._translate_discovery_failure(exc) + + if terminal_failure is not None: + raise terminal_failure from exc + + raise + unsupported_status = self._get_unsupported_discovery_status(response) + + if unsupported_status is not None: + if use_extended_discovery: + LOGGER.warning( + "Extended attribute discovery unsupported for %s endpoint %s cluster 0x%04x (%s): %s; falling back to discover_attributes", + target.endpoint.device.ieee, + target.endpoint_id, + target.cluster.cluster_id, + target.cluster.cluster_type.name.lower(), + unsupported_status.name, + ) + use_extended_discovery = False + continue + + LOGGER.warning( + "Standard attribute discovery unsupported for %s endpoint %s cluster 0x%04x (%s): %s; continuing scan without attribute discovery", + target.endpoint.device.ieee, + target.endpoint_id, + target.cluster.cluster_id, + target.cluster.cluster_type.name.lower(), + unsupported_status.name, + ) + await self._pace_requests() + await ( + self._get_dblistener().persist_device_scan_attribute_discovery_page( + ieee=target.endpoint.device.ieee, + endpoint_id=target.endpoint_id, + cluster_type=target.cluster.cluster_type, + cluster_id=target.cluster.cluster_id, + manufacturer_code_scope=target.scope.manufacturer_code_scope, + attributes=[], + next_attr_id=start_attr_id, + complete=True, + ) + ) + return + + terminal_failure = self._translate_discovery_response_failure(response) + + if terminal_failure is not None: + raise terminal_failure + + await self._pace_requests() + attribute_response = typing.cast(_AttributeDiscoveryResponse, response) + discovered_attributes, next_attr_id = self._extract_discovered_attributes( + target, + attribute_response, + start_attr_id=start_attr_id, + ) + + await self._get_dblistener().persist_device_scan_attribute_discovery_page( + ieee=target.endpoint.device.ieee, + endpoint_id=target.endpoint_id, + cluster_type=target.cluster.cluster_type, + cluster_id=target.cluster.cluster_id, + manufacturer_code_scope=target.scope.manufacturer_code_scope, + attributes=discovered_attributes, + next_attr_id=next_attr_id, + complete=bool(attribute_response.discovery_complete), + ) + + if attribute_response.discovery_complete: + return + + start_attr_id = next_attr_id + + async def _persist_attribute_read_transport_failure( + self, + target: _RawScanTarget, + attr_id: int, + exc: Exception, + ) -> None: + await self._get_dblistener().persist_device_scan_attribute_read_results( + ieee=target.endpoint.device.ieee, + endpoint_id=target.endpoint_id, + cluster_type=target.cluster.cluster_type, + cluster_id=target.cluster.cluster_id, + manufacturer_code_scope=target.scope.manufacturer_code_scope, + results=[ + ( + attr_id, + None, + False, + ATTRIBUTE_READ_STATUS_TRANSPORT_FAILURE, + None, + None, + ERROR_CODE_TRANSPORT_FAILURE, + str(exc), + ) + ], + ) + + async def _read_attribute_rows_with_fallback( + self, + target: _RawScanTarget, + attr_rows: list[zigpy.appdb.DeviceScanAttributeRow], + *, + retried: bool = False, + ) -> tuple[int, ...]: + attr_ids = [row.attr_id for row in attr_rows] + + try: + response = await target.cluster.read_attributes_raw( + attr_ids, + manufacturer=target.scope.manufacturer_code_scope, + ) + except (TimeoutError, zigpy.exceptions.ZigbeeException) as exc: + if not retried and len(attr_rows) > 1: + await asyncio.sleep(ATTRIBUTE_READ_RETRY_BACKOFF_S) + return await self._read_attribute_rows_with_fallback( + target, + attr_rows, + retried=True, + ) + + if len(attr_rows) == 1: + await self._persist_attribute_read_transport_failure( + target, attr_rows[0].attr_id, exc + ) + await self._pace_requests() + return () + + midpoint = len(attr_rows) // 2 + lower_missing_attr_ids = await self._read_attribute_rows_with_fallback( + target, attr_rows[:midpoint] + ) + upper_missing_attr_ids = await self._read_attribute_rows_with_fallback( + target, attr_rows[midpoint:] + ) + return lower_missing_attr_ids + upper_missing_attr_ids + + await self._pace_requests() + records_by_attr_id = { + int(status_record.attrid): status_record + for status_record in self._normalize_attribute_read_status_records( + response, attr_ids + ) + } + missing_attr_ids = [ + row.attr_id for row in attr_rows if row.attr_id not in records_by_attr_id + ] + missing_attr_ids_text = ", ".join( + f"0x{attr_id:04x}" for attr_id in missing_attr_ids + ) + if missing_attr_ids: + response_attr_ids_text = ", ".join( + f"0x{attr_id:04x}" for attr_id in sorted(records_by_attr_id) + ) + LOGGER.warning( + "Read_Attributes response missing status records for %s endpoint %s cluster 0x%04x (%s) manufacturer %s: missing %s; response contained %s", + target.endpoint.device.ieee, + target.endpoint_id, + target.cluster.cluster_id, + target.cluster.cluster_type.name.lower(), + target.scope.manufacturer_code_scope, + missing_attr_ids_text, + response_attr_ids_text or "(none)", + ) + last_read = datetime.now(UTC_TZ).timestamp() + results: list[ + tuple[ + int, + int | None, + bool, + str | None, + bytes | None, + float | None, + str | None, + str | None, + ] + ] = [] + missing_rows: list[zigpy.appdb.DeviceScanAttributeRow] = [] + + for row in attr_rows: + record = records_by_attr_id.get(row.attr_id) + + if record is None: + missing_rows.append(row) + + if len(attr_rows) > 1: + continue + + results.append( + ( + row.attr_id, + None, + False, + ATTRIBUTE_READ_STATUS_TRANSPORT_FAILURE, + None, + None, + ERROR_CODE_TRANSPORT_FAILURE, + f"missing read response records for attributes {missing_attr_ids_text}", + ) + ) + continue + + if record.status == foundation.Status.SUCCESS: + attribute_value = typing.cast( + foundation.TypeValue + | foundation.Array + | foundation.Bag + | foundation.Set, + record.value, + ) + datatype, value = self._canonicalize_attribute_value(attribute_value) + results.append( + ( + row.attr_id, + datatype, + True, + ATTRIBUTE_READ_STATUS_SUCCESS, + value, + last_read, + None, + None, + ) + ) + elif record.status == foundation.Status.UNSUPPORTED_ATTRIBUTE: + results.append( + ( + row.attr_id, + None, + True, + ATTRIBUTE_READ_STATUS_UNSUPPORTED, + None, + last_read, + ERROR_CODE_ATTRIBUTE_UNSUPPORTED, + None, + ) + ) + else: + results.append( + ( + row.attr_id, + None, + True, + record.status.name.lower(), + None, + last_read, + None, + None, + ) + ) + + if results: + await self._get_dblistener().persist_device_scan_attribute_read_results( + ieee=target.endpoint.device.ieee, + endpoint_id=target.endpoint_id, + cluster_type=target.cluster.cluster_type, + cluster_id=target.cluster.cluster_id, + manufacturer_code_scope=target.scope.manufacturer_code_scope, + results=results, + ) + + if len(attr_rows) > 1 and missing_rows: + retried_missing_attr_ids: list[int] = [] + + for missing_row in missing_rows: + retried_missing_attr_ids.extend( + await self._read_attribute_rows_with_fallback( + target, + [missing_row], + retried=True, + ) + ) + + return tuple(retried_missing_attr_ids) + + return tuple(missing_attr_ids) + + async def _read_attributes_for_target(self, target: _RawScanTarget) -> None: + dblistener = self._get_dblistener() + pending_rows = await dblistener.get_pending_device_scan_attributes( + ieee=target.endpoint.device.ieee, + endpoint_id=target.endpoint_id, + cluster_type=target.cluster.cluster_type, + cluster_id=target.cluster.cluster_id, + manufacturer_code_scope=target.scope.manufacturer_code_scope, + ) + + if not pending_rows: + await dblistener.set_device_scan_attr_reads_complete( + ieee=target.endpoint.device.ieee, + endpoint_id=target.endpoint_id, + cluster_type=target.cluster.cluster_type, + cluster_id=target.cluster.cluster_id, + manufacturer_code_scope=target.scope.manufacturer_code_scope, + complete=True, + ) + return + + missing_attr_ids: list[int] = [] + for index in range(0, len(pending_rows), ATTRIBUTE_READ_BATCH_SIZE): + batch = pending_rows[index : index + ATTRIBUTE_READ_BATCH_SIZE] + missing_attr_ids.extend( + await self._read_attribute_rows_with_fallback(target, batch) + ) + + remaining_rows = await dblistener.get_pending_device_scan_attributes( + ieee=target.endpoint.device.ieee, + endpoint_id=target.endpoint_id, + cluster_type=target.cluster.cluster_type, + cluster_id=target.cluster.cluster_id, + manufacturer_code_scope=target.scope.manufacturer_code_scope, + ) + if missing_attr_ids: + missing_attr_ids_text = ", ".join( + f"0x{attr_id:04x}" for attr_id in sorted(set(missing_attr_ids)) + ) + raise _TerminalStepFailure( + f"missing read response records for attributes {missing_attr_ids_text}", + error_code=ERROR_CODE_TRANSPORT_FAILURE, + ) + if remaining_rows: + if len(remaining_rows) == 1 and remaining_rows[0].last_error: + error = remaining_rows[0].last_error + else: + remaining_attr_ids_text = ", ".join( + f"0x{row.attr_id:04x}" for row in remaining_rows + ) + error = f"request timed out for attributes {remaining_attr_ids_text}" + + raise _TerminalStepFailure( + error, + error_code=ERROR_CODE_TRANSPORT_FAILURE, + ) + + await dblistener.set_device_scan_attr_reads_complete( + ieee=target.endpoint.device.ieee, + endpoint_id=target.endpoint_id, + cluster_type=target.cluster.cluster_type, + cluster_id=target.cluster.cluster_id, + manufacturer_code_scope=target.scope.manufacturer_code_scope, + complete=True, + ) + + def _get_command_definitions( + self, target: _RawScanTarget, direction: str + ) -> Mapping[int, foundation.ZCLCommandDef]: + if direction == COMMAND_DISCOVERY_DIRECTION_RECEIVED: + return ( + target.cluster.server_commands + if target.cluster.is_server + else target.cluster.client_commands + ) + + return ( + target.cluster.client_commands + if target.cluster.is_server + else target.cluster.server_commands + ) + + def _resolve_command_name( + self, target: _RawScanTarget, direction: str, command_id: int + ) -> str | None: + command_def = self._get_command_definitions(target, direction).get(command_id) + return None if command_def is None else command_def.name + + async def _discover_commands_for_target( + self, + target: _RawScanTarget, + *, + direction: str, + discover_commands: _DiscoverCommandsCallable, + start_command_id: int = 0, + ) -> None: + while True: + retried_timeout = False + + while True: + try: + response = await discover_commands( + start_command_id, + ATTRIBUTE_DISCOVERY_PAGE_SIZE, + manufacturer=target.scope.manufacturer_code_scope, + ) + except TimeoutError: + if ( + target.scope.manufacturer_code_scope is not None + and not retried_timeout + ): + LOGGER.warning( + "Retrying manufacturer-scoped %s command discovery for %s endpoint %s cluster 0x%04x (%s) after timeout", + direction, + target.endpoint.device.ieee, + target.endpoint_id, + target.cluster.cluster_id, + target.cluster.cluster_type.name.lower(), + ) + retried_timeout = True + await asyncio.sleep(COMMAND_DISCOVERY_RETRY_BACKOFF_S) + continue + + raise + except zigpy.exceptions.ZigbeeException as exc: + terminal_failure = self._translate_discovery_failure(exc) + + if terminal_failure is not None: + raise terminal_failure from exc + + raise + + break + unsupported_status = self._get_unsupported_discovery_status(response) + + if unsupported_status is not None: + await self._skip_unsupported_command_discovery( + target, + direction=direction, + start_command_id=start_command_id, + status=unsupported_status, + ) + return + + terminal_failure = self._translate_discovery_response_failure(response) + + if terminal_failure is not None: + raise terminal_failure + + await self._pace_requests() + command_response = typing.cast(_CommandDiscoveryResponse, response) + + commands: list[tuple[int, str | None, str | None]] = [ + ( + int(command_id), + self._resolve_command_name(target, direction, int(command_id)), + None, + ) + for command_id in command_response.command_ids + ] + next_command_id = ( + start_command_id + if not command_response.command_ids + else int(command_response.command_ids[-1]) + 1 + ) + + await self._get_dblistener().persist_device_scan_command_discovery_page( + ieee=target.endpoint.device.ieee, + endpoint_id=target.endpoint_id, + cluster_type=target.cluster.cluster_type, + cluster_id=target.cluster.cluster_id, + manufacturer_code_scope=target.scope.manufacturer_code_scope, + direction=direction, + commands=commands, + next_command_id=next_command_id, + complete=bool(command_response.discovery_complete), + ) + + if command_response.discovery_complete: + return + + start_command_id = next_command_id + + async def _discover_commands_received_for_target( + self, target: _RawScanTarget, *, start_command_id: int = 0 + ) -> None: + await self._discover_commands_for_target( + target, + direction=COMMAND_DISCOVERY_DIRECTION_RECEIVED, + discover_commands=target.cluster.discover_commands_received, + start_command_id=start_command_id, + ) + + async def _discover_commands_generated_for_target( + self, target: _RawScanTarget, *, start_command_id: int = 0 + ) -> None: + await self._discover_commands_for_target( + target, + direction=COMMAND_DISCOVERY_DIRECTION_GENERATED, + discover_commands=target.cluster.discover_commands_generated, + start_command_id=start_command_id, + ) diff --git a/zigpy/endpoint.py b/zigpy/endpoint.py index 99f925474..913b696d8 100644 --- a/zigpy/endpoint.py +++ b/zigpy/endpoint.py @@ -1,5 +1,6 @@ from __future__ import annotations +from dataclasses import dataclass import enum import logging from typing import TYPE_CHECKING, Any @@ -30,6 +31,18 @@ class Status(enum.IntEnum): ENDPOINT_INACTIVE = 3 +@dataclass(frozen=True, slots=True) +class DiscoveredEndpointDescriptor: + endpoint_id: int + status: Status + profile_id: int | None + device_type: ( + zigpy.profiles.zha.DeviceType | zigpy.profiles.zll.DeviceType | int | None + ) + input_clusters: tuple[int, ...] + output_clusters: tuple[int, ...] + + class Endpoint(zigpy.util.LocalLogMixin, zigpy.util.ListenableMixin): """An endpoint on a device on the network""" @@ -40,7 +53,9 @@ def __init__(self, device: Device, endpoint_id: int) -> None: self.status: Status = Status.NEW self.profile_id: int | None = None - self.device_type: zigpy.profiles.zha.DeviceType | None = None + self.device_type: ( + zigpy.profiles.zha.DeviceType | zigpy.profiles.zll.DeviceType | int | None + ) = None self.in_clusters: dict = {} self.out_clusters: dict = {} self._cluster_attr: dict = {} @@ -50,41 +65,80 @@ def __init__(self, device: Device, endpoint_id: int) -> None: self._manufacturer: str | None = None self._model: str | None = None - async def initialize(self) -> None: + async def discover_descriptor( + self, *, refresh: bool = False + ) -> DiscoveredEndpointDescriptor: self.info("Discovering endpoint information") - if self.profile_id is not None or self.status == Status.ENDPOINT_INACTIVE: + if not refresh and ( + self.profile_id is not None or self.status == Status.ENDPOINT_INACTIVE + ): self.info("Endpoint descriptor already queried") - else: - status, _, sd = await self._device.zdo.Simple_Desc_req( - self._device.nwk, self._endpoint_id + return DiscoveredEndpointDescriptor( + endpoint_id=self._endpoint_id, + status=self.status, + profile_id=self.profile_id, + device_type=self.device_type, + input_clusters=tuple(self.in_clusters), + output_clusters=tuple(self.out_clusters), ) - if status == ZDOStatus.NOT_ACTIVE: - # These endpoints are essentially junk but this lets the device join - self.status = Status.ENDPOINT_INACTIVE - return - elif status != ZDOStatus.SUCCESS: - raise zigpy.exceptions.InvalidResponse( - "Failed to retrieve service descriptor: %s", status - ) + status, _, sd = await self._device.zdo.Simple_Desc_req( + self._device.nwk, self._endpoint_id + ) + + if status == ZDOStatus.NOT_ACTIVE: + return DiscoveredEndpointDescriptor( + endpoint_id=self._endpoint_id, + status=Status.ENDPOINT_INACTIVE, + profile_id=None, + device_type=None, + input_clusters=(), + output_clusters=(), + ) + elif status != ZDOStatus.SUCCESS: + raise zigpy.exceptions.InvalidResponse( + "Failed to retrieve service descriptor: %s", status + ) + + self.info("Discovered endpoint information: %s", sd) + device_type = sd.device_type + + if sd.profile == zigpy.profiles.zha.PROFILE_ID: + device_type = zigpy.profiles.zha.DeviceType(device_type) + elif sd.profile == zigpy.profiles.zll.PROFILE_ID: + device_type = zigpy.profiles.zll.DeviceType(device_type) + + return DiscoveredEndpointDescriptor( + endpoint_id=self._endpoint_id, + status=Status.ZDO_INIT, + profile_id=sd.profile, + device_type=device_type, + input_clusters=tuple(sd.input_clusters), + output_clusters=tuple(sd.output_clusters), + ) - self.info("Discovered endpoint information: %s", sd) - self.profile_id = sd.profile - self.device_type = sd.device_type + def apply_discovered_descriptor( + self, descriptor: DiscoveredEndpointDescriptor + ) -> None: + if descriptor.status == Status.ENDPOINT_INACTIVE: + self.status = Status.ENDPOINT_INACTIVE + return - if self.profile_id == zigpy.profiles.zha.PROFILE_ID: - self.device_type = zigpy.profiles.zha.DeviceType(self.device_type) - elif self.profile_id == zigpy.profiles.zll.PROFILE_ID: - self.device_type = zigpy.profiles.zll.DeviceType(self.device_type) + self.profile_id = descriptor.profile_id + self.device_type = descriptor.device_type - for cluster in sd.input_clusters: - self.add_input_cluster(cluster) + for cluster in descriptor.input_clusters: + self.add_input_cluster(cluster) - for cluster in sd.output_clusters: - self.add_output_cluster(cluster) + for cluster in descriptor.output_clusters: + self.add_output_cluster(cluster) - self.status = Status.ZDO_INIT + self.status = descriptor.status + + async def initialize(self) -> None: + descriptor = await self.discover_descriptor() + self.apply_discovered_descriptor(descriptor) @property def clusters(self) -> list[zigpy.zcl.Cluster]: diff --git a/zigpy/ota/image.py b/zigpy/ota/image.py index 9e3728dfc..78affe356 100644 --- a/zigpy/ota/image.py +++ b/zigpy/ota/image.py @@ -91,20 +91,28 @@ class OTAImageHeader(t.Struct): image_size: t.uint32_t security_credential_version: t.uint8_t = t.StructField( - requires=lambda s: s.field_control is not None - and FieldControl.SECURITY_CREDENTIAL_VERSION_PRESENT in s.field_control + requires=lambda s: ( + s.field_control is not None + and FieldControl.SECURITY_CREDENTIAL_VERSION_PRESENT in s.field_control + ) ) upgrade_file_destination: t.EUI64 = t.StructField( - requires=lambda s: s.field_control is not None - and FieldControl.DEVICE_SPECIFIC_FILE_PRESENT in s.field_control + requires=lambda s: ( + s.field_control is not None + and FieldControl.DEVICE_SPECIFIC_FILE_PRESENT in s.field_control + ) ) minimum_hardware_version: HWVersion = t.StructField( - requires=lambda s: s.field_control is not None - and FieldControl.HARDWARE_VERSIONS_PRESENT in s.field_control + requires=lambda s: ( + s.field_control is not None + and FieldControl.HARDWARE_VERSIONS_PRESENT in s.field_control + ) ) maximum_hardware_version: HWVersion = t.StructField( - requires=lambda s: s.field_control is not None - and FieldControl.HARDWARE_VERSIONS_PRESENT in s.field_control + requires=lambda s: ( + s.field_control is not None + and FieldControl.HARDWARE_VERSIONS_PRESENT in s.field_control + ) ) @property diff --git a/zigpy/zcl/clusters/general.py b/zigpy/zcl/clusters/general.py index c01396d22..7156d5e15 100644 --- a/zigpy/zcl/clusters/general.py +++ b/zigpy/zcl/clusters/general.py @@ -1895,20 +1895,25 @@ class ImageNotifyCommand(foundation.CommandSchema): query_jitter: t.uint8_t manufacturer_code: t.uint16_t = t.StructField( requires=( - lambda s: s.payload_type - >= ImageNotifyPayloadType.QueryJitter_ManufacturerCode + lambda s: ( + s.payload_type >= ImageNotifyPayloadType.QueryJitter_ManufacturerCode + ) ) ) image_type: t.uint16_t = t.StructField( requires=( - lambda s: s.payload_type - >= ImageNotifyPayloadType.QueryJitter_ManufacturerCode_ImageType + lambda s: ( + s.payload_type + >= ImageNotifyPayloadType.QueryJitter_ManufacturerCode_ImageType + ) ) ) new_file_version: t.uint32_t = t.StructField( requires=( - lambda s: s.payload_type - >= ImageNotifyPayloadType.QueryJitter_ManufacturerCode_ImageType_NewFileVersion + lambda s: ( + s.payload_type + >= ImageNotifyPayloadType.QueryJitter_ManufacturerCode_ImageType_NewFileVersion + ) ) ) @@ -1926,8 +1931,9 @@ class QueryNextImageCommand(foundation.CommandSchema): current_file_version: t.uint32_t hardware_version: t.uint16_t = t.StructField( requires=( - lambda s: s.field_control - & QueryNextImageCommandFieldControl.HardwareVersion + lambda s: ( + s.field_control & QueryNextImageCommandFieldControl.HardwareVersion + ) ) ) diff --git a/zigpy/zcl/clusters/greenpower.py b/zigpy/zcl/clusters/greenpower.py index 70ea5db10..4d0310718 100644 --- a/zigpy/zcl/clusters/greenpower.py +++ b/zigpy/zcl/clusters/greenpower.py @@ -124,34 +124,41 @@ class PairingSchema(foundation.CommandSchema): gpd_id: zgptypes.DeviceID # Table 37 sink_ieee: t.EUI64 = StructField( - requires=lambda s: not s.options.remove_gpd - and s.options.communication_mode - in ( - zgptypes.CommunicationMode.Unicast, - zgptypes.CommunicationMode.UnicastLightweight, + requires=lambda s: ( + not s.options.remove_gpd + and s.options.communication_mode + in ( + zgptypes.CommunicationMode.Unicast, + zgptypes.CommunicationMode.UnicastLightweight, + ) ) ) sink_nwk_addr: t.NWK = StructField( - requires=lambda s: not s.options.remove_gpd - and s.options.communication_mode - in ( - zgptypes.CommunicationMode.Unicast, - zgptypes.CommunicationMode.UnicastLightweight, + requires=lambda s: ( + not s.options.remove_gpd + and s.options.communication_mode + in ( + zgptypes.CommunicationMode.Unicast, + zgptypes.CommunicationMode.UnicastLightweight, + ) ) ) sink_group: t.Group = StructField( - requires=lambda s: not s.options.remove_gpd - and s.options.communication_mode - in ( - zgptypes.CommunicationMode.GroupcastForwardToDGroup, - zgptypes.CommunicationMode.GroupcastForwardToCommGroup, + requires=lambda s: ( + not s.options.remove_gpd + and s.options.communication_mode + in ( + zgptypes.CommunicationMode.GroupcastForwardToDGroup, + zgptypes.CommunicationMode.GroupcastForwardToCommGroup, + ) ) ) device_id: t.uint8_t = StructField(requires=lambda s: s.options.add_sink) frame_counter: t.uint32_t = StructField( - requires=lambda s: s.options.add_sink - and s.options.security_frame_counter_present + requires=lambda s: ( + s.options.add_sink and s.options.security_frame_counter_present + ) ) key: t.KeyData = StructField( requires=lambda s: s.options.add_sink and s.options.security_key_present diff --git a/zigpy/zdo/types.py b/zigpy/zdo/types.py index f4b86cdd6..437e461ca 100644 --- a/zigpy/zdo/types.py +++ b/zigpy/zdo/types.py @@ -389,8 +389,9 @@ class NwkUpdate(t.Struct): ScanDuration: t.uint8_t ScanCount: t.uint8_t = t.StructField(requires=lambda s: s.ScanDuration <= 0x05) nwkUpdateId: t.uint8_t = t.StructField( # noqa: N815 - requires=lambda s: s.ScanDuration - in (CHANNEL_CHANGE_REQ, CHANNEL_MASK_MANAGER_ADDR_CHANGE_REQ) + requires=lambda s: ( + s.ScanDuration in (CHANNEL_CHANGE_REQ, CHANNEL_MASK_MANAGER_ADDR_CHANGE_REQ) + ) ) nwkManagerAddr: t.NWK = t.StructField( # noqa: N815 requires=lambda s: s.ScanDuration == CHANNEL_MASK_MANAGER_ADDR_CHANGE_REQ