diff --git a/tests/zha_devices_list.py b/tests/zha_devices_list.py index 39e5bf5f..811ba0c2 100644 --- a/tests/zha_devices_list.py +++ b/tests/zha_devices_list.py @@ -7347,4 +7347,108 @@ }, }, }, + { + DEV_SIG_DEV_NO: 102, + SIG_MANUFACTURER: "Sinope Technologies", + SIG_MODEL: "DM2550ZB", + SIG_NODE_DESC: { + "logical_type": 1, + "complex_descriptor_available": 0, + "user_descriptor_available": 1, + "reserved": 0, + "aps_flags": 0, + "frequency_band": 8, + "mac_capability_flags": 142, + "manufacturer_code": 4508, + "maximum_buffer_size": 71, + "maximum_incoming_transfer_size": 43, + "server_mask": 10752, + "maximum_outgoing_transfer_size": 43, + "descriptor_capability_field": 0, + }, + SIG_ENDPOINTS: { + 1: { + SIG_EP_TYPE: 257, + DEV_SIG_EP_ID: 1, + SIG_EP_INPUT: [0, 2, 3, 4, 5, 6, 8, 1794, 2820, 2821, 65281], + SIG_EP_OUTPUT: [3, 4, 10, 25], + SIG_EP_PROFILE: 260, + } + }, + DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], + DEV_SIG_ENT_MAP: { + ("button", "00:11:22:33:44:55:66:77-1-3"): { + DEV_SIG_CLUSTER_HANDLERS: ["identify"], + DEV_SIG_ENT_MAP_CLASS: "IdentifyButton", + DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_dm2550zb_identify", + }, + ("light", "00:11:22:33:44:55:66:77-1"): { + DEV_SIG_CLUSTER_HANDLERS: ["level", "on_off"], + DEV_SIG_ENT_MAP_CLASS: "Light", + DEV_SIG_ENT_MAP_ID: "light.sinope_technologies_dm2550zb_light", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { + DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "PolledElectricalMeasurement", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_dm2550zb_power", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { + DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", + DEV_SIG_ENT_MAP_ID: ( + "sensor.sinope_technologies_dm2550zb_apparent_power" + ), + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { + DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_dm2550zb_current", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { + DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_dm2550zb_voltage", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { + DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_dm2550zb_ac_frequency", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { + DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], + DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_dm2550zb_power_factor", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { + DEV_SIG_CLUSTER_HANDLERS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "RSSISensor", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_dm2550zb_rssi", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { + DEV_SIG_CLUSTER_HANDLERS: ["basic"], + DEV_SIG_ENT_MAP_CLASS: "LQISensor", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_dm2550zb_lqi", + }, + ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { + DEV_SIG_CLUSTER_HANDLERS: ["ota"], + DEV_SIG_ENT_MAP_CLASS: "FirmwareUpdateEntity", + DEV_SIG_ENT_MAP_ID: "update.sinope_technologies_dm2550zb_firmware", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { + DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], + DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_dm2550zb_instantaneous_demand", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { + DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], + DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_dm2550zb_summation_delivered", + }, + ("sensor", "00:11:22:33:44:55:66:77-1-2"): { + DEV_SIG_CLUSTER_HANDLERS: ["device_temperature"], + DEV_SIG_ENT_MAP_CLASS: "DeviceTemperature", + DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_dm2550zb_device_temperature", + }, + }, + }, ] diff --git a/zha/application/platforms/number/__init__.py b/zha/application/platforms/number/__init__.py index 8de89b53..6c3edec6 100644 --- a/zha/application/platforms/number/__init__.py +++ b/zha/application/platforms/number/__init__.py @@ -969,3 +969,62 @@ class DanfossRegulationSetpointOffset(NumberConfigurationEntity): _attr_native_max_value: float = 2.5 _attr_native_step: float = 0.1 _attr_multiplier = 1 / 10 + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names="sinope_manufacturer_specific", + models={"DM2500ZB", "DM2500ZB-G2", "DM2550ZB", "DM2550ZB-G2"}, +) +class SinopeDimmerOnLevelConfigurationEntity(NumberConfigurationEntity): + """Representation of a Sinope dimmer switch on level.""" + + _unique_id_suffix = "on_intensity" + _attr_entity_category = EntityCategory.CONFIG + _attr_native_min_value: float = 1 + _attr_native_max_value: float = 255 + _attribute_name = "on_intensity" + _attr_translation_key: str = "on_level" + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names="sinope_manufacturer_specific", + models={ + "DM2500ZB", + "DM2500ZB-G2", + "DM2550ZB", + "DM2550ZB-G2", + "SW2500ZB", + "SW2500ZB-G2", + }, +) +class SinopeLightLEDOnIntensityConfigurationEntity(NumberConfigurationEntity): + """Representation of a Sinope switch LED on-level brightness.""" + + _unique_id_suffix = "on_led_intensity" + _attr_entity_category = EntityCategory.CONFIG + _attr_native_min_value: float = 1 + _attr_native_max_value: float = 100 + _attribute_name = "on_led_intensity" + _attr_translation_key: str = "on_led_intensity" + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names="sinope_manufacturer_specific", + models={ + "DM2500ZB", + "DM2500ZB-G2", + "DM2550ZB", + "DM2550ZB-G2", + "SW2500ZB", + "SW2500ZB-G2", + }, +) +class SinopeLightLEDOffIntensityConfigurationEntity(NumberConfigurationEntity): + """Representation of a Sinope switch LED off-level brightness.""" + + _unique_id_suffix = "off_led_intensity" + _attr_entity_category = EntityCategory.CONFIG + _attr_native_min_value: float = 1 + _attr_native_max_value: float = 100 + _attribute_name = "off_led_intensity" + _attr_translation_key: str = "off_led_intensity" diff --git a/zha/application/platforms/select.py b/zha/application/platforms/select.py index 52e652ef..e9c532fe 100644 --- a/zha/application/platforms/select.py +++ b/zha/application/platforms/select.py @@ -819,3 +819,53 @@ class DanfossViewingDirection(ZCLEnumSelectEntity): _attribute_name = "viewing_direction" _attr_translation_key: str = "viewing_direction" _enum = danfoss_thermostat.DanfossViewingDirectionEnum + + +class SinopeLightLedColors(types.enum32): + """Color values for Sinope light switch status LEDs.""" + + Lim = 0x0AFFDC + Amber = 0x000A4B + Fushia = 0x0100A5 + Perle = 0x64FFFF + Blue = 0xFFFF00 + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names="sinope_manufacturer_specific", + models={ + "DM2500ZB", + "DM2500ZB-G2", + "DM2550ZB", + "DM2550ZB-G2", + "SW2500ZB", + "SW2500ZB-G2", + }, +) +class SinopeLightLEDOffColorSelect(ZCLEnumSelectEntity): + """Representation of the marker LED Off-state color of Sinope light switches.""" + + _unique_id_suffix = "off_led_color" + _attribute_name = "off_led_color" + _attr_translation_key: str = "off_led_color" + _enum = SinopeLightLedColors + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names="sinope_manufacturer_specific", + models={ + "DM2500ZB", + "DM2500ZB-G2", + "DM2550ZB", + "DM2550ZB-G2", + "SW2500ZB", + "SW2500ZB-G2", + }, +) +class SinopeLightLEDOnColorSelect(ZCLEnumSelectEntity): + """Representation of the marker LED On-state color of Sinope light switches.""" + + _unique_id_suffix = "on_led_color" + _attribute_name = "on_led_color" + _attr_translation_key: str = "on_led_color" + _enum = SinopeLightLedColors diff --git a/zha/application/platforms/switch.py b/zha/application/platforms/switch.py index 3659d257..3d9e81eb 100644 --- a/zha/application/platforms/switch.py +++ b/zha/application/platforms/switch.py @@ -844,3 +844,20 @@ class DanfossAdaptationRunSettings(SwitchConfigurationEntity): _unique_id_suffix = "adaptation_run_settings" _attribute_name: str = "adaptation_run_settings" _attr_translation_key: str = "adaptation_run_enabled" + + +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names="sinope_manufacturer_specific", + models={ + "DM2500ZB", + "DM2500ZB-G2", + "DM2550ZB", + "DM2550ZB-G2", + }, +) +class SinopeLightDoubleTapFullSwitch(SwitchConfigurationEntity): + """Representation of a config option that controls whether Double Tap Full option is enabled on a Sinope light switch.""" + + _unique_id_suffix = "double_up_full" + _attribute_name = "double_up_full" + _attr_translation_key: str = "double_up_full" diff --git a/zha/zigbee/cluster_handlers/const.py b/zha/zigbee/cluster_handlers/const.py index 0984c687..74ed5708 100644 --- a/zha/zigbee/cluster_handlers/const.py +++ b/zha/zigbee/cluster_handlers/const.py @@ -87,6 +87,7 @@ SONOFF_CLUSTER: Final[int] = 0xFC11 TUYA_MANUFACTURER_CLUSTER: Final[int] = 0xEF00 VOC_LEVEL_CLUSTER: Final[int] = 0x042E +SINOPE_MANUFACTURER_CLUSTER: Final[int] = 0xFF01 CLUSTER_HANDLER_EVENT: Final[str] = "cluster_handler_event" CLUSTER_HANDLER_ATTRIBUTE_UPDATED: Final[str] = "cluster_handler_attribute_updated" diff --git a/zha/zigbee/cluster_handlers/manufacturerspecific.py b/zha/zigbee/cluster_handlers/manufacturerspecific.py index 1cbe33a2..2b7f19e5 100644 --- a/zha/zigbee/cluster_handlers/manufacturerspecific.py +++ b/zha/zigbee/cluster_handlers/manufacturerspecific.py @@ -37,7 +37,10 @@ REPORT_CONFIG_IMMEDIATE, REPORT_CONFIG_MAX_INT, REPORT_CONFIG_MIN_INT, + REPORT_CONFIG_MIN_INT_IMMEDIATE, + REPORT_CONFIG_RPT_CHANGE, SIGNAL_ATTR_UPDATED, + SINOPE_MANUFACTURER_CLUSTER, SMARTTHINGS_ACCELERATION_CLUSTER, SMARTTHINGS_HUMIDITY_CLUSTER, SONOFF_CLUSTER, @@ -496,3 +499,60 @@ class DanfossDiagnosticClusterHandler(DiagnosticClusterHandler): AttrReportConfig(attr="sw_error_code", config=REPORT_CONFIG_DEFAULT), AttrReportConfig(attr="motor_step_counter", config=REPORT_CONFIG_DEFAULT), ) + + +@registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(SINOPE_MANUFACTURER_CLUSTER) +@registries.CLUSTER_HANDLER_REGISTRY.register(SINOPE_MANUFACTURER_CLUSTER) +class SinopeManufacturerClusterHandler(ClusterHandler): + """Sinope Manufacturer cluster handler.""" + + BIND = True + + def __init__(self, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> None: + """Initialize Sinope cluster handler.""" + super().__init__(cluster, endpoint) + self.ZCL_INIT_ATTRS = { + "double_up_full": True, + "on_led_color": True, + "off_led_color": True, + "off_led_intensity": True, + "on_led_intensity": True, + } + + if self.cluster.endpoint.model in [ + "DM2550ZB", + "DM2550ZB-G2", + "DM2500ZB-G2", + "DM2500ZB", + ]: + self.ZCL_INIT_ATTRS["on_intensity"] = True + + _value_attribute = "action_report" + REPORT_CONFIG = ( + AttrReportConfig( + attr="action_report", + config=( + REPORT_CONFIG_MIN_INT_IMMEDIATE, + REPORT_CONFIG_MIN_INT_IMMEDIATE, + REPORT_CONFIG_RPT_CHANGE, + ), + ), + ) + + @classmethod + def matches(cls, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> bool: + """Filter the cluster match for specific devices.""" + switches = ( + "SW2500ZB", + "SW2500ZB-G2", + "DM2500ZB", + "DM2500ZB-G2", + "DM2550ZB", + "DM2550ZB-G2", + ) + + _LOGGER.debug( + "matching sinope device to cluster handler %s", cluster.endpoint.model + ) + + return cluster.endpoint.model in switches