zigpy / zha-device-handlers

ZHA device handlers bridge the functionality gap created when manufacturers deviate from the ZCL specification, handling deviations and exceptions by parsing custom messages to and from Zigbee devices.
Apache License 2.0
696 stars 641 forks source link

[Device Support Request] PEHPL0X by Perenio #2338

Closed OlegKarasik closed 5 months ago

OlegKarasik commented 1 year ago

Is your feature request related to a problem? Please describe. The smart plug is connected and recognised by ZHA. However, it exposes only "Summation Delivered" which acts quite unreliable (the numbers goes up and down), it also explorer without metering value i.e. Wh or kWh, so it cannot be added directly to Energy statistics. Based on the experience with Z2M this device also should expose at least Active Power in Watts (current consumption).

Describe the solution you'd like It would be amazing if the device will expose Active Power sensor and Summation Delivered sensor will have metering value (kWh), to enable integration with Energy, and be more reliable (if it is possible)

Device signature ```yaml { "node_descriptor": "NodeDescriptor(logical_type=, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=, mac_capability_flags=, manufacturer_code=4660, maximum_buffer_size=108, maximum_incoming_transfer_size=0, server_mask=10752, maximum_outgoing_transfer_size=0, descriptor_capability_field=, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=False, *is_full_function_device=True, *is_mains_powered=True, *is_receiver_on_when_idle=True, *is_router=True, *is_security_capable=False)", "endpoints": { "1": { "profile_id": 260, "device_type": "0x0051", "in_clusters": [ "0x0000", "0x0003", "0x0006", "0x0702", "0xfc7b" ], "out_clusters": [ "0x0003" ] }, "10": { "profile_id": 260, "device_type": "0xfff0", "in_clusters": [ "0x0000" ], "out_clusters": [ "0x0019" ] }, "242": { "profile_id": 41440, "device_type": "0x0061", "in_clusters": [], "out_clusters": [ "0x0021" ] } }, "manufacturer": "Perenio", "model": "PEHPL0X", "class": "zigpy.device.Device" } ```
Diagnostic information ```yaml { "home_assistant": { "installation_type": "Home Assistant Container", "version": "2023.4.2", "dev": false, "hassio": false, "virtualenv": false, "python_version": "3.10.10", "docker": true, "arch": "aarch64", "timezone": "Europe/Minsk", "os_name": "Linux", "os_version": "5.15.84-v8+", "run_as_root": true }, "custom_components": { "xiaomi_cloud_map_extractor": { "version": "v2.2.0", "requirements": [ "pillow", "pybase64", "python-miio", "requests", "pycryptodome" ] }, "xiaomi_gateway3": { "version": "3.1.0", "requirements": [ "zigpy>=0.42.0" ] }, "xiaomi_miot": { "version": "0.7.7", "requirements": [ "construct==2.10.56", "python-miio>=0.5.6", "micloud>=0.3" ] }, "hacs": { "version": "1.32.1", "requirements": [ "aiogithubapi>=22.10.1" ] } }, "integration_manifest": { "domain": "zha", "name": "Zigbee Home Automation", "after_dependencies": [ "onboarding", "usb" ], "codeowners": [ "@dmulcahey", "@adminiuga", "@puddly" ], "config_flow": true, "dependencies": [ "file_upload" ], "documentation": "https://www.home-assistant.io/integrations/zha", "iot_class": "local_polling", "loggers": [ "aiosqlite", "bellows", "crccheck", "pure_pcapy3", "zhaquirks", "zigpy", "zigpy_deconz", "zigpy_xbee", "zigpy_zigate", "zigpy_znp" ], "requirements": [ "bellows==0.35.0", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.96", "zigpy-deconz==0.20.0", "zigpy==0.54.0", "zigpy-xbee==0.17.0", "zigpy-zigate==0.10.3", "zigpy-znp==0.10.0" ], "usb": [ { "vid": "10C4", "pid": "EA60", "description": "*2652*", "known_devices": [ "slae.sh cc2652rb stick" ] }, { "vid": "1A86", "pid": "55D4", "description": "*sonoff*plus*", "known_devices": [ "sonoff zigbee dongle plus v2" ] }, { "vid": "10C4", "pid": "EA60", "description": "*sonoff*plus*", "known_devices": [ "sonoff zigbee dongle plus" ] }, { "vid": "10C4", "pid": "EA60", "description": "*tubeszb*", "known_devices": [ "TubesZB Coordinator" ] }, { "vid": "1A86", "pid": "7523", "description": "*tubeszb*", "known_devices": [ "TubesZB Coordinator" ] }, { "vid": "1A86", "pid": "7523", "description": "*zigstar*", "known_devices": [ "ZigStar Coordinators" ] }, { "vid": "1CF1", "pid": "0030", "description": "*conbee*", "known_devices": [ "Conbee II" ] }, { "vid": "10C4", "pid": "8A2A", "description": "*zigbee*", "known_devices": [ "Nortek HUSBZB-1" ] }, { "vid": "0403", "pid": "6015", "description": "*zigate*", "known_devices": [ "ZiGate+" ] }, { "vid": "10C4", "pid": "EA60", "description": "*zigate*", "known_devices": [ "ZiGate" ] }, { "vid": "10C4", "pid": "8B34", "description": "*bv 2010/10*", "known_devices": [ "Bitron Video AV2010/10" ] } ], "zeroconf": [ { "type": "_esphomelib._tcp.local.", "name": "tube*" }, { "type": "_zigate-zigbee-gateway._tcp.local.", "name": "*zigate*" }, { "type": "_zigstar_gw._tcp.local.", "name": "*zigstar*" }, { "type": "_slzb-06._tcp.local.", "name": "slzb-06*" } ], "is_built_in": true }, "data": { "ieee": "**REDACTED**", "nwk": 35168, "manufacturer": "Perenio", "model": "PEHPL0X", "name": "Perenio PEHPL0X", "quirk_applied": false, "quirk_class": "zigpy.device.Device", "manufacturer_code": 4660, "power_source": "Mains", "lqi": 100, "rssi": -75, "last_seen": "2023-04-13T19:25:54", "available": true, "device_type": "Router", "signature": { "node_descriptor": "NodeDescriptor(logical_type=, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=, mac_capability_flags=, manufacturer_code=4660, maximum_buffer_size=108, maximum_incoming_transfer_size=0, server_mask=10752, maximum_outgoing_transfer_size=0, descriptor_capability_field=, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=False, *is_full_function_device=True, *is_mains_powered=True, *is_receiver_on_when_idle=True, *is_router=True, *is_security_capable=False)", "endpoints": { "1": { "profile_id": 260, "device_type": "0x0051", "in_clusters": [ "0x0000", "0x0003", "0x0006", "0x0702", "0xfc7b" ], "out_clusters": [ "0x0003" ] }, "10": { "profile_id": 260, "device_type": "0xfff0", "in_clusters": [ "0x0000" ], "out_clusters": [ "0x0019" ] }, "242": { "profile_id": 41440, "device_type": "0x0061", "in_clusters": [], "out_clusters": [ "0x0021" ] } } }, "active_coordinator": false, "entities": [ { "entity_id": "button.commroom_equipment_0_identify", "name": "Perenio PEHPL0X" }, { "entity_id": "sensor.commroom_equipment_0_summation_delivered", "name": "Perenio PEHPL0X" }, { "entity_id": "switch.commroom_equipment_0_switch", "name": "Perenio PEHPL0X" } ], "neighbors": [ { "device_type": "Router", "rx_on_when_idle": "On", "relationship": "NoneOfTheAbove", "extended_pan_id": "**REDACTED**", "ieee": "**REDACTED**", "nwk": "0x6823", "permit_joining": "NotAccepting", "depth": "1", "lqi": "48" }, { "device_type": "Router", "rx_on_when_idle": "On", "relationship": "Parent", "extended_pan_id": "**REDACTED**", "ieee": "**REDACTED**", "nwk": "0x6AED", "permit_joining": "NotAccepting", "depth": "1", "lqi": "84" }, { "device_type": "Coordinator", "rx_on_when_idle": "On", "relationship": "Parent", "extended_pan_id": "**REDACTED**", "ieee": "**REDACTED**", "nwk": "0x0000", "permit_joining": "NotAccepting", "depth": "0", "lqi": "32" }, { "device_type": "Unknown", "rx_on_when_idle": "On", "relationship": "NoneOfTheAbove", "extended_pan_id": "**REDACTED**", "ieee": "**REDACTED**", "nwk": "0x5B8C", "permit_joining": "NotAccepting", "depth": "0", "lqi": "0" }, { "device_type": "Router", "rx_on_when_idle": "On", "relationship": "NoneOfTheAbove", "extended_pan_id": "**REDACTED**", "ieee": "**REDACTED**", "nwk": "0x881A", "permit_joining": "NotAccepting", "depth": "1", "lqi": "20" } ], "routes": [], "endpoint_names": [ { "name": "SMART_PLUG" }, { "name": "undefined_0xfff0" }, { "name": "unknown 97 device_type of 0xa1e0 profile id" } ], "user_given_name": "\u041a\u043e\u043c\u043c\u0443\u043d\u0438\u043a\u0430\u0446\u0438\u043e\u043d\u043d\u043e\u0435 \u043e\u0431\u043e\u0440\u0443\u0434\u043e\u0432\u0430\u043d\u0438\u0435", "device_reg_id": "7d5ae74757a544fdee2c076787957cf2", "area_id": "communication_room", "cluster_details": { "1": { "device_type": { "name": "SMART_PLUG", "id": 81 }, "profile_id": 260, "in_clusters": { "0x0000": { "endpoint_attribute": "basic", "attributes": { "0x0004": { "attribute_name": "manufacturer", "value": "Perenio" }, "0x0005": { "attribute_name": "model", "value": "PEHPL0X" } }, "unsupported_attributes": {} }, "0x0003": { "endpoint_attribute": "identify", "attributes": {}, "unsupported_attributes": {} }, "0x0006": { "endpoint_attribute": "on_off", "attributes": { "0x0000": { "attribute_name": "on_off", "value": 1 } }, "unsupported_attributes": { "0x4003": { "attribute_name": "start_up_on_off" } } }, "0x0702": { "endpoint_attribute": "smartenergy_metering", "attributes": { "0x0000": { "attribute_name": "current_summ_delivered", "value": 100 }, "0x0200": { "attribute_name": "status", "value": 65 }, "0x0300": { "attribute_name": "unit_of_measure", "value": 121 }, "0x0303": { "attribute_name": "summation_formatting", "value": 157 }, "0x0306": { "attribute_name": "metering_device_type", "value": 199 } }, "unsupported_attributes": { "0x0400": { "attribute_name": "instantaneous_demand" }, "0x0100": { "attribute_name": "current_tier1_summ_delivered" }, "0x0106": { "attribute_name": "current_tier4_summ_delivered" }, "0x0304": { "attribute_name": "demand_formatting" }, "0x0302": { "attribute_name": "divisor" }, "0x0301": { "attribute_name": "multiplier" }, "0x0001": { "attribute_name": "current_summ_received" } } }, "0xfc7b": { "endpoint_attribute": "manufacturer_specific", "attributes": {}, "unsupported_attributes": {} } }, "out_clusters": { "0x0003": { "endpoint_attribute": "identify", "attributes": {}, "unsupported_attributes": {} } } }, "10": { "device_type": { "name": "undefined_0xfff0", "id": 65520 }, "profile_id": 260, "in_clusters": { "0x0000": { "endpoint_attribute": "basic", "attributes": {}, "unsupported_attributes": {} } }, "out_clusters": { "0x0019": { "endpoint_attribute": "ota", "attributes": {}, "unsupported_attributes": {} } } }, "242": { "device_type": { "name": "unknown", "id": 97 }, "profile_id": 41440, "in_clusters": {}, "out_clusters": { "0x0021": { "endpoint_attribute": "green_power", "attributes": {}, "unsupported_attributes": {} } } } } } } ```
OlegKarasik commented 1 year ago

I have created a Quirk for it (to display information from ElectricalMeasurementCluster). However, the current issue is that ManufacturerCluster isn't pulled by ZHA and therefore ElectricalMeasurementCluster attributes aren't updated.

Is there a way to request ZHA to pull cluster with configured interval from the quirk?

Quirk ``` python """Perenio PEHPL0X plug.""" import zigpy.types as t from zigpy.profiles import zha from zigpy.quirks import CustomCluster, CustomDevice from zigpy.zcl.clusters.general import ( Basic, Identify, OnOff, Ota, GreenPowerProxy ) from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement from zigpy.zcl.clusters.smartenergy import Metering from zhaquirks import ( Bus, LocalDataCluster ) from zhaquirks.const import ( DEVICE_TYPE, ENDPOINTS, INPUT_CLUSTERS, OUTPUT_CLUSTERS, PROFILE_ID, ) MANUFACTURER_CLUSTER_ID = 0xFC7B POWER_REPORTED = "power_reported" VOLTAGE_REPORTED = "voltage_reported" CONSUMPTION_REPORTED = "consumption_reported" POWER_ON_DEFAULT_REPORTED = "power_on_default_reported" class ElectricalMeasurementCluster(LocalDataCluster, ElectricalMeasurement): """Electrical measurement cluster to receive reports that are sent to the basic cluster.""" cluster_id = ElectricalMeasurement.cluster_id POWER_ID = 0x050B VOLTAGE_ID = 0x0505 CONSUMPTION_ID = 0x0304 def __init__(self, *args, **kwargs): """Init.""" super().__init__(*args, **kwargs) self.endpoint.device.voltage_bus.add_listener(self) self.endpoint.device.consumption_bus.add_listener(self) self.endpoint.device.power_bus.add_listener(self) # put a default value so the sensors are created if self.POWER_ID not in self._attr_cache: self._update_attribute(self.POWER_ID, 0) if self.VOLTAGE_ID not in self._attr_cache: self._update_attribute(self.VOLTAGE_ID, 0) if self.CONSUMPTION_ID not in self._attr_cache: self._update_attribute(self.CONSUMPTION_ID, 0) def power_reported(self, value): """Power reported.""" self._update_attribute(self.POWER_ID, value) def voltage_reported(self, value): """Voltage reported.""" self._update_attribute(self.VOLTAGE_ID, value) def consumption_reported(self, value): """Consumption reported.""" self._update_attribute(self.CONSUMPTION_ID, value) class PerenioManufacturerCluster(CustomCluster): """PerenioManufacturerCluster input cluster, only used to provide power consumption information to ElectricalMeasurementCluster.""" cluster_id = MANUFACTURER_CLUSTER_ID POWER_ID = 0x000A VOLTAGE_ID = 0x0003 CONSUMPTION_ID = 0x000E POWER_ON_DEFAULT_ID = 0x0000 name = "Perenio Manufacturer specific" ep_attribute = "perenio_manufacturer_specific" attributes = { POWER_ID: ("active_power", t.uint16_t, True), VOLTAGE_ID: ("rms_voltage", t.uint16_t, True), CONSUMPTION_ID: ("consumed_energy", t.uint32_t, True), POWER_ON_DEFAULT_ID: ("power_on_default", t.uint8_t, True) } def __init__(self, *args, **kwargs): """Init.""" super().__init__(*args, **kwargs) def _update_attribute(self, attrid, value): super()._update_attribute(attrid, value) if value is not None and value >= 0: if attrid == self.POWER_ID: self.endpoint.device.power_bus.listener_event(POWER_REPORTED, value) elif attrid == self.VOLTAGE_ID: self.endpoint.device.voltage_bus.listener_event(VOLTAGE_REPORTED, value) elif attrid == self.CONSUMPTION_ID: self.endpoint.device.consumption_bus.listener_event(CONSUMPTION_REPORTED, value) elif attrid == self.POWER_ON_DEFAULT_ID: self.endpoint.device.power_on_default_bus.listener_event(POWER_ON_DEFAULT_REPORTED, value) class PEHPL0X(CustomDevice): """Perenio PEHPL0X smart plug.""" def __init__(self, *args, **kwargs): """Init.""" self.voltage_bus = Bus() self.consumption_bus = Bus() self.power_bus = Bus() self.power_on_default_bus = Bus() super().__init__(*args, **kwargs) signature = { ENDPOINTS: { # 1: { PROFILE_ID: 0x0104, DEVICE_TYPE: zha.DeviceType.SMART_PLUG, INPUT_CLUSTERS: [ Basic.cluster_id, Identify.cluster_id, OnOff.cluster_id, Metering.cluster_id, MANUFACTURER_CLUSTER_ID, ], OUTPUT_CLUSTERS: [ Identify.cluster_id ], }, # 10: { PROFILE_ID: 0x0104, DEVICE_TYPE: 0xFFF0, INPUT_CLUSTERS: [ Basic.cluster_id ], OUTPUT_CLUSTERS: [ Ota.cluster_id ], }, # 242: { PROFILE_ID: 0xA1E0, DEVICE_TYPE: 0x0061, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [ GreenPowerProxy.cluster_id ], } } } replacement = { ENDPOINTS: { 1: { PROFILE_ID: 0x0104, DEVICE_TYPE: zha.DeviceType.SMART_PLUG, INPUT_CLUSTERS: [ Basic.cluster_id, Identify.cluster_id, OnOff.cluster_id, Metering.cluster_id, ElectricalMeasurementCluster, PerenioManufacturerCluster, ], OUTPUT_CLUSTERS: [ Identify.cluster_id ], }, 10: { PROFILE_ID: 0x0104, DEVICE_TYPE: 0xFFF0, INPUT_CLUSTERS: [ Basic.cluster_id ], OUTPUT_CLUSTERS: [ Ota.cluster_id ], }, 242: { PROFILE_ID: 0xA1E0, DEVICE_TYPE: 0x0061, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [ GreenPowerProxy.cluster_id ], } } } ```
OlegKarasik commented 12 months ago

I have successfully bound PerenioManufacturerCluster to the coordinator (using Zha-Toolkit). So with the above quirk you can get electrical measurements.

The only important note is to make sure to configure reporting because by default these plugs generate a lot of spam.

OlegKarasik commented 11 months ago

Update the Quick in comment to include power_on_default attribute (still hasn't figured out how to make it appear in ZHA as a select).

github-actions[bot] commented 5 months ago

There hasn't been any activity on this issue recently. Due to the high number of incoming GitHub notifications, we have to clean some of the old issues, as many of them have already been resolved with the latest updates. Please make sure to update to the latest version and check if that solves the issue. Let us know if that works for you by adding a comment 👍 This issue has now been marked as stale and will be closed if no further activity occurs. Thank you for your contributions.