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, ), }
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, ), } )
class TestCluster(zcl.Cluster): cluster_id = 0x1234 ep_attribute = "test_cluster" server_commands = { 0x00: foundation.ZCLCommandDef("command1", {}, False), 0x01: foundation.ZCLCommandDef("command1", {}, False), }
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 = {}
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)
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)
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
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 ) }
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), }
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
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")
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
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
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
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)
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
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)
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"), [])
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))
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
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])
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)
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", [])
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)
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)