Skip to content

Latest commit

 

History

History
637 lines (513 loc) · 25.6 KB

File metadata and controls

637 lines (513 loc) · 25.6 KB

This file provides guidance to AI coding agents when working with code in this repository.

Overview

ZHA-Quirks provides device-specific handlers for ZHA (Zigbee Home Automation) in Home Assistant. Quirks handle devices that don't follow standard ZCL (Zigbee Cluster Library) specifications by defining custom attributes, clusters, and entity mappings.

Commands

# Setup development environment (includes uv sync and pre-commit install)
script/setup

# Sync dependencies after switching branches or pulling updates
uv sync

# Run all tests
pytest tests/

# Run a single test file
pytest tests/test_tuya.py

# Run a specific test function
pytest tests/test_tuya.py::test_function_name -v

# Run pre-commit checks (ruff, mypy, codespell)
pre-commit run --all-files

# Lint and format
ruff check zhaquirks/
ruff format zhaquirks/

# Type checking
mypy zhaquirks/

Zigbee Concepts

Clusters: Functionality groupings containing attributes and commands.

  • in_clusters (Server clusters): Control the device, send attribute reports. E.g., OnOff on a light bulb.
  • out_clusters (Client clusters): Send commands to other devices. E.g., OnOff on a remote control.

Endpoints: Groupings of clusters. Multi-gang switches have separate endpoints per switch.

Quirk Architecture

V2 Quirks (Preferred for New Quirks)

Use QuirkBuilder from zhaquirks.builder for declarative quirk definition:

from zhaquirks.builder import QuirkBuilder

(
    QuirkBuilder("Manufacturer", "Model")
    .applies_to("AltManufacturer", "Model")  # Additional models
    .replaces(CustomClusterClass)             # Replace standard cluster
    .device_automation_triggers({...})        # Button/action mappings
    .switch(attribute_name=..., fallback_name=...)  # HA entity
    .add_to_registry()
)

QuirkBuilder Methods Reference

Device Matching:

  • .applies_to(manufacturer, model) - Add manufacturer/model pair to match
  • .filter(filter_function) - Custom filter function (device) -> bool
  • .firmware_version_filter(min_version, max_version, allow_missing) - Filter by firmware version

Firmware version filtering is useful when different firmware versions need different quirks:

# Quirk for OLD firmware (before bug was fixed)
(
    QuirkBuilder("innr", "SP 240")
    .firmware_version_filter(max_version=0x191B3685, allow_missing=False)
    .replaces(OldFirmwareCluster)
    .add_to_registry()
)

# Quirk for NEW firmware (after bug was fixed)
(
    QuirkBuilder("innr", "SP 240")
    .firmware_version_filter(min_version=0x191B3685, allow_missing=True)
    .replaces(NewFirmwareCluster)
    .add_to_registry()
)
  • min_version: Minimum firmware version (inclusive)
  • max_version: Maximum firmware version (exclusive) - the version specified is NOT included
  • allow_missing: If True, quirk applies when device has no firmware version

Note: In the example above, 0x191B3685 appears in both quirks because max_version is exclusive (old quirk applies to versions before this) while min_version is inclusive (new quirk applies to this version and newer).

Cluster Modification:

  • .adds(cluster, endpoint_id=1, cluster_type=ClusterType.Server, constant_attributes={}) - Add a cluster. constant_attributes dict forces specific attribute values (same as _CONSTANT_ATTRIBUTES on a custom cluster)
  • .removes(cluster_id, endpoint_id=1, cluster_type=ClusterType.Server) - Remove a cluster
  • .replaces(replacement_cluster_class, endpoint_id=1, cluster_type=ClusterType.Server) - Replace cluster with custom implementation
  • .replace_cluster_occurrences(cluster_class, replace_server=True, replace_client=True) - Replace across all endpoints

cluster_type can be ClusterType.Server (in_clusters) or ClusterType.Client (out_clusters).

Endpoint Modification:

  • .adds_endpoint(endpoint_id, profile_id, device_type)
  • .removes_endpoint(endpoint_id)
  • .replaces_endpoint(endpoint_id, profile_id, device_type) - Change endpoint's profile/device type

Example: Change device type so HA creates correct entity (ZHA profile used by default):

.replaces_endpoint(1, device_type=zha.DeviceType.DIMMABLE_LIGHT)

Entity Creation (Home Assistant): All entity methods require fallback_name. Common parameters:

  • attribute_name: ZCL attribute to expose
  • cluster_id: Cluster containing the attribute
  • endpoint_id: Endpoint (default 1)
  • translation_key: For HA translations (required if no device_class)
  • fallback_name: English name in sentence case (always required)
  • entity_type: EntityType.STANDARD, CONFIG, or DIAGNOSTIC
  • initially_disabled: Start disabled in HA
  • device_class: HA device class for the entity
  • reporting_config: Configure ZCL attribute reporting
  • unique_id_suffix: Suffix appended to the entity's unique_id. Defaults to attribute_name (or command_name for command-based entities). Required when creating multiple entities from the same attribute/command on the same endpoint, since otherwise the default suffixes collide. See Entity unique_id format below before changing this on existing quirks.

Parameter order convention: attribute_name, cluster_id, endpoint_id first; translation_key and fallback_name always last (in that order). Use keyword arguments for clarity.

reporting_config sets up automatic attribute reporting from the device:

from zhaquirks.builder import ReportingConfig

.sensor(
    attribute_name="measured_value",
    cluster_id=VOCIndex.cluster_id,
    reporting_config=ReportingConfig(
        min_interval=60,      # Minimum seconds between reports
        max_interval=120,     # Maximum seconds between reports
        reportable_change=1,  # Minimum change to trigger report
    ),
    ...
)

Entity Methods:

# Switch (on/off control)
.switch(
    attribute_name="led_enable",
    cluster_id=CustomCluster.cluster_id,
    cluster_type=ClusterType.Server,  # Optional: default Server; use ClusterType.Client for out_clusters
    force_inverted=False,             # Optional: invert on/off
    off_value=0,                      # Optional: value written when turning off (default 0)
    on_value=1,                       # Optional: value written when turning on (default 1)
    translation_key="led_enable",
    fallback_name="LED enable",
)

# Sensor (read-only value)
.sensor(
    attribute_name="temperature",
    cluster_id=TemperatureMeasurement.cluster_id,
    cluster_type=ClusterType.Server,          # Optional: default Server
    divisor=100,                              # Optional: divide raw value (default 1)
    multiplier=1,                             # Optional: multiply raw value (default 1)
    suggested_display_precision=1,            # Optional: decimal places in HA UI
    device_class=SensorDeviceClass.TEMPERATURE,  # Optional: HA device class
    state_class=SensorStateClass.MEASUREMENT,    # Optional: HA state class
    unit=UnitOfTemperature.CELSIUS,           # Optional: use unit constants, not strings
    translation_key="temperature",
    fallback_name="Temperature",
)

# Binary Sensor (on/off state)
.binary_sensor(
    attribute_name="occupancy",
    cluster_id=OccupancySensing.cluster_id,
    cluster_type=ClusterType.Server,  # Optional: default Server
    device_class=BinarySensorDeviceClass.OCCUPANCY,  # Optional: HA device class
    translation_key="occupancy",
    fallback_name="Occupancy",
)

# Binary Sensor with attribute_converter (extract bit from zone_status)
.binary_sensor(
    attribute_name=IasZone.AttributeDefs.zone_status.name,
    cluster_id=IasZone.cluster_id,
    endpoint_id=44,
    device_class=BinarySensorDeviceClass.TAMPER,
    attribute_converter=lambda value: bool(value & IasZone.ZoneStatus.Tamper),
    unique_id_suffix="tamper",  # Required when multiple entities use same attribute
    fallback_name="Tamper",
)

# Number (adjustable value)
.number(
    attribute_name="off_to_on_delay",
    cluster_id=CustomCluster.cluster_id,
    cluster_type=ClusterType.Server,  # Optional: default Server
    min_value=0,                      # Optional: minimum allowed value
    max_value=65535,                  # Optional: maximum allowed value
    step=1,                           # Optional: step increment
    unit=UnitOfTime.SECONDS,          # Optional: unit constant
    mode="box",                       # Optional: "box" for text input, "slider" for slider
    multiplier=1,                     # Optional: scale between HA value and raw attribute (default 1)
    device_class=NumberDeviceClass.DURATION,  # Optional: HA device class
    translation_key="turn_on_delay",
    fallback_name="Turn on delay",
)

Number multiplier semantics: Defined on the entity by ZHA. HA Core is a pure pass-through — it does no scaling itself. The conversion is:

  • Display: native_value = raw_attr_value * multiplier
  • Write: raw_attr_value = int(ha_value / multiplier)

If the underlying attribute is an integer representing a fractional unit (e.g., the ZCL local_temperature_calibration attribute is int8s storing tenths of a degree), pair fractional step with the matching multiplier so HA values are scaled correctly. The attribute's raw type comes from the cluster's AttributeDefs — for .number() (unlike .tuya_number()) you don't pass a type. Without a multiplier (default 1), int() silently truncates fractional input on write and raw values are displayed unscaled.

# ZCL Thermostat local_temperature_calibration: int8s, tenths of a degree.
# HA range -2.5..2.5 °C ↔ raw attribute value -25..25
.number(
    attribute_name=Thermostat.AttributeDefs.local_temperature_calibration.name,
    cluster_id=Thermostat.cluster_id,
    min_value=-2.5,
    max_value=2.5,
    step=0.1,
    multiplier=0.1,   # required: pairs with step=0.1
    unit=UnitOfTemperature.CELSIUS,
    translation_key="local_temperature_calibration",
    fallback_name="Local temperature calibration",
)

min_value/max_value are HA-side values (after the multiplier), not raw attribute values. Confirm the device's raw value range and pick HA-side limits accordingly.

# Enum as SELECT (dropdown, default) - user can change value
.enum(
    attribute_name="mode",
    enum_class=ModeEnum,
    cluster_id=CustomCluster.cluster_id,
    cluster_type=ClusterType.Server,  # Optional: default Server
    translation_key="mode",
    fallback_name="Mode",
)

# Enum as SENSOR (read-only display)
.enum(
    attribute_name="operating_mode",
    enum_class=OperatingModeEnum,
    cluster_id=CustomCluster.cluster_id,
    entity_platform=EntityPlatform.SENSOR,  # Optional: makes it read-only (default SELECT)
    entity_type=EntityType.DIAGNOSTIC,      # Optional: default CONFIG
    translation_key="operating_mode",
    fallback_name="Operating mode",
)

# Button (write attribute on press)
.write_attr_button(
    attribute_name="reset",
    attribute_value=1,
    cluster_id=CustomCluster.cluster_id,
    cluster_type=ClusterType.Server,  # Optional: default Server
    translation_key="reset",
    fallback_name="Reset",
)

# Button (execute ZCL command on press)
.command_button(
    command_name="reset_to_factory_defaults",
    cluster_id=Basic.cluster_id,
    command_args=(),                  # Optional: positional args for command
    command_kwargs={},                # Optional: keyword args for command
    cluster_type=ClusterType.Server,  # Optional: default Server
    translation_key="factory_reset",
    fallback_name="Factory reset",
)

Entity unique_id format:

HA uses unique_id to identify an entity across restarts. If a quirk change causes it to change, HA treats the result as a new entity — the old one is orphaned and anything referencing it breaks.

For v2 quirk entities the format is:

{device.ieee}-{endpoint_id}-{suffix}

Note there is no cluster_id between the endpoint and the suffix. This differs from the format used by ZHA-native entities (see below).

{suffix} resolves in this order:

  1. Explicit unique_id_suffix= on the builder call
  2. Otherwise attribute_name (attribute-based entities)
  3. Otherwise command_name (for .command_button())
  4. Otherwise no suffix

Breaking-change implications:

  • Renaming attribute_name on a custom cluster used by an existing v2 quirk changes the default suffix and breaks existing entities. Avoid unless necessary.
  • Moving an entity to a different endpoint_id also changes the unique_id and breaks existing entities.
  • translation_key and fallback_name do not affect unique_id — renaming these is safe.
  • If a rename is genuinely required, preserve the old suffix via unique_id_suffix= on each affected entity. Flag the breakage in the PR.
  • When reviewing PRs that rename attributes in an existing v2 quirk (or move entities to different endpoints), call this out before it lands.

ZHA-native entities (not created by a v2 quirk):

Some entities are not created by a v2 quirk's entity declarations — they come from a class defined in the ZHA library itself. These entities go through ZHA's standard discovery path in PlatformEntity.__init__, which uses a different format:

{device.ieee}-{endpoint_id}-{cluster_id}-{suffix}

The cluster_id appears as a decimal integer. {suffix} comes from a hardcoded _unique_id_suffix class attribute on the ZHA-native entity class (typically matching the entity's _attribute_name, e.g. "power_outage_memory", "invert_switch", "child_lock").

Note: several primary platform-entity classes use a shorter legacy {ieee}-{endpoint_id} format (no cluster_id, no suffix) for backwards compatibility — Light, Shade, DeviceTracker, Switch (main OnOff on light/smart-plug/ballast/plug-in-unit device types), and Cover/Thermostat/Siren when the endpoint's device type matches the entity's primary type. Config switches, numbers and sensors on the same device still use the standard cluster_id-included format above.

When migrating such an entity to a quirks v2 definition, the v2 entity must produce the same unique_id as the old one or HA will treat it as a new entity. Because v2 quirk unique_ids do not auto-include the cluster_id, the v2 unique_id_suffix= must include the cluster_id explicitly to match.

Example: for a ZHA-native entity on cluster_id 0xFCC0 (= 64704) with attribute child_lock, the existing unique_id is {ieee}-1-64704-child_lock. To preserve that under v2, pass unique_id_suffix="64704-child_lock" on the corresponding .switch(...) call to get the same unique_id.

If you have access to a checkout of the ZHA library, you can find existing unique_ids in the device diagnostics dumps under zha/tests/data/devices/.

Device Automation Triggers:

# Maps device events to HA automation triggers
# Format: {(action, subtype): {COMMAND: command_name, ...}}
.device_automation_triggers({
    (SHORT_PRESS, TURN_ON): {COMMAND: COMMAND_ON},
    (SHORT_PRESS, TURN_OFF): {COMMAND: COMMAND_OFF},
    (LONG_PRESS, DIM_UP): {COMMAND: COMMAND_STEP, CLUSTER_ID: 8, ENDPOINT_ID: 1},
})

The trigger tuple (action, subtype) appears in the HA UI. The dict value must uniquely match the zha_event fired by the device.

Other Methods:

  • .friendly_name(model="...", manufacturer="...") - Override device name displayed in HA
  • .device_class(custom_device_class) - Use a custom device class (e.g., CustomDeviceV2 subclass for special request handling)
  • .skip_configuration() - Skip attribute reporting configuration
  • .add_to_registry() - Required - Registers the quirk
# Example: Show user-friendly name instead of model code
.friendly_name(
    model="Hue OmniGlow lightstrip",
    manufacturer="Philips",
)

Preventing Default Entity Creation: Hide entities that ZHA would create by default:

# Hide all entities from a cluster
.prevent_default_entity_creation(endpoint_id=1, cluster_id=BinaryInput.cluster_id)

# Hide entities matching a condition
.prevent_default_entity_creation(
    endpoint_id=1,
    cluster_id=OnOff.cluster_id,
    function=lambda entity: entity.device_class == "opening",
)

Changing Default Entity Metadata: Modify properties of entities ZHA creates by default:

# Make an entity the primary entity for the device
.change_entity_metadata(
    endpoint_id=35,
    cluster_id=IasZone.cluster_id,
    new_primary=True,
)

# Change entity category and primary status
.change_entity_metadata(
    endpoint_id=35,
    cluster_id=IasWd.cluster_id,
    new_primary=False,
    new_entity_category=EntityType.DIAGNOSTIC,
)

Available new_* parameters: new_primary, new_unique_id, new_translation_key, new_device_class, new_state_class, new_entity_category, new_fallback_name.

Tuya Devices (TuyaQuirkBuilder)

Tuya devices use datapoints (DPs) instead of ZCL attributes. Use TuyaQuirkBuilder:

from zhaquirks.tuya.builder import TuyaQuirkBuilder

(
    TuyaQuirkBuilder("_TZE200_xxx", "TS0601")
    .tuya_temperature(dp_id=1, scale=10)
    .tuya_humidity(dp_id=2)
    .tuya_battery(dp_id=4)
    .tuya_switch(dp_id=5, attribute_name="valve", fallback_name="Valve")
    .skip_configuration()
    .add_to_registry()
)

See tuya.md for detailed Tuya quirk documentation including finding DPs and all available methods.

V1 Quirks (Legacy)

V1 quirks inherit from CustomDevice with explicit signature and replacement dicts. The signature must match the device exactly; the replacement defines what ZHA should use instead:

from zhaquirks.legacy import CustomDevice
from zhaquirks.const import MODELS_INFO, ENDPOINTS, INPUT_CLUSTERS, ...

class MyDevice(CustomDevice):
    signature = {
        MODELS_INFO: [("Manufacturer", "Model")],
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
                INPUT_CLUSTERS: [Basic.cluster_id, OnOff.cluster_id],
                OUTPUT_CLUSTERS: [],
            }
        },
    }
    replacement = {
        ENDPOINTS: {
            1: {
                INPUT_CLUSTERS: [Basic.cluster_id, CustomOnOffCluster],
            }
        },
    }

Custom Clusters

Extending a ZCL cluster - Add manufacturer-specific attributes to a standard cluster:

from zhaquirks.clusters import CustomCluster
from zigpy.zcl.clusters.general import OnOff

class CustomOnOffCluster(CustomCluster, OnOff):
    """Custom OnOff with manufacturer-specific attributes."""

    class AttributeDefs(OnOff.AttributeDefs):
        custom_attr: Final = ZCLAttributeDef(
            id=0x8000, type=t.Bool, manufacturer_code=0x117C
        )

Fully custom cluster - For manufacturer-specific clusters not based on ZCL:

from zhaquirks.clusters import CustomCluster
from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef

class VOCIndex(CustomCluster):
    """Custom cluster with no ZCL base."""

    cluster_id: t.uint16_t = 0xFC7E       # Manufacturer-specific cluster ID
    name: str = "IKEA VOC Index"
    ep_attribute: str = "voc_index"        # Attribute name on endpoint

    class AttributeDefs(BaseAttributeDefs):  # Note: BaseAttributeDefs, not a ZCL cluster
        measured_value: Final = ZCLAttributeDef(
            id=0x0000, type=t.Single, access="rp", manufacturer_code=0x117C
        )

manufacturer_code: Specifies the manufacturer code to send with read/write requests for this attribute. Required for vendor-specific attributes that aren't part of the ZCL standard. Without it, the device may not recognize or respond to the attribute request. Use the hex code for the manufacturer (e.g., 0x117C for IKEA, 0x115F for Xiaomi). Set manufacturer_code=None to explicitly suppress sending a manufacturer code, even on manufacturer-specific clusters. This replaces the older is_manufacturer_specific=True approach which obtained the code from the device's NodeDescriptor.

access: Controls attribute read/write/report capabilities. Not needed to explicitly specify - defaults to "rwp". Values:

  • "r" - Read-only
  • "w" - Write-only
  • "rw" - Read and write
  • "rp" - Read and reportable (device sends reports on change)
  • "rwp" - Read, write, and reportable

Custom enum types for attribute values - Define t.enum8 or t.enum16 subclasses:

class BoschOperatingMode(t.enum8):
    """Operating mode values."""
    Schedule = 0x00
    Manual = 0x01
    Pause = 0x05

# Use in attribute definition:
operating_mode = ZCLAttributeDef(
    id=0x4007, type=BoschOperatingMode, manufacturer_code=0x1209
)

_CONSTANT_ATTRIBUTES: Force specific attribute values, overriding what the device reports. Useful when devices report incorrect values (e.g., wrong multiplier/divisor for energy metering):

class MeteringClusterFixed(CustomCluster, Metering):
    """Fix incorrect multiplier and divisor values."""

    _CONSTANT_ATTRIBUTES = {
        Metering.AttributeDefs.multiplier.id: 1,
        Metering.AttributeDefs.divisor.id: 100,
    }

Key base classes in zhaquirks/__init__.py:

  • LocalDataCluster: Prevents remote calls, responds locally
  • EventableCluster: Converts cluster requests to events

Entity Creation Rules

When adding entities with v2 quirks:

  1. fallback_name is always required - English entity name in sentence case (e.g., "Soil moisture" not "Soil Moisture"). Abbreviations like "LED" stay uppercase.

  2. translation_key is optional but required when no device class is set. Typically the attribute name or slugified entity name.

  3. Entity name priority in Home Assistant: translation_key → device_class name → fallback_name

Testing

Test fixtures are in tests/conftest.py:

# For v1 quirks
quirked = zigpy_device_from_quirk(quirk_class)

# For v2 quirks
quirked = zigpy_device_from_v2_quirk(model, manufacturer)

# Verify signature matches quirk (useful for v1 quirks)
def test_my_device_signature(assert_signature_matches_quirk):
    signature = {...}  # From HA device page "Zigbee Device Signature"
    assert_signature_matches_quirk(MyDeviceQuirk, signature)

When tests are NOT needed: Purely declarative v2 quirks that contain no custom logic do not require test coverage. This includes quirks that only use existing custom clusters (already tested elsewhere), .device_automation_triggers(), .friendly_name(), .applies_to(), .skip_configuration(), or other pure definitions. Example:

(
    QuirkBuilder("Manufacturer", "Model")
    .friendly_name(model="Wireless Mini Switch", manufacturer="Acme")
    .replaces(ExistingCustomCluster)
    .device_automation_triggers({
        (SHORT_PRESS, BUTTON): {COMMAND: COMMAND_1_SINGLE},
        (DOUBLE_PRESS, BUTTON): {COMMAND: COMMAND_1_DOUBLE},
    })
    .add_to_registry()
)

Tests are needed when a quirk introduces custom logic such as custom clusters with overridden methods (e.g., handle_cluster_request, update_attribute), attribute_converter lambdas, or custom filter functions.

Code Organization

Quirks are organized by manufacturer in zhaquirks/<manufacturer>/:

  • __init__.py: Shared clusters, constants, base classes for the manufacturer
  • <device>.py: Device-specific quirks

Key Imports

# Constants for signatures
from zhaquirks.const import (
    MODELS_INFO, ENDPOINTS, INPUT_CLUSTERS, OUTPUT_CLUSTERS,
    PROFILE_ID, DEVICE_TYPE, SKIP_CONFIGURATION,
)

# Device automation triggers
from zhaquirks.const import (
    SHORT_PRESS, LONG_PRESS, DOUBLE_PRESS, TRIPLE_PRESS,
    COMMAND, COMMAND_ON, COMMAND_OFF, COMMAND_TOGGLE,
)

# Quirk building
from zhaquirks.builder import QuirkBuilder
from zhaquirks.builder import EntityPlatform, EntityType
from zhaquirks.builder import (  # Unit constants
    UnitOfTemperature, UnitOfTime, UnitOfEnergy, UnitOfPower,
)
from zhaquirks.builder import BinarySensorDeviceClass
from zhaquirks.builder import NumberDeviceClass
from zhaquirks.builder import SensorDeviceClass, SensorStateClass
from zhaquirks.tuya.builder import TuyaQuirkBuilder

# Cluster types
from zigpy.zcl import ClusterType
from zigpy.zcl.clusters.general import Basic, OnOff, Groups, Scenes
from zigpy.zcl.clusters.measurement import TemperatureMeasurement, RelativeHumidity
import zigpy.types as t

Code Style

Avoid magic numbers for cluster IDs, attribute IDs, and command IDs. Use the cluster's definition instead:

# Good - use cluster and attribute/command references
Metering.cluster_id                            # Cluster ID (int)
Metering.AttributeDefs.multiplier.id           # Attribute ID (int)
Metering.AttributeDefs.multiplier.name         # Attribute name (str)
WindowCovering.ServerCommandDefs.go_to_lift_percentage.id  # Server command ID
IasZone.ClientCommandDefs.status_change_notification.id    # Client command ID

# Bad - magic numbers
0x0702  # What cluster is this?
0x0301  # What attribute is this?
0x00    # What command is this?

Accessing clusters on an endpoint - Use the cluster's ep_attribute (e.g., IasZone.ep_attribute is "ias_zone"):

# Access cluster on current endpoint
self.endpoint.ias_zone.update_attribute(
    IasZone.AttributeDefs.zone_status.id,
    IasZone.ZoneStatus.Alarm_1,
)

# Access cluster on a different endpoint
self.endpoint.device.endpoints[1].electrical_measurement.update_attribute(
    ElectricalMeasurement.AttributeDefs.active_power.id,
    value,
)

Handling commands in cluster request handlers - Compare by ID using command definitions:

def handle_cluster_request(self, hdr, args, *, dst_addressing=None):
    if hdr.command_id in (
        LevelControl.ServerCommandDefs.move.id,
        LevelControl.ServerCommandDefs.move_with_on_off.id,
    ):
        # Handle move command
        pass

PR Requirements

  • Run pre-commit run --all-files before submitting
  • New quirks require device diagnostics data (download from HA device page → three dots → "Download diagnostics")
  • Tests should verify entity creation