示例#1
0
class DevelcoIasZone(CustomCluster, IasZone):
    """Custom IasZone for Develco."""

    client_commands = {
        0x00:
        foundation.ZCLCommandDef(
            "status_change_notification",
            {
                "zone_status": IasZone.ZoneStatus,
                "extended_status?": t.bitmap8,
                "zone_id?": t.uint8_t,
                "delay?": t.uint16_t,
            },
            False,
        ),
        0x01:
        foundation.ZCLCommandDef(
            "enroll",
            {
                "zone_type": IasZone.ZoneType,
                "manufacturer_code": t.uint16_t
            },
            False,
        ),
    }
示例#2
0
class ScenesCluster(CustomCluster, Scenes):
    """Ikea Scenes cluster."""

    server_commands = Scenes.server_commands.copy()
    server_commands.update(
        {
            0x0007: foundation.ZCLCommandDef(
                "press",
                {"param1": t.int16s, "param2": t.int8s, "param3": t.int8s},
                False,
                is_manufacturer_specific=True,
            ),
            0x0008: foundation.ZCLCommandDef(
                "hold",
                {"param1": t.int16s, "param2": t.int8s},
                False,
                is_manufacturer_specific=True,
            ),
            0x0009: foundation.ZCLCommandDef(
                "release",
                {
                    "param1": t.int16s,
                },
                False,
                is_manufacturer_specific=True,
            ),
        }
    )
示例#3
0
 class TestCluster(zcl.Cluster):
     cluster_id = 0x1234
     ep_attribute = "test_cluster"
     server_commands = {
         0x00: foundation.ZCLCommandDef("command1", {}, False),
         0x01: foundation.ZCLCommandDef("command1", {}, False),
     }
示例#4
0
    class TestCluster(zhaquirks.kof.kof_mr101z.NoReplyMixin,
                      zigpy.quirks.CustomCluster):
        """Test Cluster Class."""

        cluster_id = 0x1234
        void_input_commands = {0x02}
        server_commands = {
            0x01: foundation.ZCLCommandDef("noop", {}, False),
            0x02: foundation.ZCLCommandDef("noop_noreply", {}, False),
        }
        client_commands = {}
示例#5
0
def test_schema():
    """Test schema parameter parsing"""

    bad_s = foundation.ZCLCommandDef(
        id=0x12,
        name="test",
        schema={
            "uh oh": t.uint16_t,
        },
        is_reply=False,
    )

    with pytest.raises(ValueError):
        bad_s.with_compiled_schema()

    s = foundation.ZCLCommandDef(
        id=0x12,
        name="test",
        schema={
            "foo": t.uint8_t,
            "bar?": t.uint16_t,
            "baz?": t.uint8_t,
        },
        is_reply=False,
    )
    s = s.with_compiled_schema()

    str(s)

    assert s.schema.foo.type is t.uint8_t
    assert not s.schema.foo.optional

    assert s.schema.bar.type is t.uint16_t
    assert s.schema.bar.optional

    assert s.schema.baz.type is t.uint8_t
    assert s.schema.baz.optional

    assert "test" in str(s) and "is_reply=False" in str(s)

    for kwargs, value in [
        (dict(foo=1), b"\x01"),
        (dict(foo=1, bar=2), b"\x01\x02\x00"),
        (dict(foo=1, bar=2, baz=3), b"\x01\x02\x00\x03"),
    ]:
        assert s.schema(**kwargs) == s.schema(*kwargs.values())
        assert s.schema(**kwargs).serialize() == value
        assert s.schema.deserialize(value) == (s.schema(**kwargs), b"")

    assert issubclass(s.schema, tuple)
示例#6
0
class WAXMANApplianceEventAlerts(CustomCluster, ApplianceEventAlerts):
    """WAXMAN specific ApplianceEventAlert cluster."""

    client_commands = {
        WAXMAN_CMDID:
        foundation.ZCLCommandDef(
            "alerts_notification",
            {
                "param1": t.uint8_t,
                "state": t.bitmap24
            },
            False,
            is_manufacturer_specific=True,
        )
    }

    def __init__(self, *args, **kwargs):
        """Init."""
        super().__init__(*args, **kwargs)
        self.endpoint.device.app_cluster = self

    def handle_cluster_request(
        self,
        hdr: foundation.ZCLHeader,
        args: List[Any],
        *,
        dst_addressing: Optional[Union[t.Addressing.Group, t.Addressing.IEEE,
                                       t.Addressing.NWK]] = None,
    ):
        """Handle a cluster command received on this cluster."""
        if hdr.command_id == WAXMAN_CMDID:
            state = bool(args[1] & 0x1000)

            self.endpoint.device.ias_bus.listener_event("update_state", state)
class AdeoManufacturerCluster(EventableCluster):
    """Custom manufacturer cluster (used for preset buttons 1-4)."""

    cluster_id = MANUFACTURER_SPECIFIC_CLUSTER_ID
    name = "AdeoManufacturerCluster"
    ep_attribute = "adeo_manufacturer_cluster"
    client_commands = {
        0x00: foundation.ZCLCommandDef(
            "preset",
            {"param1": t.uint8_t, "param2": t.uint8_t},
            is_manufacturer_specific=True,
            is_reply=False,
        )
    }

    def handle_cluster_request(
        self,
        hdr: foundation.ZCLHeader,
        args: List[Any],
        *,
        dst_addressing: Optional[
            Union[t.Addressing.Group, t.Addressing.IEEE, t.Addressing.NWK]
        ] = None,
    ):
        """Handle the cluster command."""
        if hdr.command_id == 0x0000:
            self.endpoint.device.scenes_bus.listener_event(
                "listener_event", ZHA_SEND_EVENT, "view", [SCENE_NO_GROUP, args[0]]
            )
        else:
            super().handle_cluster_request(hdr, args, dst_addressing=dst_addressing)
示例#8
0
class DanfossThermostatCluster(CustomCluster, Thermostat):
    """Danfoss custom cluster."""

    server_commands = Thermostat.server_commands.copy()
    server_commands[0x40] = foundation.ZCLCommandDef(
        "setpoint_command",
        {
            "param1": t.enum8,
            "param2": t.int16s
        },
        is_manufacturer_specific=True,
    )

    attributes = Thermostat.attributes.copy()
    attributes.update({
        0x4000: ("etrv_open_windows_detection", t.enum8, True),
        0x4003: ("external_open_windows_detected", t.Bool, True),
        0x4010: ("exercise_day_of_week", t.enum8, True),
        0x4011: ("exercise_trigger_time", t.uint16_t, True),
        0x4012: ("mounting_mode_active", t.Bool, True),
        0x4013: ("mounting_mode_control", t.Bool, True),
        0x4014: ("orientation", t.Bool, True),
        0x4015: ("external_measured_room_sensor", t.int16s, True),
        0x4016: ("radiator_covered", t.Bool, True),
        0x4020: ("control_algorithm_scale_factor", t.uint8_t, True),
        0x4030: ("heat_available", t.Bool, True),
        0x4031: ("heat_supply_request", t.Bool, True),
        0x4032: ("load_balancing_enable", t.Bool, True),
        0x4040: ("load_radiator_room_mean", t.uint16_t, True),
        0x404A: ("load_estimate_radiator", t.uint16_t, True),
        0x404B: ("regulation_setPoint_offset", t.int8s, True),
        0x404C: ("adaptation_run_control", t.enum8, True),
        0x404D: ("adaptation_run_status", t.bitmap8, True),
        0x404E: ("adaptation_run_settings", t.bitmap8, True),
        0x404F: ("preheat_status", t.Bool, True),
        0x4050: ("preheat_time", t.uint32_t, True),
        0x4051: ("window_open_feature_on_off", t.Bool, True),
        0xFFFD: ("cluster_revision", t.uint16_t, True),
    })

    async def write_attributes(self, attributes, manufacturer=None):
        """Send SETPOINT_COMMAND after setpoint change."""

        write_res = await super().write_attributes(attributes,
                                                   manufacturer=manufacturer)

        if "occupied_heating_setpoint" in attributes:
            self.debug("sending setpoint command: %s",
                       attributes["occupied_heating_setpoint"])
            await self.setpoint_command(
                0x01,
                attributes["occupied_heating_setpoint"],
                manufacturer=manufacturer)

        return write_res
示例#9
0
class LedvanceLightCluster(CustomCluster):
    """LedvanceLightCluster."""

    cluster_id = 0xFC01
    ep_attribute = "ledvance_light"
    name = "LedvanceLight"
    server_commands = {
        0x0001: foundation.ZCLCommandDef(
            "save_defaults", {}, False, is_manufacturer_specific=True
        )
    }
示例#10
0
    class XBeeGroupResponse(zigpy.quirks.CustomCluster, Groups):
        cluster_id = 0x8006
        ep_attribute = "xbee_groups_response"

        client_commands = {
            **Groups.client_commands,
            0x04:
            foundation.ZCLCommandDef("remove_all_response",
                                     {"status": foundation.Status},
                                     is_reply=True),
        }
示例#11
0
def test_zcl_command_item_access_warning():
    s = foundation.ZCLCommandDef(
        id=0x12,
        name="test",
        schema={
            "foo": t.uint8_t,
        },
        is_reply=False,
    )

    with pytest.deprecated_call():
        assert s[0] == s.name
        assert s[1] == s.schema
        assert s[2] == s.is_reply
示例#12
0
def test_invalid_command_def_name():
    command = foundation.ZCLCommandDef(
        id=0x12,
        name="test",
        schema={
            "foo": t.uint8_t,
        },
        is_reply=False,
    )

    with pytest.raises(ValueError):
        command.replace(name="bad name")

    with pytest.raises(ValueError):
        command.replace(name="123name")
示例#13
0
class DevelcoIASZone(CustomCluster, IasZone):
    """IAS Zone."""

    client_commands = IasZone.client_commands.copy()
    client_commands[0x0000] = foundation.ZCLCommandDef(
        "status_change_notification",
        {
            "zone_status": IasZone.ZoneStatus,
            "extended_status": t.bitmap8,
            # These two should not be optional
            "zone_id?": t.uint8_t,
            "delay?": t.uint16_t,
        },
        False,
        is_manufacturer_specific=True,
    )
def test_tuya_cluster_request_no_handler(default_rsp_mock, TuyaCluster):
    """Test cluster specific request handler -- no handler."""

    hdr = zcl_f.ZCLHeader.general(1, 0xFE, is_reply=True)
    hdr.frame_control.disable_default_response = False

    new_client_commands = TuyaCluster.client_commands.copy()
    new_client_commands[0xFE] = zcl_f.ZCLCommandDef(
        "no_such_handler", {}, is_manufacturer_specific=True
    )

    with mock.patch.object(TuyaCluster, "client_commands", new_client_commands):
        TuyaCluster.handle_cluster_request(hdr, (mock.sentinel.args,))

    assert default_rsp_mock.call_count == 1
    assert default_rsp_mock.call_args[1]["status"] == zcl_f.Status.UNSUP_CLUSTER_COMMAND
示例#15
0
def convert_list_schema(schema: Sequence[type], command_id: int,
                        is_reply: bool) -> type[t.Struct]:
    schema_dict = {}

    for i, param_type in enumerate(schema, start=1):
        name = f"param{i}"
        real_type = next(c for c in param_type.__mro__
                         if c.__name__ != "Optional")

        if real_type is not param_type:
            name += "?"

        schema_dict[name] = real_type

    temp = foundation.ZCLCommandDef(schema=schema_dict,
                                    is_reply=is_reply,
                                    id=command_id,
                                    name="schema")

    return temp.with_compiled_schema().schema
示例#16
0
class TuyaMCUCluster(TuyaAttributesCluster, TuyaNewManufCluster):
    """Manufacturer specific cluster for sending Tuya MCU commands."""
    class MCUVersion(t.Struct):
        """Tuya MCU version response Zcl payload."""

        status: t.uint8_t
        tsn: t.uint8_t
        version_raw: t.uint8_t

        @property
        def version(self) -> str:
            """Format the raw version to X.Y.Z."""

            if self.version_raw:
                # MCU version is 1 byte length
                # is converted from HEX -> BIN -> XX.XX.XXXX -> DEC (x.y.z)
                # example: 0x98 -> 10011000 -> 10.01.1000 -> 2.1.8
                # https://developer.tuya.com/en/docs/iot-device-dev/firmware-version-description?id=K9zzuc5n2gff8#title-1-Zigbee%20firmware%20versions
                major = self.version_raw >> 6
                minor = (self.version_raw & 63) >> 4
                release = self.version_raw & 15

                return "{}.{}.{}".format(major, minor, release)

            return None

    attributes = TuyaNewManufCluster.attributes.copy()
    attributes.update({
        # MCU version
        ATTR_MCU_VERSION: ("mcu_version", t.uint48_t, True),
    })

    client_commands = TuyaNewManufCluster.client_commands.copy()
    client_commands.update({
        TUYA_MCU_VERSION_RSP:
        foundation.ZCLCommandDef(
            "mcu_version_response",
            {"version": MCUVersion},
            True,
            is_manufacturer_specific=True,
        ),
    })

    def __init__(self, *args, **kwargs):
        """Init."""
        super().__init__(*args, **kwargs)
        # Cluster for endpoint: 1 (listen MCU commands)
        self.endpoint.device.command_bus = Bus()
        self.endpoint.device.command_bus.add_listener(self)

    def from_cluster_data(self,
                          data: TuyaClusterData) -> Optional[TuyaCommand]:
        """Convert from cluster data to a tuya data payload."""

        dp, mapping = self.get_dp_mapping(data.endpoint_id, data.cluster_attr)
        self.debug("from_cluster_data: %s, %s", dp, mapping)
        if dp:
            cmd_payload = TuyaCommand()
            cmd_payload.status = 0
            cmd_payload.tsn = self.endpoint.device.application.get_sequence()
            cmd_payload.dp = dp
            cmd_payload.data = TuyaData()
            datapoint_type = mapping.dp_type
            cmd_payload.data.dp_type = datapoint_type
            cmd_payload.data.function = 0
            val = data.attr_value
            if mapping.dp_converter:
                val = mapping.dp_converter(val)
                self.debug("converted: %s", val)
            if datapoint_type.ztype:
                val = datapoint_type.ztype(val)
                self.debug("ztype: %s", val)
            val = Data.from_value(val)
            self.debug("from_value: %s", val)
            cmd_payload.data.raw = t.LVBytes.deserialize(val)[0]
            self.debug("raw: %s", cmd_payload.data.raw)
            return cmd_payload
        else:
            self.warning(
                "No cluster_dp found for %s, %s",
                data.endpoint_id,
                data.cluster_attr,
            )
            return None

    def tuya_mcu_command(self, cluster_data: TuyaClusterData):
        """Tuya MCU command listener. Only manufacturer endpoint must listen to MCU commands."""

        self.debug(
            "tuya_mcu_command: cluster_data=%s",
            cluster_data,
        )

        tuya_command = self.from_cluster_data(cluster_data)
        self.debug("tuya_command: %s", tuya_command)
        if tuya_command:
            self.create_catching_task(
                self.command(
                    TUYA_SET_DATA,
                    tuya_command,
                    expect_reply=cluster_data.expect_reply,
                    manufacturer=cluster_data.manufacturer,
                ))
        else:
            self.warning(
                "no MCU command for data %s",
                cluster_data,
            )

    def get_dp_mapping(
            self, endpoint_id: int,
            attribute_name: str) -> Optional[Tuple[int, DPToAttributeMapping]]:
        """Search for the DP in dp_to_attribute."""

        for dp, dp_mapping in self.dp_to_attribute.items():
            if (attribute_name
                    == dp_mapping.attribute_name) and (endpoint_id in [
                        dp_mapping.endpoint_id, self.endpoint.endpoint_id
                    ]):
                self.debug("get_dp_mapping --> found DP: %s", dp)
                return [dp, dp_mapping]
        return [None, None]

    def handle_mcu_version_response(self,
                                    payload: MCUVersion) -> foundation.Status:
        """Handle MCU version response."""

        self.debug("MCU version: %s", payload.version)
        self.update_attribute("mcu_version", payload.version)
        return foundation.Status.SUCCESS
示例#17
0
class PhilipsRemoteCluster(CustomCluster):
    """Philips remote cluster."""

    cluster_id = 64512
    name = "PhilipsRemoteCluster"
    ep_attribute = "philips_remote_cluster"
    client_commands = {
        0x00:
        foundation.ZCLCommandDef(
            "notification",
            {
                "param1": t.uint8_t,
                "param2": t.uint24_t,
                "param3": t.uint8_t,
                "param4": t.uint8_t,
                "param5": t.uint8_t,
                "param6": t.uint8_t,
            },
            is_manufacturer_specific=True,
            is_reply=False,
        )
    }
    BUTTONS = {
        1: "left",
        2: "right",
    }
    PRESS_TYPES = {
        0: "press",
        1: "hold",
        2: "press_release",
        3: "hold_release"
    }

    def handle_cluster_request(
        self,
        hdr: foundation.ZCLHeader,
        args: List[Any],
        *,
        dst_addressing: Optional[Union[t.Addressing.Group, t.Addressing.IEEE,
                                       t.Addressing.NWK]] = None,
    ):
        """Handle the cluster command."""
        _LOGGER.debug(
            "PhilipsRemoteCluster - handle_cluster_request tsn: [%s] command id: %s - args: [%s]",
            hdr.tsn,
            hdr.command_id,
            args,
        )

        button = self.BUTTONS.get(args[0], args[0])
        press_type = self.PRESS_TYPES.get(args[2], args[2])

        event_args = {
            BUTTON: button,
            PRESS_TYPE: press_type,
            COMMAND_ID: hdr.command_id,
            ARGS: args,
        }

        action = f"{button}_{press_type}"
        self.listener_event(ZHA_SEND_EVENT, action, event_args)
示例#18
0
class Inovelli_VZM31SN_Cluster(CustomCluster):
    """Inovelli VZM31-SN custom cluster."""

    cluster_id = 0xFC31
    name = "InovelliVZM31SNCluster"
    ep_attribute = "inovelli_vzm31sn_cluster"

    attributes = ManufacturerSpecificCluster.attributes.copy()
    attributes.update({
        0x0001: ("dimming_speed_up_remote", t.uint8_t, True),
        0x0002: ("dimming_speed_up_local", t.uint8_t, True),
        0x0003: ("ramp_rate_off_to_on_local", t.uint8_t, True),
        0x0004: ("ramp_rate_off_to_on_remote", t.uint8_t, True),
        0x0005: ("dimming_speed_down_remote", t.uint8_t, True),
        0x0006: ("dimming_speed_down_local", t.uint8_t, True),
        0x0007: ("ramp_rate_on_to_off_local", t.uint8_t, True),
        0x0008: ("ramp_rate_on_to_off_remote", t.uint8_t, True),
        0x0009: ("minimum_level", t.uint8_t, True),
        0x000A: ("maximum_level", t.uint8_t, True),
        0x000B: ("invert_switch", t.Bool, True),
        0x000C: ("auto_off_timer", t.uint16_t, True),
        0x000D: ("default_level_local", t.uint8_t, True),
        0x000E: ("default_level_remote", t.uint8_t, True),
        0x000F: ("state_after_power_restored", t.uint8_t, True),
        0x0011: ("load_level_indicator_timeout", t.uint8_t, True),
        0x0012: ("active_power_reports", t.uint8_t, True),
        0x0013: ("periodic_power_and_energy_reports", t.uint8_t, True),
        0x0014: ("active_energy_reports", t.uint16_t, True),
        0x0015: ("power_type", t.uint8_t, True),
        0x0016: ("switch_type", t.uint8_t, True),
        0x0032: ("button_delay", t.uint8_t, True),
        0x0033: ("device_bind_number", t.uint8_t, True),
        0x0034: ("smart_bulb_mode", t.Bool, True),
        0x0035: ("double_tap_up_for_full_brightness", t.Bool, True),
        0x003C: ("default_led1_strip_color_when_on", t.uint8_t, True),
        0x003D: ("default_led1_strip_color_when_off", t.uint8_t, True),
        0x003E: ("default_led1_strip_intensity_when_on", t.uint8_t, True),
        0x003F: ("default_led1_strip_intensity_when_off", t.uint8_t, True),
        0x0041: ("default_led2_strip_color_when_on", t.uint8_t, True),
        0x0042: ("default_led2_strip_color_when_off", t.uint8_t, True),
        0x0043: ("default_led2_strip_intensity_when_on", t.uint8_t, True),
        0x0044: ("default_led2_strip_intensity_when_off", t.uint8_t, True),
        0x0046: ("default_led3_strip_color_when_on", t.uint8_t, True),
        0x0047: ("default_led3_strip_color_when_off", t.uint8_t, True),
        0x0048: ("default_led3_strip_intensity_when_on", t.uint8_t, True),
        0x0049: ("default_led3_strip_intensity_when_off", t.uint8_t, True),
        0x004B: ("default_led4_strip_color_when_on", t.uint8_t, True),
        0x004C: ("default_led4_strip_color_when_off", t.uint8_t, True),
        0x004D: ("default_led4_strip_intensity_when_on", t.uint8_t, True),
        0x004E: ("default_led4_strip_intensity_when_off", t.uint8_t, True),
        0x0050: ("default_led5_strip_color_when_on", t.uint8_t, True),
        0x0051: ("default_led5_strip_color_when_off", t.uint8_t, True),
        0x0052: ("default_led5_strip_intensity_when_on", t.uint8_t, True),
        0x0053: ("default_led5_strip_intensity_when_off", t.uint8_t, True),
        0x0055: ("default_led6_strip_color_when_on", t.uint8_t, True),
        0x0056: ("default_led6_strip_color_when_off", t.uint8_t, True),
        0x0057: ("default_led6_strip_intensity_when_on", t.uint8_t, True),
        0x0058: ("default_led6_strip_intensity_when_off", t.uint8_t, True),
        0x005A: ("default_led7_strip_color_when_on", t.uint8_t, True),
        0x005B: ("default_led7_strip_color_when_off", t.uint8_t, True),
        0x005C: ("default_led7_strip_intensity_when_on", t.uint8_t, True),
        0x005D: ("default_led7_strip_intensity_when_off", t.uint8_t, True),
        0x005F: ("led_color_when_on", t.uint8_t, True),
        0x0060: ("led_color_when_off", t.uint8_t, True),
        0x0061: ("led_intensity_when_on", t.uint8_t, True),
        0x0062: ("led_intensity_when_off", t.uint8_t, True),
        0x0100: ("local_protection", t.Bool, True),
        0x0101: ("remote_protection", t.Bool, True),
        0x0102: ("output_mode", t.Bool, True),
        0x0103: ("on_off_led_mode", t.Bool, True),
        0x0104: ("firmware_progress_led", t.Bool, True),
        0x0105: ("relay_click_in_on_off_mode", t.Bool, True),
    })

    server_commands = {
        0x00:
        foundation.ZCLCommandDef(
            "button_event",
            {
                "button_pressed": t.uint8_t,
                "press_type": t.uint8_t
            },
            is_reply=False,
            is_manufacturer_specific=True,
        ),
        0x01:
        foundation.ZCLCommandDef(
            "led_effect",
            {
                "led_effect": t.uint8_t,
                "led_color": t.uint8_t,
                "led_level": t.uint8_t,
                "led_duration": t.uint8_t,
            },
            is_reply=False,
            is_manufacturer_specific=True,
        ),  # LED Effect
        0x03:
        foundation.ZCLCommandDef(
            "individual_led_effect",
            {
                "led_number": t.uint8_t,
                "led_effect": t.uint8_t,
                "led_color": t.uint8_t,
                "led_level": t.uint8_t,
                "led_duration": t.uint8_t,
            },
            is_reply=False,
            is_manufacturer_specific=True,
        ),  # individual LED Effect
    }

    def handle_cluster_request(
        self,
        hdr: foundation.ZCLHeader,
        args: List[Any],
        *,
        dst_addressing: Optional[Union[t.Addressing.Group, t.Addressing.IEEE,
                                       t.Addressing.NWK]] = None,
    ):
        """Handle a cluster request."""
        _LOGGER.debug(
            "%s: handle_cluster_request - Command: %s Data: %s",
            self.name,
            hdr.command_id,
            args,
        )
        if hdr.command_id == self.commands_by_name["button_event"].id:
            button = BUTTONS[args.button_pressed]
            press_type = PRESS_TYPES[args.press_type]
            action = f"{button}_{press_type}"
            event_args = {
                BUTTON: button,
                PRESS_TYPE: press_type,
                COMMAND_ID: hdr.command_id,
            }
            self.listener_event(ZHA_SEND_EVENT, action, event_args)
            return
示例#19
0
class F000LevelControlCluster(NoManufacturerCluster, LevelControl):
    """LevelControlCluster that reports to attrid 0xF000."""

    server_commands = LevelControl.server_commands.copy()
    server_commands[TUYA_CUSTOM_LEVEL_COMMAND] = foundation.ZCLCommandDef(
        "moveToLevelTuya",
        (TuyaLevelPayload, ),
        is_manufacturer_specific=False,
    )

    attributes = LevelControl.attributes.copy()
    attributes.update({
        # 0xF000
        TUYA_LEVEL_ATTRIBUTE: ("manufacturer_current_level", t.uint16_t),
        # 0xFC02
        TUYA_BULB_TYPE_ATTRIBUTE: ("bulb_type", TuyaBulbType),
        # 0xFC03
        TUYA_MIN_LEVEL_ATTRIBUTE: ("manufacturer_min_level", t.uint16_t),
        # 0xFC04
        TUYA_MAX_LEVEL_ATTRIBUTE: ("manufacturer_max_level", t.uint16_t),
    })

    # 0xF000 reported values are 10-1000, convert to 0-254
    def _update_attribute(self, attrid, value):
        if attrid == TUYA_LEVEL_ATTRIBUTE:
            self.debug(
                "Getting brightness %s",
                value,
            )
            value = (value + 4 - 10) * 254 // (1000 - 10)
            attrid = 0x0000

        super()._update_attribute(attrid, value)

    async def command(
        self,
        command_id: Union[foundation.Command, int, t.uint8_t],
        *args,
        manufacturer: Optional[Union[int, t.uint16_t]] = None,
        expect_reply: bool = True,
        tsn: Optional[Union[int, t.uint8_t]] = None,
    ):
        """Override the default Cluster command."""
        self.debug(
            "Sending Cluster Command. Cluster Command is %x, Arguments are %s",
            command_id,
            args,
        )
        # move_to_level, move, move_to_level_with_on_off
        if command_id in (0x0000, 0x0001, 0x0004):
            # convert dim values to 10-1000
            brightness = args[0] * (1000 - 10) // 254 + 10
            self.debug(
                "Setting brightness to %s",
                brightness,
            )
            return await super().command(
                TUYA_CUSTOM_LEVEL_COMMAND,
                TuyaLevelPayload(level=brightness, transtime=0),
                manufacturer=manufacturer,
                expect_reply=expect_reply,
                tsn=tsn,
            )

        return super().command(command_id, *args, manufacturer, expect_reply,
                               tsn)
示例#20
0
class TuyaSmartRemoteOnOffCluster(OnOff, EventableCluster):
    """TuyaSmartRemoteOnOffCluster: fire events corresponding to press type."""

    rotate_type = {
        0x00: RIGHT,
        0x01: LEFT,
        0x02: STOP,
    }
    press_type = {
        0x00: SHORT_PRESS,
        0x01: DOUBLE_PRESS,
        0x02: LONG_PRESS,
    }
    name = "TS004X_cluster"
    ep_attribute = "TS004X_cluster"
    attributes = OnOff.attributes.copy()
    attributes.update({0x8001: ("backlight_mode", SwitchBackLight)})
    attributes.update({0x8002: ("power_on_state", PowerOnState)})
    attributes.update({0x8004: ("switch_mode", SwitchMode)})

    def __init__(self, *args, **kwargs):
        """Init."""
        self.last_tsn = -1
        super().__init__(*args, **kwargs)

    server_commands = OnOff.server_commands.copy()
    server_commands.update({
        0xFC:
        foundation.ZCLCommandDef(
            "rotate_type",
            {"rotate_type": t.uint8_t},
            False,
            is_manufacturer_specific=True,
        ),
        0xFD:
        foundation.ZCLCommandDef(
            "press_type",
            {"press_type": t.uint8_t},
            False,
            is_manufacturer_specific=True,
        ),
    })

    def handle_cluster_request(
        self,
        hdr: foundation.ZCLHeader,
        args: List[Any],
        *,
        dst_addressing: Optional[Union[t.Addressing.Group, t.Addressing.IEEE,
                                       t.Addressing.NWK]] = None,
    ):
        """Handle press_types command."""
        # normally if default response sent, TS004x wouldn't send such repeated zclframe (with same sequence number),
        # but for stability reasons (e. g. the case the response doesn't arrive the device), we can simply ignore it
        if hdr.tsn == self.last_tsn:
            _LOGGER.debug("TS004X: ignoring duplicate frame")
            return
        # save last sequence number
        self.last_tsn = hdr.tsn

        # send default response (as soon as possible), so avoid repeated zclframe from device
        if not hdr.frame_control.disable_default_response:
            self.debug("TS004X: send default response")
            self.send_default_rsp(hdr, status=foundation.Status.SUCCESS)
        # handle command
        if hdr.command_id == 0xFC:
            rotate_type = args[0]
            self.listener_event(ZHA_SEND_EVENT,
                                self.rotate_type.get(rotate_type, "unknown"),
                                [])
        elif hdr.command_id == 0xFD:
            press_type = args[0]
            self.listener_event(ZHA_SEND_EVENT,
                                self.press_type.get(press_type, "unknown"), [])
示例#21
0
class TuyaManufCluster(CustomCluster):
    """Tuya manufacturer specific cluster."""

    name = "Tuya Manufacturer Specicific"
    cluster_id = TUYA_CLUSTER_ID
    ep_attribute = "tuya_manufacturer"
    set_time_offset = 0
    set_time_local_offset = None

    class Command(t.Struct):
        """Tuya manufacturer cluster command."""

        status: t.uint8_t
        tsn: t.uint8_t
        command_id: t.uint16_t
        function: t.uint8_t
        data: Data

    class MCUVersionRsp(t.Struct):
        """Tuya MCU version response Zcl payload."""

        tsn: t.uint16_t
        version: t.uint8_t

    """ Time sync command (It's transparent between MCU and server)
            Time request device -> server
               payloadSize = 0
            Set time, server -> device
               payloadSize, should be always 8
               payload[0-3] - UTC timestamp (big endian)
               payload[4-7] - Local timestamp (big endian)

            Zigbee payload is very similar to the UART payload which is described here: https://developer.tuya.com/en/docs/iot/device-development/access-mode-mcu/zigbee-general-solution/tuya-zigbee-module-uart-communication-protocol/tuya-zigbee-module-uart-communication-protocol?id=K9ear5khsqoty#title-10-Time%20synchronization

            Some devices need the timestamp in seconds from 1/1/1970 and others in seconds from 1/1/2000.
            Also, there is devices which uses both timestamps variants (probably bug). Use set_time_local_offset var in this cases.

            NOTE: You need to wait for time request before setting it. You can't set time without request."""

    server_commands = {
        0x0000:
        foundation.ZCLCommandDef("set_data", {"param": Command},
                                 False,
                                 is_manufacturer_specific=True),
        0x0010:
        foundation.ZCLCommandDef(
            "mcu_version_req",
            {"param": t.uint16_t},
            False,
            is_manufacturer_specific=True,
        ),
        0x0024:
        foundation.ZCLCommandDef("set_time", {"param": TuyaTimePayload},
                                 False,
                                 is_manufacturer_specific=True),
    }

    client_commands = {
        0x0001:
        foundation.ZCLCommandDef("get_data", {"param": Command},
                                 True,
                                 is_manufacturer_specific=True),
        0x0002:
        foundation.ZCLCommandDef("set_data_response", {"param": Command},
                                 True,
                                 is_manufacturer_specific=True),
        0x0006:
        foundation.ZCLCommandDef(
            "active_status_report",
            {"param": Command},
            True,
            is_manufacturer_specific=True,
        ),
        0x0011:
        foundation.ZCLCommandDef(
            "mcu_version_rsp",
            {"param": MCUVersionRsp},
            True,
            is_manufacturer_specific=True,
        ),
        0x0024:
        foundation.ZCLCommandDef("set_time_request", {"param": t.data16},
                                 True,
                                 is_manufacturer_specific=True),
    }

    def __init__(self, *args, **kwargs):
        """Init."""
        super().__init__(*args, **kwargs)
        self.endpoint.device.command_bus = Bus()
        self.endpoint.device.command_bus.add_listener(
            self)  # listen MCU commands

    def tuya_mcu_command(self, command: Command):
        """Tuya MCU command listener. Only endpoint:1 must listen to MCU commands."""

        self.create_catching_task(
            self.command(TUYA_SET_DATA, command, expect_reply=True))

    def handle_cluster_request(
        self,
        hdr: foundation.ZCLHeader,
        args: Tuple,
        *,
        dst_addressing: Optional[Union[t.Addressing.Group, t.Addressing.IEEE,
                                       t.Addressing.NWK]] = None,
    ) -> None:
        """Handle time request."""

        if hdr.command_id != 0x0024 or self.set_time_offset == 0:
            return super().handle_cluster_request(
                hdr, args, dst_addressing=dst_addressing)

        # Send default response because the MCU expects it
        if not hdr.frame_control.disable_default_response:
            self.send_default_rsp(hdr, status=foundation.Status.SUCCESS)

        _LOGGER.debug(
            "[0x%04x:%s:0x%04x] Got set time request (command 0x%04x)",
            self.endpoint.device.nwk,
            self.endpoint.endpoint_id,
            self.cluster_id,
            hdr.command_id,
        )
        payload = TuyaTimePayload()
        utc_timestamp = int(
            (datetime.datetime.utcnow() -
             datetime.datetime(self.set_time_offset, 1, 1)).total_seconds())
        local_timestamp = int((datetime.datetime.now() - datetime.datetime(
            self.set_time_local_offset or self.set_time_offset, 1,
            1)).total_seconds())
        payload.extend(utc_timestamp.to_bytes(4, "big", signed=False))
        payload.extend(local_timestamp.to_bytes(4, "big", signed=False))

        self.create_catching_task(super().command(TUYA_SET_TIME,
                                                  payload,
                                                  expect_reply=False))
示例#22
0
    def __init_subclass__(cls):
        # Fail on deprecated attribute presence
        for a in ("attributes", "client_commands", "server_commands"):
            if not hasattr(cls, f"manufacturer_{a}"):
                continue

            raise TypeError(
                f"`manufacturer_{a}` is deprecated. Copy the parent class's `{a}`"
                f" dictionary and update it with your manufacturer-specific `{a}`. Make"
                f" sure to specify that it is manufacturer-specific through the "
                f" appropriate constructor or tuple!")

        if cls.cluster_id is not None:
            cls.cluster_id = t.ClusterId(cls.cluster_id)

        # Clear the caches and lookup tables. Their contents should correspond exactly
        # to what's in their respective command/attribute dictionaries.
        cls.attributes_by_name = {}
        cls.commands_by_name = {}
        cls._server_commands_idx = {}
        cls._client_commands_idx = {}

        # Compile command definitions
        for commands, index in [
            (cls.server_commands, cls._server_commands_idx),
            (cls.client_commands, cls._client_commands_idx),
        ]:
            for command_id, command in list(commands.items()):
                if isinstance(command, tuple):
                    # Backwards compatibility with old command tuples
                    name, schema, is_reply = command
                    command = foundation.ZCLCommandDef(
                        id=command_id,
                        name=name,
                        schema=convert_list_schema(schema, command_id,
                                                   is_reply),
                        is_reply=is_reply,
                    )
                else:
                    command = command.replace(id=command_id)

                if command.name in cls.commands_by_name:
                    raise TypeError(
                        f"Command name {command} is not unique in {cls}: {cls.commands_by_name}"
                    )

                index[command.name] = command.id

                command = command.with_compiled_schema()
                commands[command.id] = command
                cls.commands_by_name[command.name] = command

        # Compile attributes
        for attr_id, attr in list(cls.attributes.items()):
            if isinstance(attr, tuple):
                if len(attr) == 2:
                    attr_name, attr_type = attr
                    attr_manuf_specific = False
                else:
                    attr_name, attr_type, attr_manuf_specific = attr

                attr = foundation.ZCLAttributeDef(
                    id=attr_id,
                    name=attr_name,
                    type=attr_type,
                    is_manufacturer_specific=attr_manuf_specific,
                )
            else:
                attr = attr.replace(id=attr_id)

            cls.attributes[attr.id] = attr
            cls.attributes_by_name[attr.name] = attr

        if cls._skip_registry:
            return

        if cls.cluster_id is not None:
            cls._registry[cls.cluster_id] = cls

        if cls.cluster_id_range is not None:
            cls._registry_range[cls.cluster_id_range] = cls
示例#23
0
class ZigfredCluster(CustomCluster):
    """Siglis manufacturer specific cluster for zigfred."""

    name = "Siglis Manufacturer Specific"
    cluster_id = ZIGFRED_CLUSTER_ID
    buttons_attribute_id = ZIGFRED_CLUSTER_BUTTONS_ATTRIBUTE_ID

    server_commands = {
        ZIGFRED_CLUSTER_COMMAND_BUTTON_EVENT: foundation.ZCLCommandDef(
            "button_event",
            {"param1": t.uint32_t},
            is_reply=False,
            is_manufacturer_specific=True,
        ),
    }

    def _process_button_event(self, value: t.uint32_t):
        button_lookup = {
            0: BUTTON_1,
            1: BUTTON_2,
            2: BUTTON_3,
            3: BUTTON_4,
        }

        press_type_lookup = {
            0: LONG_RELEASE,
            1: SHORT_PRESS,
            2: DOUBLE_PRESS,
            3: LONG_PRESS,
        }

        button = value & 0xFF
        press_type = (value >> 8) & 0xFF

        button = button_lookup[button]
        press_type = press_type_lookup[press_type]

        action = f"{button}_{press_type}"

        event_args = {
            BUTTON: button,
            PRESS_TYPE: press_type,
        }

        _LOGGER.info(f"Got button press on zigfred cluster: {action}")

        if button and press_type:
            self.listener_event(ZHA_SEND_EVENT, action, event_args)

    def handle_cluster_request(
        self,
        hdr: foundation.ZCLHeader,
        args: List[Any],
        *,
        dst_addressing: Optional[
            Union[t.Addressing.Group, t.Addressing.IEEE, t.Addressing.NWK]
        ] = None,
    ) -> None:
        """Handle cluster specific commands."""
        if hdr.command_id == ZIGFRED_CLUSTER_COMMAND_BUTTON_EVENT:
            self._process_button_event(args[0])
示例#24
0
class PhilipsRemoteCluster(CustomCluster):
    """Philips remote cluster."""

    cluster_id = 0xFC00
    name = "PhilipsRemoteCluster"
    ep_attribute = "philips_remote_cluster"
    client_commands = {
        0x0000:
        foundation.ZCLCommandDef(
            "notification",
            {
                "button": t.uint8_t,
                "param2": t.uint24_t,
                "press_type": t.uint8_t,
                "param4": t.uint8_t,
                "param5": t.uint8_t,
                "param6": t.uint8_t,
            },
            False,
            is_manufacturer_specific=True,
        )
    }
    BUTTONS = {1: "on", 2: "up", 3: "down", 4: "off"}
    PRESS_TYPES = {
        0: "press",
        1: "hold",
        2: "short_release",
        3: "long_release"
    }

    button_press_queue = ButtonPressQueue()

    def handle_cluster_request(
        self,
        hdr: foundation.ZCLHeader,
        args: List[Any],
        *,
        dst_addressing: Optional[Union[t.Addressing.Group, t.Addressing.IEEE,
                                       t.Addressing.NWK]] = None,
    ):
        """Handle the cluster command."""
        _LOGGER.debug(
            "PhilipsRemoteCluster - handle_cluster_request tsn: [%s] command id: %s - args: [%s]",
            hdr.tsn,
            hdr.command_id,
            args,
        )

        button = self.BUTTONS.get(args[0], args[0])
        press_type = self.PRESS_TYPES.get(args[2], args[2])

        event_args = {
            BUTTON: button,
            PRESS_TYPE: press_type,
            COMMAND_ID: hdr.command_id,
            ARGS: args,
        }

        def send_press_event(click_count):
            _LOGGER.debug(
                "PhilipsRemoteCluster - send_press_event click_count: [%s]",
                click_count)
            press_type = None
            if click_count == 1:
                press_type = "press"
            elif click_count == 2:
                press_type = "double_press"
            elif click_count == 3:
                press_type = "triple_press"
            elif click_count == 4:
                press_type = "quadruple_press"
            elif click_count > 4:
                press_type = "quintuple_press"

            if press_type:
                # Override PRESS_TYPE
                event_args[PRESS_TYPE] = press_type
                action = f"{button}_{press_type}"
                self.listener_event(ZHA_SEND_EVENT, action, event_args)

        # Derive Multiple Presses
        if press_type == "press":
            self.button_press_queue.press(send_press_event, button)
        else:
            action = f"{button}_{press_type}"
            self.listener_event(ZHA_SEND_EVENT, action, event_args)
示例#25
0
class SengledE1EG7FManufacturerSpecificCluster(CustomCluster):
    """Sengled E1E-G7F manufacturer-specific cluster."""

    cluster_id = 0xFC10
    name = "Sengled Manufacturer Specific"
    ep_attribute = "sengled_manufacturer_specific"

    server_commands = {
        0x0000:
        foundation.ZCLCommandDef(
            name="command",
            schema={
                "param1": t.uint8_t,
                "param2": t.uint8_t,
                "param3": t.uint8_t,
                "param4": t.uint8_t,
            },
            is_reply=False,
            is_manufacturer_specific=True,
        )
    }

    def handle_cluster_request(
        self,
        hdr: foundation.ZCLHeader,
        args: List[Any],
        *,
        dst_addressing: Optional[Union[t.Addressing.Group, t.Addressing.IEEE,
                                       t.Addressing.NWK]] = None,
    ):
        """Handle cluster request."""

        if args[0] == 1:
            self.endpoint.device.on_off_bus.listener_event(
                "listener_event", ZHA_SEND_EVENT, COMMAND_ON, [])
        elif args[0] == 2:
            if args[2] == 2:
                self.endpoint.device.level_control_bus.listener_event(
                    "listener_event", ZHA_SEND_EVENT, COMMAND_STEP, [0, 2, 0])
            else:
                self.endpoint.device.level_control_bus.listener_event(
                    "listener_event", ZHA_SEND_EVENT, COMMAND_STEP, [0, 1, 0])
        elif args[0] == 3:
            if args[2] == 2:
                self.endpoint.device.level_control_bus.listener_event(
                    "listener_event", ZHA_SEND_EVENT, COMMAND_STEP, [1, 2, 0])
            else:
                self.endpoint.device.level_control_bus.listener_event(
                    "listener_event", ZHA_SEND_EVENT, COMMAND_STEP, [1, 1, 0])
        elif args[0] == 4:
            self.endpoint.device.on_off_bus.listener_event(
                "listener_event", ZHA_SEND_EVENT, COMMAND_OFF, [])
        elif args[0] == 5:
            self.endpoint.device.on_off_bus.listener_event(
                "listener_event", ZHA_SEND_EVENT, "on_double", [])
        elif args[0] == 6:
            self.endpoint.device.on_off_bus.listener_event(
                "listener_event", ZHA_SEND_EVENT, "on_long", [])
        elif args[0] == 7:
            self.endpoint.device.on_off_bus.listener_event(
                "listener_event", ZHA_SEND_EVENT, "off_double", [])
        elif args[0] == 8:
            self.endpoint.device.on_off_bus.listener_event(
                "listener_event", ZHA_SEND_EVENT, "off_long", [])
示例#26
0
class TuyaNewManufCluster(CustomCluster):
    """Tuya manufacturer specific cluster.

    This is an attempt to consolidate the multiple above clusters into a
    single framework. Instead of overriding the handle_cluster_request()
    method, implement handlers for commands, like get_data, set_data_response,
    set_time_request, etc.
    """

    name: str = "Tuya Manufacturer Specific"
    cluster_id: t.uint16_t = TUYA_CLUSTER_ID
    ep_attribute: str = "tuya_manufacturer"

    server_commands = {
        TUYA_SET_DATA:
        foundation.ZCLCommandDef("set_data", {"data": TuyaCommand},
                                 False,
                                 is_manufacturer_specific=True),
        TUYA_SEND_DATA:
        foundation.ZCLCommandDef("send_data", {"data": TuyaCommand},
                                 False,
                                 is_manufacturer_specific=True),
        TUYA_SET_TIME:
        foundation.ZCLCommandDef("set_time", {"time": TuyaTimePayload},
                                 False,
                                 is_manufacturer_specific=True),
    }

    client_commands = {
        TUYA_GET_DATA:
        foundation.ZCLCommandDef("get_data", {"data": TuyaCommand},
                                 True,
                                 is_manufacturer_specific=True),
        TUYA_SET_DATA_RESPONSE:
        foundation.ZCLCommandDef(
            "set_data_response",
            {"data": TuyaCommand},
            True,
            is_manufacturer_specific=True,
        ),
        TUYA_ACTIVE_STATUS_RPT:
        foundation.ZCLCommandDef(
            "active_status_report",
            {"data": TuyaCommand},
            True,
            is_manufacturer_specific=True,
        ),
        TUYA_SET_TIME:
        foundation.ZCLCommandDef("set_time_request", {"data": t.data16},
                                 True,
                                 is_manufacturer_specific=True),
    }

    data_point_handlers: Dict[int, str] = {}

    def handle_cluster_request(
        self,
        hdr: foundation.ZCLHeader,
        args: Tuple,
        *,
        dst_addressing: Optional[Union[t.Addressing.Group, t.Addressing.IEEE,
                                       t.Addressing.NWK]] = None,
    ) -> None:
        """Handle cluster specific request."""

        try:
            if hdr.is_reply:
                # server_cluster -> client_cluster cluster specific command
                handler_name = f"handle_{self.client_commands[hdr.command_id].name}"
            else:
                handler_name = f"handle_{self.server_commands[hdr.command_id].name}"
        except KeyError:
            self.debug("Received unknown manufacturer command %s: %s",
                       hdr.command_id, args)
            if not hdr.frame_control.disable_default_response:
                self.send_default_rsp(
                    hdr, status=foundation.Status.UNSUP_CLUSTER_COMMAND)
                return

        try:
            status = getattr(self, handler_name)(*args)
        except AttributeError:
            self.warning(
                "No '%s' tuya handler found for %s",
                handler_name,
                args,
            )
            status = foundation.Status.UNSUP_CLUSTER_COMMAND

        if not hdr.frame_control.disable_default_response:
            self.send_default_rsp(hdr, status=status)

    def handle_get_data(self, command: TuyaCommand) -> foundation.Status:
        """Handle get_data response (report)."""
        try:
            dp_handler = self.data_point_handlers[command.dp]
            getattr(self, dp_handler)(command)
        except (AttributeError, KeyError):
            self.debug("No datapoint handler for %s", command)
            return foundation.status.UNSUPPORTED_ATTRIBUTE

        return foundation.Status.SUCCESS

    handle_set_data_response = handle_get_data
    handle_active_status_report = handle_get_data

    def handle_set_time_request(self,
                                payload: t.uint16_t) -> foundation.Status:
        """Handle Time set request."""
        return foundation.Status.SUCCESS

    def _dp_2_attr_update(self, command: TuyaCommand) -> None:
        """Handle data point to attribute report conversion."""
        try:
            dp_map = self.dp_to_attribute[command.dp]
        except KeyError:
            self.debug("No attribute mapping for %s data point", command.dp)
            return

        endpoint = self.endpoint
        if dp_map.endpoint_id:
            endpoint = self.endpoint.device.endpoints[dp_map.endpoint_id]
        cluster = getattr(endpoint, dp_map.ep_attribute)
        value = command.data.payload
        if dp_map.converter:
            value = dp_map.converter(value)

        cluster.update_attribute(dp_map.attribute_name, value)
示例#27
0
class TerncyRawCluster(CustomCluster):
    """Terncy Raw Cluster."""

    cluster_id = MANUFACTURER_SPECIFIC_CLUSTER_ID
    name = "Terncy Raw cluster"

    client_commands = {
        0x00: foundation.ZCLCommandDef(
            "click_event",
            {"count": t.uint8_t, "state": t.uint8_t},
            False,
            is_manufacturer_specific=True,
        ),
        0x04: foundation.ZCLCommandDef(
            "motion_event",
            {"param1": t.uint8_t, "param2": t.uint8_t, "state": t.uint8_t},
            False,
            is_manufacturer_specific=True,
        ),
    }

    def __init__(self, *args, **kwargs):
        """Init."""
        super().__init__(*args, **kwargs)
        self._last_clicks = deque(maxlen=10)

    def handle_cluster_request(
        self,
        hdr: foundation.ZCLHeader,
        args: List[Any],
        *,
        dst_addressing: Optional[
            Union[t.Addressing.Group, t.Addressing.IEEE, t.Addressing.NWK]
        ] = None,
    ):
        """Handle a cluster command received on this cluster."""
        if hdr.command_id == 0:  # click event
            count = args[0]
            state = args[1]
            if (state, count) in self._last_clicks:
                return  # ignore repeated event for single action.
            else:
                self._last_clicks.append((state, count))
            if state > 5:
                state = 5
            event_args = {PRESS_TYPE: CLICK_TYPES[state], "count": count, VALUE: state}
            action = "button_{}".format(CLICK_TYPES[state])
            self.listener_event(ZHA_SEND_EVENT, action, event_args)
        elif hdr.command_id == 4:  # motion event
            state = args[2]
            side = SIDE_LOOKUP[state]
            if side == LEFT:
                self.endpoint.device.motion_left_bus.listener_event(MOTION_EVENT)
            elif side == RIGHT:
                self.endpoint.device.motion_right_bus.listener_event(MOTION_EVENT)

    def _update_attribute(self, attrid, value):
        super()._update_attribute(attrid, value)
        if attrid == 27:  # knob rotate event
            if value > 0:
                action = ROTATE_RIGHT
            else:
                action = ROTATE_LEFT
            steps = value / 12
            event_args = {STEPS: abs(steps)}
            self.listener_event(ZHA_SEND_EVENT, action, event_args)