This commit is contained in:
2024-10-13 22:59:09 +02:00
parent 63fb7f8f73
commit e426efb80f
107 changed files with 20616 additions and 5533 deletions

558
zha_quirks/ts0601_din_power.py Executable file
View File

@@ -0,0 +1,558 @@
"""Tuya Din Power Meter."""
from zigpy.profiles import zha
from zigpy.quirks import CustomDevice
import zigpy.types as t
from zigpy.zcl.clusters.general import Basic, Groups, Ota, Scenes, Time
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,
MODELS_INFO,
OUTPUT_CLUSTERS,
PROFILE_ID,
)
from zhaquirks.tuya import TuyaManufClusterAttributes, TuyaOnOff, TuyaSwitch
TUYA_TOTAL_ENERGY_ATTR = 0x0211
TUYA_CURRENT_ATTR = 0x0212
TUYA_POWER_ATTR = 0x0213
TUYA_VOLTAGE_ATTR = 0x0214
TUYA_DIN_SWITCH_ATTR = 0x0101
SWITCH_EVENT = "switch_event"
"""Hiking Power Meter Attributes"""
HIKING_DIN_SWITCH_ATTR = 0x0110
HIKING_TOTAL_ENERGY_DELIVERED_ATTR = 0x0201
HIKING_TOTAL_ENERGY_RECEIVED_ATTR = 0x0266
HIKING_VOLTAGE_CURRENT_ATTR = 0x0006
HIKING_POWER_ATTR = 0x0267
HIKING_FREQUENCY_ATTR = 0x0269
HIKING_POWER_FACTOR_ATTR = 0x026F
HIKING_TOTAL_REACTIVE_ATTR = 0x026D
HIKING_REACTIVE_POWER_ATTR = 0x026E
"""Zemismart Power Meter Attributes"""
ZEMISMART_TOTAL_ENERGY_ATTR = 0x0201
ZEMISMART_TOTAL_REVERSE_ENERGY_ATTR = 0x0202
ZEMISMART_VCP_ATTR = 0x0006
ZEMISMART_VCP_P2_ATTR = ZEMISMART_VCP_ATTR + 1
ZEMISMART_VCP_P3_ATTR = ZEMISMART_VCP_ATTR + 2
class TuyaManufClusterDinPower(TuyaManufClusterAttributes):
"""Manufacturer Specific Cluster of the Tuya Power Meter device."""
attributes = {
TUYA_TOTAL_ENERGY_ATTR: ("energy", t.uint32_t, True),
TUYA_CURRENT_ATTR: ("current", t.int16s, True),
TUYA_POWER_ATTR: ("power", t.uint16_t, True),
TUYA_VOLTAGE_ATTR: ("voltage", t.uint16_t, True),
TUYA_DIN_SWITCH_ATTR: ("switch", t.uint8_t, True),
}
def _update_attribute(self, attrid, value):
super()._update_attribute(attrid, value)
if attrid == TUYA_TOTAL_ENERGY_ATTR:
self.endpoint.smartenergy_metering.energy_deliver_reported(value / 100)
elif attrid == TUYA_CURRENT_ATTR:
self.endpoint.electrical_measurement.current_reported(value)
elif attrid == TUYA_POWER_ATTR:
self.endpoint.electrical_measurement.power_reported(value / 10)
elif attrid == TUYA_VOLTAGE_ATTR:
self.endpoint.electrical_measurement.voltage_reported(value / 10)
elif attrid == TUYA_DIN_SWITCH_ATTR:
self.endpoint.device.switch_bus.listener_event(
SWITCH_EVENT, self.endpoint.endpoint_id, value
)
class TuyaPowerMeasurement(LocalDataCluster, ElectricalMeasurement):
"""Custom class for power, voltage and current measurement."""
cluster_id = ElectricalMeasurement.cluster_id
POWER_ID = 0x050B
VOLTAGE_ID = 0x0505
CURRENT_ID = 0x0508
REACTIVE_POWER_ID = 0x050E
AC_FREQUENCY_ID = 0x0300
TOTAL_REACTIVE_POWER_ID = 0x0305
POWER_FACTOR_ID = 0x0510
AC_CURRENT_MULTIPLIER = 0x0602
AC_CURRENT_DIVISOR = 0x0603
AC_FREQUENCY_MULTIPLIER = 0x0400
AC_FREQUENCY_DIVISOR = 0x0401
_CONSTANT_ATTRIBUTES = {
AC_CURRENT_MULTIPLIER: 1,
AC_CURRENT_DIVISOR: 1000,
AC_FREQUENCY_MULTIPLIER: 1,
AC_FREQUENCY_DIVISOR: 100,
}
def voltage_reported(self, value):
"""Voltage reported."""
self._update_attribute(self.VOLTAGE_ID, value)
def power_reported(self, value):
"""Power reported."""
self._update_attribute(self.POWER_ID, value)
def power_factor_reported(self, value):
"""Power Factor reported."""
self._update_attribute(self.POWER_FACTOR_ID, value)
def reactive_power_reported(self, value):
"""Reactive Power reported."""
self._update_attribute(self.REACTIVE_POWER_ID, value)
def current_reported(self, value):
"""Ampers reported."""
self._update_attribute(self.CURRENT_ID, value)
def frequency_reported(self, value):
"""AC Frequency reported."""
self._update_attribute(self.AC_FREQUENCY_ID, value)
def reactive_energy_reported(self, value):
"""Summation Reactive Energy reported."""
self._update_attribute(self.TOTAL_REACTIVE_POWER_ID, value)
class TuyaElectricalMeasurement(LocalDataCluster, Metering):
"""Custom class for total energy measurement."""
cluster_id = Metering.cluster_id
CURRENT_DELIVERED_ID = 0x0000
CURRENT_RECEIVED_ID = 0x0001
POWER_WATT = 0x0000
"""Setting unit of measurement."""
_CONSTANT_ATTRIBUTES = {0x0300: POWER_WATT}
def energy_deliver_reported(self, value):
"""Summation Energy Deliver reported."""
self._update_attribute(self.CURRENT_DELIVERED_ID, value)
def energy_receive_reported(self, value):
"""Summation Energy Receive reported."""
self._update_attribute(self.CURRENT_RECEIVED_ID, value)
class HikingManufClusterDinPower(TuyaManufClusterAttributes):
"""Manufacturer Specific Cluster of the Hiking Power Meter device."""
attributes = {
HIKING_DIN_SWITCH_ATTR: ("switch", t.uint8_t, True),
HIKING_TOTAL_ENERGY_DELIVERED_ATTR: ("energy_delivered", t.uint32_t, True),
HIKING_TOTAL_ENERGY_RECEIVED_ATTR: ("energy_received", t.uint16_t, True),
HIKING_VOLTAGE_CURRENT_ATTR: ("voltage_current", t.uint32_t, True),
HIKING_POWER_ATTR: ("power", t.uint16_t, True),
HIKING_FREQUENCY_ATTR: ("frequency", t.uint16_t, True),
HIKING_TOTAL_REACTIVE_ATTR: ("total_reactive_energy", t.int32s, True),
HIKING_REACTIVE_POWER_ATTR: ("reactive_power", t.int16s, True),
HIKING_POWER_FACTOR_ATTR: ("power_factor", t.uint16_t, True),
}
def _update_attribute(self, attrid, value):
super()._update_attribute(attrid, value)
if attrid == HIKING_DIN_SWITCH_ATTR:
self.endpoint.device.switch_bus.listener_event(SWITCH_EVENT, 16, value)
elif attrid == HIKING_TOTAL_ENERGY_DELIVERED_ATTR:
self.endpoint.smartenergy_metering.energy_deliver_reported(value / 100)
elif attrid == HIKING_TOTAL_ENERGY_RECEIVED_ATTR:
self.endpoint.smartenergy_metering.energy_receive_reported(value / 100)
elif attrid == HIKING_VOLTAGE_CURRENT_ATTR:
self.endpoint.electrical_measurement.current_reported(value >> 16)
self.endpoint.electrical_measurement.voltage_reported(
(value & 0x0000FFFF) / 10
)
elif attrid == HIKING_POWER_ATTR:
self.endpoint.electrical_measurement.power_reported(value)
elif attrid == HIKING_FREQUENCY_ATTR:
self.endpoint.electrical_measurement.frequency_reported(value)
elif attrid == HIKING_TOTAL_REACTIVE_ATTR:
self.endpoint.electrical_measurement.reactive_energy_reported(value)
elif attrid == HIKING_REACTIVE_POWER_ATTR:
self.endpoint.electrical_measurement.reactive_power_reported(value)
elif attrid == HIKING_POWER_FACTOR_ATTR:
self.endpoint.electrical_measurement.power_factor_reported(value / 10)
class ZemismartManufCluster(TuyaManufClusterAttributes):
"""Manufacturer Specific Cluster of the Zemismart SPM series Power Meter devices."""
attributes = {
ZEMISMART_TOTAL_ENERGY_ATTR: ("energy", t.uint32_t, True),
ZEMISMART_TOTAL_REVERSE_ENERGY_ATTR: ("reverse_energy", t.uint32_t, True),
ZEMISMART_VCP_ATTR: ("vcp_raw", t.data64, True),
ZEMISMART_VCP_P2_ATTR: ("vcp_p2_raw", t.data64, True),
ZEMISMART_VCP_P3_ATTR: ("vcp_p3_raw", t.data64, True),
}
def _update_attribute(self, attrid, value):
super()._update_attribute(attrid, value)
if attrid == ZEMISMART_TOTAL_ENERGY_ATTR:
self.endpoint.smartenergy_metering.energy_deliver_reported(value)
elif attrid == ZEMISMART_TOTAL_REVERSE_ENERGY_ATTR:
self.endpoint.smartenergy_metering.energy_receive_reported(value)
elif attrid == ZEMISMART_VCP_ATTR:
self.endpoint.electrical_measurement.vcp_reported(value, 0)
elif attrid == ZEMISMART_VCP_P2_ATTR:
self.endpoint.electrical_measurement.vcp_reported(value, 1)
elif attrid == ZEMISMART_VCP_P3_ATTR:
self.endpoint.electrical_measurement.vcp_reported(value, 2)
class ZemismartPowerMeasurement(LocalDataCluster, ElectricalMeasurement):
"""Custom class for power, voltage and current measurement."""
"""Setting unit of measurement."""
_CONSTANT_ATTRIBUTES = {
ElectricalMeasurement.AttributeDefs.ac_voltage_multiplier.id: 1,
ElectricalMeasurement.AttributeDefs.ac_voltage_divisor.id: 10,
ElectricalMeasurement.AttributeDefs.ac_current_multiplier.id: 1,
ElectricalMeasurement.AttributeDefs.ac_current_divisor.id: 1000,
}
phase_attributes = [
{ # Phase 1 (X)
"voltage_x": ElectricalMeasurement.AttributeDefs.rms_voltage.id,
"current_x": ElectricalMeasurement.AttributeDefs.rms_current.id,
"power_x": ElectricalMeasurement.AttributeDefs.active_power.id,
},
{ # Phase 2 (Y)
"voltage_y": ElectricalMeasurement.AttributeDefs.rms_voltage_ph_b.id,
"current_y": ElectricalMeasurement.AttributeDefs.rms_current_ph_b.id,
"power_y": ElectricalMeasurement.AttributeDefs.active_power_ph_b.id,
},
{ # Phase 3 (Z)
"voltage_z": ElectricalMeasurement.AttributeDefs.rms_voltage_ph_c.id,
"current_z": ElectricalMeasurement.AttributeDefs.rms_current_ph_c.id,
"power_z": ElectricalMeasurement.AttributeDefs.active_power_ph_c.id,
},
]
# Voltage, current, power is delivered in one value
def vcp_reported(self, value, phase=0):
"""Voltage, current, power reported."""
if phase < 0 or phase > 2:
phase = 0
voltage = int.from_bytes(value[6:8], byteorder="little")
current = int.from_bytes(value[3:6], byteorder="little")
power = int.from_bytes(value[0:3], byteorder="little")
self._update_attribute(self.phase_attributes[phase]["voltage"], voltage)
self._update_attribute(self.phase_attributes[phase]["current"], current)
self._update_attribute(self.phase_attributes[phase]["power"], power)
if phase == 0:
self.endpoint.device.clamp_bus["power"]["a"].listener_event("power_reported", power)
self.endpoint.device.clamp_bus["power"]["a"].listener_event("voltage_reported", voltage)
self.endpoint.device.clamp_bus["power"]["a"].listener_event("current_reported", current)
if phase == 1:
self.endpoint.device.clamp_bus["power"]["b"].listener_event("power_reported", power)
self.endpoint.device.clamp_bus["power"]["b"].listener_event("voltage_reported", voltage)
self.endpoint.device.clamp_bus["power"]["b"].listener_event("current_reported", current)
if phase == 2:
self.endpoint.device.clamp_bus["power"]["c"].listener_event("power_reported", power)
self.endpoint.device.clamp_bus["power"]["c"].listener_event("voltage_reported", voltage)
self.endpoint.device.clamp_bus["power"]["c"].listener_event("current_reported", current)
class PowerMeasurement_2Clamp(LocalDataCluster, ElectricalMeasurement):
"""Custom class for power, voltage and current measurement."""
# use constants from zigpy/zcl/clusters/homeautomation.py
cluster_id = ElectricalMeasurement.cluster_id
_CONSTANT_ATTRIBUTES = {
ElectricalMeasurement.AttributeDefs.ac_current_divisor.id: 1000,
ElectricalMeasurement.AttributeDefs.ac_voltage_divisor.id: 10,
}
def ac_frequency_reported(self, value):
self._update_attribute(
ElectricalMeasurement.AttributeDefs.ac_frequency.id, value
)
def voltage_reported(self, value):
self._update_attribute(
ElectricalMeasurement.AttributeDefs.rms_voltage.id, value
)
def power_reported(self, value):
self._update_attribute(
ElectricalMeasurement.AttributeDefs.active_power.id, value
)
def power_factor_reported(self, value):
self._update_attribute(
ElectricalMeasurement.AttributeDefs.power_factor.id, value
)
def current_reported(self, value):
self._update_attribute(
ElectricalMeasurement.AttributeDefs.rms_current.id, value
)
# class EnergyA(Metering_2Clamp):
# def __init__(self, *args, **kwargs):
# super().__init__(*args, **kwargs)
# self.endpoint.device.clamp_bus["energy"]["a"].add_listener(self)
# class EnergyB(Metering_2Clamp):
# def __init__(self, *args, **kwargs):
# super().__init__(*args, **kwargs)
# self.endpoint.device.clamp_bus["energy"]["b"].add_listener(self)
# class EnergyC(Metering_2Clamp):
# def __init__(self, *args, **kwargs):
# super().__init__(*args, **kwargs)
# self.endpoint.device.clamp_bus["energy"]["c"].add_listener(self)
class PowerA(PowerMeasurement_2Clamp):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.endpoint.device.clamp_bus["power"]["a"].add_listener(self)
class PowerB(PowerMeasurement_2Clamp):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.endpoint.device.clamp_bus["power"]["b"].add_listener(self)
class PowerC(PowerMeasurement_2Clamp):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.endpoint.device.clamp_bus["power"]["c"].add_listener(self)
class ZemismartElectricalMeasurement(TuyaElectricalMeasurement):
"""Custom class for total energy measurement."""
"""Setting unit of measurement."""
_CONSTANT_ATTRIBUTES = {
Metering.AttributeDefs.unit_of_measure.id: 0, # kWh
Metering.AttributeDefs.divisor.id: 100,
}
class TuyaPowerMeter(TuyaSwitch):
"""Tuya power meter device."""
def __init__(self, *args, **kwargs):
"""Init device."""
self.switch_bus = Bus()
self.clamp_bus = {}
for i in ["power", "energy"]:
self.clamp_bus[i] = {}
for j in ["abc", "a", "b", "c"]:
self.clamp_bus[i][j] = Bus()
super().__init__(*args, **kwargs)
signature = {
# "node_descriptor": "<NodeDescriptor byte1=1 byte2=64 mac_capability_flags=142 manufacturer_code=4098
# maximum_buffer_size=82 maximum_incoming_transfer_size=82 server_mask=11264
# maximum_outgoing_transfer_size=82 descriptor_capability_field=0>",
# device_version=1
# input_clusters=[0x0000, 0x0004, 0x0005, 0xef00]
# output_clusters=[0x000a, 0x0019]
MODELS_INFO: [
("_TZE200_byzdayie", "TS0601"),
("_TZE200_ewxhg6o9", "TS0601"),
],
ENDPOINTS: {
# <SimpleDescriptor endpoint=1 profile=260 device_type=51
# device_version=1
# input_clusters=[0, 4, 5, 61184]
# output_clusters=[10, 25]>
1: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
INPUT_CLUSTERS: [
Basic.cluster_id,
Groups.cluster_id,
Scenes.cluster_id,
TuyaManufClusterAttributes.cluster_id,
],
OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
}
},
}
replacement = {
ENDPOINTS: {
1: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
INPUT_CLUSTERS: [
Basic.cluster_id,
Groups.cluster_id,
Scenes.cluster_id,
TuyaManufClusterDinPower,
TuyaPowerMeasurement,
TuyaElectricalMeasurement,
TuyaOnOff,
],
OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
}
}
}
class HikingPowerMeter(TuyaSwitch):
"""Hiking Power Meter Device - DDS238-2."""
signature = {
# "node_descriptor": "<NodeDescriptor byte1=1 byte2=64 mac_capability_flags=142 manufacturer_code=4098
# maximum_buffer_size=82 maximum_incoming_transfer_size=82 server_mask=11264
# maximum_outgoing_transfer_size=82 descriptor_capability_field=0>",
# device_version=1
# input_clusters=[0x0000, 0x0004, 0x0005, 0xef00]
# output_clusters=[0x000a, 0x0019]
MODELS_INFO: [("_TZE200_bkkmqmyo", "TS0601")],
ENDPOINTS: {
# <SimpleDescriptor endpoint=1 profile=260 device_type=51
# device_version=1
# input_clusters=[0, 4, 5, 61184]
# output_clusters=[10, 25]>
1: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
INPUT_CLUSTERS: [
Basic.cluster_id,
Groups.cluster_id,
Scenes.cluster_id,
TuyaManufClusterAttributes.cluster_id,
],
OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
}
},
}
replacement = {
ENDPOINTS: {
1: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
INPUT_CLUSTERS: [
Basic.cluster_id,
Groups.cluster_id,
Scenes.cluster_id,
HikingManufClusterDinPower,
TuyaElectricalMeasurement,
TuyaPowerMeasurement,
],
OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
},
16: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
INPUT_CLUSTERS: [
TuyaOnOff,
],
OUTPUT_CLUSTERS: [],
},
}
}
class TuyaZemismartPowerMeter(CustomDevice):
"""Zemismart power meter device."""
def __init__(self, *args, **kwargs):
"""Init device."""
self.switch_bus = Bus()
self.clamp_bus = {}
for i in ["power", "energy"]:
self.clamp_bus[i] = {}
for j in ["abc", "a", "b", "c"]:
self.clamp_bus[i][j] = Bus()
super().__init__(*args, **kwargs)
signature = {
# "node_descriptor": "NodeDescriptor(logical_type=<LogicalType.Router: 1>, complex_descriptor_available=0,
# user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>,
# mac_capability_flags=<MACCapabilityFlags.FullFunctionDevice|MainsPowered|RxOnWhenIdle|AllocateAddress: 142>,
# manufacturer_code=4098, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264,
# maximum_outgoing_transfer_size=82, descriptor_capability_field=<DescriptorCapability.NONE: 0>,
# *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)",
# device_version=1
# input_clusters=[0x0000, 0x0004, 0x0005, 0xef00]
# output_clusters=[0x000a, 0x0019]
MODELS_INFO: [
("_TZE200_v9hkz2yn", "TS0601"), # SPM02
],
ENDPOINTS: {
# <SimpleDescriptor endpoint=1 profile=260 device_type=51
# device_version=1
# input_clusters=[0, 4, 5, 61184]
# output_clusters=[10, 25]>
1: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
INPUT_CLUSTERS: [
Basic.cluster_id,
Groups.cluster_id,
Scenes.cluster_id,
TuyaManufClusterAttributes.cluster_id,
],
OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
}
},
}
replacement = {
ENDPOINTS: {
1: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
INPUT_CLUSTERS: [
Basic.cluster_id,
Groups.cluster_id,
Scenes.cluster_id,
ZemismartManufCluster,
ZemismartElectricalMeasurement,
ZemismartPowerMeasurement,
],
OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
},
10: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.METER_INTERFACE,
INPUT_CLUSTERS: [
# EnergyA,
PowerA,
],
},
20: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.METER_INTERFACE,
INPUT_CLUSTERS: [
# EnergyB,
PowerB,
],
},
30: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.METER_INTERFACE,
INPUT_CLUSTERS: [
# EnergyC,
PowerC,
],
},
}
}