Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Device Support Request] TS0601_TZE284_cjbofhxw PJ-1203 clamp power meter #3152

Open
zibijw23 opened this issue May 14, 2024 · 11 comments
Open
Labels
Tuya Request/PR regarding a Tuya device

Comments

@zibijw23
Copy link

Problem description

The energy meter doesn't display any measurements.

Solution description

It's time for some new quirks

Screenshots/Video

Screenshots/Video

image

Device signature

Device 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)",
  "endpoints": {
    "1": {
      "profile_id": "0x0104",
      "device_type": "0x0051",
      "input_clusters": [
        "0x0000",
        "0x0004",
        "0x0005",
        "0xed00",
        "0xef00"
      ],
      "output_clusters": [
        "0x000a",
        "0x0019"
      ]
    }
  },
  "manufacturer": "_TZE284_cjbofhxw",
  "model": "TS0601",
  "class": "zigpy.device.Device"
}```

</details>


### Diagnostic information

<details><summary>Diagnostic information</summary>

```json
[Paste the diagnostic information here]

Logs

Logs
[Paste the logs here]

Custom quirk

Custom quirk
[Paste your custom quirk here]

Additional information

https://pl.aliexpress.com/item/1005005994777032.html?spm=a2g0o.order_list.order_list_main.5.51dc1c24SJTQMe&gatewayAdapt=glo2pol

@TheJulianJES TheJulianJES added the Tuya Request/PR regarding a Tuya device label May 15, 2024
@mike-nani
Copy link

Hi! I'm very new to HA with level 0.000001 coding skills.
Recently ordered and received this device, although previous reviews show the TZE204 model. I was trying to build a custom quirk for it but can't figure out some details.
The "0xed00" and "0xef00" clusters seem to have no attributes, although one in each cluster appears on the full scan. Is it supposed to be like this? Are the correct attributes only visible after que quirk is applied? What am I missing?

No attributes shown in the "Manage Zigbee Device" section (same for "0xef00"):
image

Full ZHA Toolkit device scan:

zha_toolkit_version: v1.1.10
zigpy_version: 0.64.0
zigpy_rf_version: 0.38.4
ieee_org: update.tze284_cjbofhxw_ts0601_firmware
ieee: xx:xx:xx:xx:xx:xx:xx:xx
command: scan_device
command_data: null
start_time: "2024-05-22T08:54:19.201159+00:00"
errors: []
params:
  dir: 0
  tries: 1
  expect_reply: true
  args: []
  read_before_write: true
  read_after_write: true
scan:
  ieee: xx:xx:xx:xx:xx:xx:xx:xx
  nwk: "0x55e1"
  model: TS0601
  manufacturer: _TZE284_cjbofhxw
  manufacturer_id: "0x4098"
  endpoints:
    - id: 1
      device_type: "0x0051"
      profile: "0x0104"
      in_clusters:
        "0x0000":
          cluster_id: "0x0000"
          title: Basic
          name: basic
          attributes:
            "0x0000":
              attribute_id: "0x0000"
              attribute_name: zcl_version
              value_type:
                - "0x20"
                - uint8_t
                - Analog
              access: READ|REPORT
              access_acl: 5
              attribute_value: 3
            "0x0001":
              attribute_id: "0x0001"
              attribute_name: app_version
              value_type:
                - "0x20"
                - uint8_t
                - Analog
              access: READ|REPORT
              access_acl: 5
              attribute_value: 74
            "0x0002":
              attribute_id: "0x0002"
              attribute_name: stack_version
              value_type:
                - "0x20"
                - uint8_t
                - Analog
              access: READ|REPORT
              access_acl: 5
              attribute_value: 0
            "0x0003":
              attribute_id: "0x0003"
              attribute_name: hw_version
              value_type:
                - "0x20"
                - uint8_t
                - Analog
              access: READ|REPORT
              access_acl: 5
              attribute_value: 1
            "0x0004":
              attribute_id: "0x0004"
              attribute_name: manufacturer
              value_type:
                - "0x42"
                - CharacterString
                - Discrete
              access: READ|REPORT
              access_acl: 5
              attribute_value: _TZE284_cjbofhxw
            "0x0005":
              attribute_id: "0x0005"
              attribute_name: model
              value_type:
                - "0x42"
                - CharacterString
                - Discrete
              access: READ|REPORT
              access_acl: 5
              attribute_value: TS0601
            "0x0006":
              attribute_id: "0x0006"
              attribute_name: date_code
              value_type:
                - "0x42"
                - CharacterString
                - Discrete
              access: READ|REPORT
              access_acl: 5
              attribute_value: ""
            "0x0007":
              attribute_id: "0x0007"
              attribute_name: power_source
              value_type:
                - "0x30"
                - enum8
                - Discrete
              access: READ|REPORT
              access_acl: 5
              attribute_value: 1
            "0xffde":
              attribute_id: "0xffde"
              attribute_name: "65502"
              value_type:
                - "0x20"
                - uint8_t
                - Analog
              access: READ|WRITE|REPORT
              access_acl: 7
            "0xffe0":
              attribute_id: "0xffe0"
              attribute_name: "65504"
              value_type:
                - "0x48"
                - Array
                - Discrete
              access: READ|REPORT
              access_acl: 5
            "0xffe1":
              attribute_id: "0xffe1"
              attribute_name: "65505"
              value_type:
                - "0x48"
                - Array
                - Discrete
              access: READ|REPORT
              access_acl: 5
            "0xffe2":
              attribute_id: "0xffe2"
              attribute_name: "65506"
              value_type:
                - "0x20"
                - uint8_t
                - Analog
              access: READ|REPORT
              access_acl: 5
            "0xffe3":
              attribute_id: "0xffe3"
              attribute_name: "65507"
              value_type:
                - "0x48"
                - Array
                - Discrete
              access: READ|REPORT
              access_acl: 5
            "0xfffd":
              attribute_id: "0xfffd"
              attribute_name: cluster_revision
              value_type:
                - "0x21"
                - uint16_t
                - Analog
              access: READ|REPORT
              access_acl: 5
            "0xfffe":
              attribute_id: "0xfffe"
              attribute_name: reporting_status
              value_type:
                - "0x30"
                - enum8
                - Discrete
              access: READ|REPORT
              access_acl: 5
          commands_received: {}
          commands_generated: {}
        "0x0004":
          cluster_id: "0x0004"
          title: Groups
          name: groups
          attributes:
            "0x0000":
              attribute_id: "0x0000"
              attribute_name: name_support
              value_type:
                - "0x18"
                - bitmap8
                - Discrete
              access: READ|REPORT
              access_acl: 5
              attribute_value: 0
            "0xfffd":
              attribute_id: "0xfffd"
              attribute_name: cluster_revision
              value_type:
                - "0x21"
                - uint16_t
                - Analog
              access: READ|REPORT
              access_acl: 5
              attribute_value: 2
          commands_received: {}
          commands_generated: {}
        "0x0005":
          cluster_id: "0x0005"
          title: Scenes
          name: scenes
          attributes:
            "0x0000":
              attribute_id: "0x0000"
              attribute_name: count
              value_type:
                - "0x20"
                - uint8_t
                - Analog
              access: READ|REPORT
              access_acl: 5
              attribute_value: 0
            "0x0001":
              attribute_id: "0x0001"
              attribute_name: current_scene
              value_type:
                - "0x20"
                - uint8_t
                - Analog
              access: READ|REPORT
              access_acl: 5
              attribute_value: 0
            "0x0002":
              attribute_id: "0x0002"
              attribute_name: current_group
              value_type:
                - "0x21"
                - uint16_t
                - Analog
              access: READ|REPORT
              access_acl: 5
              attribute_value: 0
            "0x0003":
              attribute_id: "0x0003"
              attribute_name: scene_valid
              value_type:
                - "0x10"
                - Bool
                - Discrete
              access: READ|REPORT
              access_acl: 5
              attribute_value: 0
            "0x0004":
              attribute_id: "0x0004"
              attribute_name: name_support
              value_type:
                - "0x18"
                - bitmap8
                - Discrete
              access: READ|REPORT
              access_acl: 5
              attribute_value: 0
            "0xfffd":
              attribute_id: "0xfffd"
              attribute_name: cluster_revision
              value_type:
                - "0x21"
                - uint16_t
                - Analog
              access: READ|REPORT
              access_acl: 5
              attribute_value: 2
          commands_received: {}
          commands_generated: {}
        "0xed00":
          cluster_id: "0xed00"
          title: Cluster
          name: null
          attributes:
            "0xfffd":
              attribute_id: "0xfffd"
              attribute_name: "65533"
              value_type:
                - "0x21"
                - uint16_t
                - Analog
              access: READ|REPORT
              access_acl: 5
              attribute_value: 1
          commands_received: {}
          commands_generated: {}
        "0xef00":
          cluster_id: "0xef00"
          title: Cluster
          name: null
          attributes:
            "0x0000":
              attribute_id: "0x0000"
              attribute_name: "0"
              value_type:
                - "0x20"
                - uint8_t
                - Analog
              access: READ|REPORT
              access_acl: 5
              attribute_value: 0
          commands_received: {}
          commands_generated: {}
      out_clusters:
        "0x000a":
          cluster_id: "0x000a"
          title: Time
          name: time
          attributes:
            "0xfffd":
              attribute_id: "0xfffd"
              attribute_name: cluster_revision
              value_type:
                - "0x21"
                - uint16_t
                - Analog
              access: READ|REPORT
              access_acl: 5
              attribute_value: 1
          commands_received: {}
          commands_generated: {}
        "0x0019":
          cluster_id: "0x0019"
          title: Ota
          name: ota
          attributes:
            "0x0000":
              attribute_id: "0x0000"
              attribute_name: upgrade_server_id
              value_type:
                - "0xf0"
                - EUI64
                - Discrete
              access: READ|REPORT
              access_acl: 5
              attribute_value:
                - 255
                - 255
                - 255
                - 255
                - 255
                - 255
                - 255
                - 255
            "0x0001":
              attribute_id: "0x0001"
              attribute_name: file_offset
              value_type:
                - "0x23"
                - uint32_t
                - Analog
              access: READ|REPORT
              access_acl: 5
              attribute_value: 4294967295
            "0x0006":
              attribute_id: "0x0006"
              attribute_name: image_upgrade_status
              value_type:
                - "0x30"
                - enum8
                - Discrete
              access: READ|REPORT
              access_acl: 5
              attribute_value: 0
            "0xfffd":
              attribute_id: "0xfffd"
              attribute_name: cluster_revision
              value_type:
                - "0x21"
                - uint16_t
                - Analog
              access: READ|REPORT
              access_acl: 5
              attribute_value: 3
          commands_received: {}
          commands_generated: {}
success: true

Thanks!

@MattWestb
Copy link
Contributor

From the first post i see:

  },
  "manufacturer": "_TZE284_cjbofhxw",
  "model": "TS0601",
  "class": "zigpy.device.Device"
}

So you dont have any quirk loaded and then its one tuya MCU device is no use t scanning it with ZHA-ToolKit then you is only getting the information of the standard Zigbee module and the interesting is for putting in the quirk signature but all magic information is from the MCU that you cant getting before getting the signature OK in the quirk.

@lucianojss
Copy link

Hi, got this device also was anyone able to run this device in ZHA? 🙇

@nachogarcia
Copy link

Looks like zigbee2mqtt people just got it working
Koenkk/zigbee2mqtt#22784

@JHurk
Copy link

JHurk commented Jun 11, 2024

Tried to add this device through a custom_quirk but after a few hours of trial and error I gave up.
Would be really nice if someone could come up with support for this device through ZHA.
I have not been able to get it working with a custom quirk, somehow I can not get the custom_quirk to recognize the device.
I see various mismatches/fails in the debug log, like:
[zigpy.quirks] Fail because input cluster mismatch on at least one endpoint

I added the device code tze284_cjbofhxw as model info, in multiple quirks (ts0601_energy_meter.py and ts0601_din_power.py) all with no success.

So following this thread hoping support will be added to finally get this device to work with ZHA.

@mike-nani
Copy link

I also spent several hours trying to create a custom quirk, using ts0601_energy_meter.py as a template. Even adjusting for the differences in clusters, I can't get it to recognize the device. Don't know what else to do...

@JHurk
Copy link

JHurk commented Jun 13, 2024

Not really sure of this is helpful but looking at the PR I found this one:
#2961
It seems like a PR for a similar version of 'our' TZE284_cjbofhxw.
Anyone with a little bit more knowledge on how this works who can tell me if it is worth connecting/mentioning our device in the PR?

@nachogarcia
Copy link

nachogarcia commented Jun 18, 2024

I used the quirk I found in #1768 (edit: yeah, that PR @JHurk ) but needed to edit the signature of the 1CH, because it has an extra in put cluster (0xed00, I don't know which kind it is).
Disclaimer: I needed to restart it after adding it, and it always had power through the clamp.

class TuyaEnergyMeter_1CH(CustomDevice):
    """Tuya PJ-MGW1203 1 channel energy meter."""

    signature = {
        MODELS_INFO: [
            ("_TZE204_cjbofhxw", "TS0601"),
            ("_TZE284_cjbofhxw", "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,
                    TuyaMCUCluster.cluster_id,
                    0xed00
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            }
        },
    }
Full file to just paste it for now ;)
"""Tuya Energy Meter."""

from enum import Enum
from typing import Any, Callable, Dict, Optional, Tuple, Type, Union

from zigpy.profiles import zgp, zha
from zigpy.quirks import CustomDevice
import zigpy.types as t
from zigpy.zcl.clusters.general import Basic, GreenPowerProxy, Groups, Ota, Scenes, Time
from zigpy.zcl.foundation import ZCLAttributeDef

from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
)
from zhaquirks.tuya import (
    NoManufacturerCluster,
    TuyaLocalCluster,
    TuyaZBElectricalMeasurement,
    TuyaZBMeteringClusterWithUnit,
)
from zhaquirks.tuya.mcu import DPToAttributeMapping, TuyaMCUCluster

# from zigpy.zcl.clusters.homeautomation import MeasurementType


# Manufacturer cluster identifiers for device signatures
EARU_MANUFACTURER_CLUSTER_ID = 0xFF66

# Offset of 512 (0x200) for transating DP ID to Attribute ID
# Attribute IDs don't need to match every device's specific values
DP_ATTR_OFFSET = 512

# Power direction acttributes
POWER_FLOW = 102 + DP_ATTR_OFFSET  # PowerFlow (0: forward, 1: reverse)
POWER_FLOW_B = 104 + DP_ATTR_OFFSET  # PowerFlow (0: forward, 1: reverse)

# Calibration attributes
AC_FREQUENCY_COEF = 122 + DP_ATTR_OFFSET  # uint32_t_be
CURRENT_SUMM_DELIVERED_COEF = 119 + DP_ATTR_OFFSET  # uint32_t_be
CURRENT_SUMM_DELIVERED_COEF_B = 125 + DP_ATTR_OFFSET  # uint32_t_be
CURRENT_SUMM_RECEIVED_COEF = 127 + DP_ATTR_OFFSET  # uint32_t_be
CURRENT_SUMM_RECEIVED_COEF_B = 128 + DP_ATTR_OFFSET  # uint32_t_be
INSTANTANEOUS_DEMAND_COEF = 118 + DP_ATTR_OFFSET  # uint32_t_be
INSTANTANEOUS_DEMAND_COEF_B = 124 + DP_ATTR_OFFSET  # uint32_t_be
RMS_CURRENT_COEF = 117 + DP_ATTR_OFFSET  # uint32_t_be
RMS_CURRENT_COEF_B = 123 + DP_ATTR_OFFSET  # uint32_t_be
RMS_VOLTAGE_COEF = 116 + DP_ATTR_OFFSET  # uint32_t_be

# Device configuration attributes
UPDATE_PERIOD = 129 + DP_ATTR_OFFSET  # uint32_t_be (3-60 seconds supported)

# Local configuration attributes
CHANNEL_CONFIGURATION = 0x5000
SUPPRESS_REVERSE_FLOW = 0x5010
SUPPRESS_REVERSE_FLOW_B = 0x5011
POWER_FLOW_PREEMPT = 0x5020

# Suffix for device attributes which need power flow direction applied
UNSIGNED_POWER_ATTR_SUFFIX = "_attr_unsigned"

# Default Tuya MCU cluster endpoint_id
TUYA_MCU_ENDPOINT_ID = 1


def is_type_uint(attr_type: Type) -> bool:
    """True if the specified attribute type is an unsigned integer."""
    return issubclass(attr_type, t.uint_t)


class Channel(str, Enum):
    """Meter channels."""

    A = "a"
    B = "b"
    AB = "ab"

    @classmethod
    def attr_with_channel(cls, attr_name: str, channel=None) -> str:
        """Returns the attr_name with channel suffix."""
        assert channel is None or channel in cls, "Invalid channel."
        if channel and channel != cls.A:
            attr_name = attr_name + "_ch_" + channel
        return attr_name


class ChannelConfiguration(t.enum8):
    """Enums for for all energy meter configurations."""

    NONE = 0x00
    A_PLUS_B = 0x01
    A_MINUS_B = 0x02
    GRID_PLUS_PRODUCTION = 0x03
    CONSUMPTION_MINUS_PRODUCTION = 0x04


class ChannelConfiguration_1CH(t.enum8):
    """Enums for 1 channel energy meter configuration."""

    NONE = ChannelConfiguration.NONE
    DEFAULT = NONE


class ChannelConfiguration_1CHB(t.enum8):
    """Enums for 1 channel bidirectional energy meter configuration."""

    NONE = ChannelConfiguration.NONE
    DEFAULT = NONE


class ChannelConfiguration_2CH(t.enum8):
    """Enums for 2 channel energy meter configuration."""

    A_PLUS_B = ChannelConfiguration.A_PLUS_B
    A_MINUS_B = ChannelConfiguration.A_MINUS_B
    CONSUMPTION_MINUS_PRODUCTION = ChannelConfiguration.CONSUMPTION_MINUS_PRODUCTION
    DEFAULT = CONSUMPTION_MINUS_PRODUCTION


class ChannelConfiguration_2CHB(t.enum8):
    """Enums for 2 channel bidirectional energy meter configuration."""

    A_PLUS_B = ChannelConfiguration.A_PLUS_B
    A_MINUS_B = ChannelConfiguration.A_MINUS_B
    GRID_PLUS_PRODUCTION = ChannelConfiguration.GRID_PLUS_PRODUCTION
    CONSUMPTION_MINUS_PRODUCTION = ChannelConfiguration.CONSUMPTION_MINUS_PRODUCTION
    DEFAULT = GRID_PLUS_PRODUCTION


class MeasurementType(
    t.bitmap32
):  # Would like to import this from zigpy.zcl.clusters.homeautomation, but its offset is currently incorrect
    """Defines the measurement type bits for the ElectricalMeasurement cluster."""

    Active_measurement_AC = 1 << 0
    Reactive_measurement_AC = 1 << 1
    Apparent_measurement_AC = 1 << 2
    Phase_A_measurement = 1 << 3
    Phase_B_measurement = 1 << 4
    Phase_C_measurement = 1 << 5
    DC_measurement = 1 << 6
    Harmonics_measurement = 1 << 7
    Power_quality_measurement = 1 << 8


class Metering:
    """Functions for use with the ZCL Metering cluster."""

    @staticmethod
    def format(
        int_digits: int, dec_digits: int, suppress_leading_zeros: bool = True
    ) -> int:
        """Returns the formatter value for summation and demand Metering attributes."""
        assert 0 <= int_digits <= 7, "int_digits must be within range of 0 to 7."
        assert 0 <= dec_digits <= 7, "dec_digits must be within range of 0 to 7."
        return (suppress_leading_zeros << 6) | (int_digits << 3) | dec_digits


class PowerFlow(t.enum1):
    """Indicates power flow direction."""

    FORWARD = 0x0
    REVERSE = 0x1

    @classmethod
    def align_value(cls, value: int, power_flow=None) -> int:
        """Aligns the value with the power_flow direction."""
        if (
            power_flow == cls.REVERSE
            and value > 0
            or power_flow == cls.FORWARD
            and value < 0
        ):
            value = -value
        return value


class TuyaPowerPhase:
    """Extracts values from Tuya power phase datapoints."""

    @staticmethod
    def variant_1(value) -> Tuple[t.uint_t, t.uint_t]:
        voltage = value[14] | value[13] << 8
        current = value[12] | value[11] << 8
        return voltage, current

    @staticmethod
    def variant_2(value) -> Tuple[t.uint_t, t.uint_t, int]:
        voltage = value[1] | value[0] << 8
        current = value[4] | value[3] << 8
        power = value[7] | value[6] << 8
        return voltage, current, power * 10

    @staticmethod
    def variant_3(value) -> Tuple[t.uint_t, t.uint_t, int]:
        voltage = (value[0] << 8) | value[1]
        current = (value[2] << 16) | (value[3] << 8) | value[4]
        power = (value[5] << 16) | (value[6] << 8) | value[7]
        return voltage, current, power * 10


class PowerCalculation:
    """Methods for calculating power values."""

    @staticmethod
    def active_power_from_apparent_power_power_factor_and_power_flow(
        apparent_power: Optional[t.uint_t],
        power_factor: Optional[t.int_t],
        power_flow: Optional[PowerFlow] = None,
    ) -> Optional[t.int_t]:
        if apparent_power is None or power_factor is None:
            return
        power_factor *= 0.01
        return round(apparent_power * abs(power_factor) * (-1 if power_flow else 1))

    @staticmethod
    def apparent_power_from_active_power_and_power_factor(
        active_power: Optional[t.int_t], power_factor: Optional[t.int_t]
    ) -> Optional[t.uint_t]:
        if active_power is None or power_factor is None:
            return
        power_factor *= 0.01
        return round(abs(active_power) / abs(power_factor))

    @staticmethod
    def apparent_power_from_rms_current_and_rms_voltage(
        rms_current: Optional[t.uint_t],
        rms_voltage: Optional[t.uint_t],
        ac_current_divisor: int = 1,
        ac_current_multiplier: int = 1,
        ac_voltage_divisor: int = 1,
        ac_voltage_multiplier: int = 1,
        ac_power_divisor: int = 1,
        ac_power_multiplier: int = 1,
    ) -> Optional[t.uint_t]:
        if rms_current is None or rms_voltage is None:
            return
        return round(
            (rms_current * ac_current_multiplier / ac_current_divisor)
            * (rms_voltage * ac_voltage_multiplier / ac_voltage_divisor)
            * ac_power_divisor
            / ac_power_multiplier
        )

    @staticmethod
    def reactive_power_from_apparent_power_and_power_factor(
        apparent_power: Optional[t.uint_t], power_factor: Optional[t.int_t]
    ) -> Optional[t.int_t]:
        if apparent_power is None or power_factor is None:
            return
        power_factor *= 0.01
        return round(
            (apparent_power * (1 - power_factor**2) ** 0.5)
            * (-1 if power_factor < 0 else 1)
        )


class LocalClusterAttributes:
    """Methods for handling local configuration attributes on device."""

    _ATTRIBUTE_DEFAULTS: Dict[int, Any] = {}
    _LOCAL_ATTRIBUTES: Tuple[int] = ()

    def _attr_default(
        self, attrid: Union[str, int], default: Optional[Any] = None
    ) -> Optional[Any]:
        """Returns an attribute's default value."""
        attr_def = self.find_attribute(attrid)
        return self._ATTRIBUTE_DEFAULTS.get(
            attr_def.id, getattr(attr_def.type, "DEFAULT", default)
        )

    def _format_attr_value(self, attrid: Union[str, int], value: Any) -> Optional[Any]:
        """Used to format the input the input value with the attribute's type."""
        try:
            attr_def = self.find_attribute(attrid)
            value = attr_def.type(value)
            return value
        except KeyError:
            self.error("%s is not a valid attribute id", attrid)
        except ValueError as e:
            self.error(
                "Failed to convert attribute %s from %s (%s) to type %s: %s",
                attr_def.id,
                value,
                type(value),
                attr_def.type,
                e,
            )
        return

    def get(self, key: Union[int, str], default: Optional[Any] = None) -> Optional[Any]:
        """Get cached attribute value and fall back to its device/type default if defined."""
        value = super().get(key, default)
        if value is None:
            value = self._attr_default(key, default)
        return value

    async def read_attributes(self, attributes, *args, **kwargs):
        """Handle reads to local configuration attributes."""
        success, failure = await super().read_attributes(attributes, *args, **kwargs)
        for attrid in set(self._LOCAL_ATTRIBUTES).intersection(set(attributes)):
            if attrid not in success:
                default = self._attr_default(attrid)
                if default is None:
                    continue
                success[attrid] = default
                failure.pop(attrid, None)
            if success[attrid] not in (None, ""):
                success[attrid] = self.attributes[attrid].type(success[attrid])
        return success, failure

    async def write_attributes(self, attributes, *args, **kwargs):
        """Handle writes to local configuration attributes."""
        local_attributes = {}
        for attrid in set(self._LOCAL_ATTRIBUTES).intersection(set(attributes)):
            value = attributes.pop(attrid)
            if value in (None, ""):
                local_attributes[attrid] = None
                continue
            value = self._format_attr_value(attrid, value)
            if value is not None:
                local_attributes[attrid] = value
        await TuyaLocalCluster.write_attributes(self, local_attributes, *args, **kwargs)
        return await super().write_attributes(attributes, *args, **kwargs)


class TuyaEnergyMeterManufCluster(
    LocalClusterAttributes, NoManufacturerCluster, TuyaMCUCluster
):
    """Manufactuter cluster for Tuya energy meter devices."""

    _CHANNEL_CONFIGURATION_ATTRIBUTES: Dict[Type, Tuple[int]] = {
        ChannelConfiguration_1CHB: (SUPPRESS_REVERSE_FLOW,),
        ChannelConfiguration_2CHB: (
            POWER_FLOW_PREEMPT,
            SUPPRESS_REVERSE_FLOW,
            SUPPRESS_REVERSE_FLOW_B,
        ),
    }

    _LOCAL_ATTRIBUTES: Tuple[int] = (
        CHANNEL_CONFIGURATION,
        POWER_FLOW_PREEMPT,
        SUPPRESS_REVERSE_FLOW,
        SUPPRESS_REVERSE_FLOW_B,
    )

    attributes: Dict[int, ZCLAttributeDef] = {
        AC_FREQUENCY_COEF: ("ac_frequency_coefficient", t.uint32_t_be, True),
        CURRENT_SUMM_DELIVERED_COEF: (
            "current_summ_delivered_coefficient",
            t.uint32_t_be,
            True,
        ),
        CURRENT_SUMM_DELIVERED_COEF_B: (
            "current_summ_delivered_coefficient_ch_b",
            t.uint32_t_be,
            True,
        ),
        CURRENT_SUMM_RECEIVED_COEF: (
            "current_summ_received_coefficient",
            t.uint32_t_be,
            True,
        ),
        CURRENT_SUMM_RECEIVED_COEF_B: (
            "current_summ_received_coefficient_ch_b",
            t.uint32_t_be,
            True,
        ),
        INSTANTANEOUS_DEMAND_COEF: (
            "instantaneous_demand_coefficient",
            t.uint32_t_be,
            True,
        ),
        INSTANTANEOUS_DEMAND_COEF_B: (
            "instantaneous_demand_coefficient_ch_b",
            t.uint32_t_be,
            True,
        ),
        POWER_FLOW: ("power_flow", PowerFlow, True),
        POWER_FLOW_B: ("power_flow_ch_b", PowerFlow, True),
        RMS_CURRENT_COEF: ("rms_current_coefficient", t.uint32_t_be, True),
        RMS_CURRENT_COEF_B: (
            "rms_current_coefficient_ch_b",
            t.uint32_t_be,
            True,
        ),
        RMS_VOLTAGE_COEF: ("rms_voltage_coefficient", t.uint32_t_be, True),
        CHANNEL_CONFIGURATION: (
            "channel_configuration",
            ChannelConfiguration,
            True,
        ),
        UPDATE_PERIOD: ("update_period", t.uint32_t_be, True),
        POWER_FLOW_PREEMPT: ("power_flow_preempt", t.Bool, True),
        SUPPRESS_REVERSE_FLOW: ("suppress_reverse_flow", t.Bool, True),
        SUPPRESS_REVERSE_FLOW_B: ("suppress_reverse_flow_ch_b", t.Bool, True),
    }

    def get_optional(
        self, key: Union[int, str], default: Optional[Any] = None
    ) -> Optional[Any]:
        """Returns the provided default value or None if an attribute is undefined."""
        try:
            return self.get(key, default)
        except KeyError:
            return default

    def __init_subclass__(cls, configuration_type: Type) -> None:
        """Init cluster subclass."""
        cls.attributes = {**TuyaMCUCluster.attributes}
        cls._populate_mapped_attributes_lookup(cls)
        cls._setup_channel_config_attributes(cls, configuration_type)
        cls._setup_device_attributes(cls)
        super().__init_subclass__()

    def _populate_mapped_attributes_lookup(cls) -> None:
        """Stores a tuple for each cluster attribute mapped from MCU data points."""
        cls.mapped_attributes: Tuple[Tuple[str, str, int]] = tuple(
            (dp_map.ep_attribute, attr_name, dp_map.endpoint_id or TUYA_MCU_ENDPOINT_ID)
            for dp_map in cls.dp_to_attribute.values()
            for attr_name in (
                dp_map.attribute_name
                if isinstance(dp_map.attribute_name, tuple)
                else (dp_map.attribute_name,)
            )
        )

    def _setup_channel_config_attributes(cls, configuration_type: Type) -> None:
        """Setup local attributes for the device channel configuration type."""
        config_type_attr = TuyaEnergyMeterManufCluster.attributes[CHANNEL_CONFIGURATION]
        cls.attributes[CHANNEL_CONFIGURATION] = (
            config_type_attr.name,
            configuration_type,
            config_type_attr.is_manufacturer_specific,
        )
        config_attr = cls._CHANNEL_CONFIGURATION_ATTRIBUTES.get(configuration_type, ())
        for attrid in config_attr:
            cls.attributes[attrid] = TuyaEnergyMeterManufCluster.attributes[attrid]

    def _setup_device_attributes(cls) -> None:
        """Setup manufacturer cluster attributes for mapped device data points."""
        attr_name_to_id: Dict[str, int] = {
            attr[0] if isinstance(attr, tuple) else attr.name: attrid
            for attrid, attr in TuyaEnergyMeterManufCluster.attributes.items()
        }
        for ep_attribute, attr_name, endpoint_id in cls.mapped_attributes:
            if ep_attribute != cls.ep_attribute:
                continue
            assert (
                endpoint_id == 1
            ), "Check endpoint_id of TuyaEnergyMeterManufCluster dp_to_attribute."
            attrid = attr_name_to_id.get(attr_name)
            if attrid is not None:
                cls.attributes[attrid] = TuyaEnergyMeterManufCluster.attributes[attrid]


class EnergyMeterChannel:
    """Methods and properties for energy meter channel clusters."""

    _ENDPOINT_TO_CHANNEL: Dict[Tuple[Type, int], Channel] = {
        (ChannelConfiguration_1CH, 1): Channel.A,
        (ChannelConfiguration_1CHB, 1): Channel.A,
        (ChannelConfiguration_2CH, 1): Channel.A,
        (ChannelConfiguration_2CH, 2): Channel.B,
        (ChannelConfiguration_2CH, 3): Channel.AB,
        (ChannelConfiguration_2CHB, 1): Channel.A,
        (ChannelConfiguration_2CHB, 2): Channel.B,
        (ChannelConfiguration_2CHB, 3): Channel.AB,
    }

    _EXTENSIVE_ATTRIBUTES: Tuple[str] = ()
    _INTENSIVE_ATTRIBUTES: Tuple[str] = ()
    _CUMULATIVE_FORWARD_ATTRIBUTES: Tuple[str] = ()
    _CUMULATIVE_REVERSE_ATTRIBUTES: Tuple[str] = ()
    _INVERSE_ATTRIBUTES: Dict[str, str] = {}

    def __init__(self, *args, **kwargs):
        """Init."""
        self._CHANNEL_TO_ENDPOINT: Dict[Tuple[Type, Channel], int] = {
            (k[0], v): k[1] for k, v in self._ENDPOINT_TO_CHANNEL.items()
        }
        self._INVERSE_ATTRIBUTES.update(
            {v: k for k, v in dict(self._INVERSE_ATTRIBUTES).items()}
        )
        self._CUMULATIVE_ATTRIBUTES = (
            self._CUMULATIVE_FORWARD_ATTRIBUTES + self._CUMULATIVE_REVERSE_ATTRIBUTES
        )
        super().__init__(*args, **kwargs)

    @property
    def channel(self) -> Optional[str]:
        """Returns the cluster's channel."""
        return self._ENDPOINT_TO_CHANNEL.get(
            (self.channel_configuration_type, self.endpoint.endpoint_id), None
        )

    @property
    def channel_configuration(self) -> Optional[ChannelConfiguration]:
        """Returns the device's current channel configuration."""
        return self.manufacturer_cluster.get("channel_configuration")

    @property
    def channel_configuration_type(self) -> Type:
        """Returns the device's channel configuration type."""
        return self.manufacturer_cluster.AttributeDefs.channel_configuration.type

    @property
    def manufacturer_cluster(self) -> TuyaEnergyMeterManufCluster:
        """Returns the device's manufacturer cluster."""
        return getattr(
            self.endpoint.device.endpoints[TUYA_MCU_ENDPOINT_ID],
            TuyaEnergyMeterManufCluster.ep_attribute,
        )

    def attr_present(
        self,
        *attr_names: str,
        ep_attribute: Optional[str] = None,
        endpoint_id: Optional[int] = None,
    ) -> bool:
        """Returns True if any of the specified attributes are provided by the device."""
        ep_attribute = ep_attribute or self.ep_attribute
        endpoint_id = endpoint_id or self.endpoint.endpoint_id
        return any(
            attr in self.manufacturer_cluster.mapped_attributes
            for attr in tuple(
                (ep_attribute, attr_name, endpoint_id) for attr_name in attr_names
            )
        )

    def attr_type(self, attr_name: str) -> Type:
        """Returns the type of the specified attribute."""
        return getattr(self.AttributeDefs, attr_name).type

    def get_cluster(
        self,
        channel_or_endpoint_id: Union[Channel, int],
        ep_attribute: Optional[str] = None,
    ):
        """Returns the device cluster for the given channel or endpoint."""
        if channel_or_endpoint_id in Channel:
            channel_or_endpoint_id = self._CHANNEL_TO_ENDPOINT.get(
                (self.channel_configuration_type, channel_or_endpoint_id), None
            )
        assert channel_or_endpoint_id is not None, "Invalid channel_or_endpoint_id."
        return getattr(
            self.endpoint.device.endpoints[channel_or_endpoint_id],
            ep_attribute or self.ep_attribute,
        )

    def update_calculated_attribute(self, attr_name: str, calculated_value) -> None:
        """Updates the specified attribute if the calculated value is valid."""
        if calculated_value is None:
            return
        self.update_attribute(attr_name, calculated_value)


class EnergyMeterPowerFlow(EnergyMeterChannel):
    """Methods and properties for handling power flow on Tuya energy meter devices."""

    @property
    def power_flow(self) -> Optional[PowerFlow]:
        """Returns the channel's current power flow direction."""
        return self.manufacturer_cluster.get_optional(
            Channel.attr_with_channel("power_flow", self.channel)
        )

    @power_flow.setter
    def power_flow(self, value: PowerFlow) -> None:
        """Updates the channel's power flow direction."""
        self.manufacturer_cluster.update_attribute(
            Channel.attr_with_channel("power_flow", self.channel), value
        )

    @property
    def suppress_reverse_flow(self) -> bool:
        """Returns True if suppress_reverse_flow is enabled for the channel."""
        return self.manufacturer_cluster.get_optional(
            Channel.attr_with_channel("suppress_reverse_flow", self.channel), False
        )

    def _align_unsigned_attribute_with_power_flow(
        self, attr_name: str, value
    ) -> Tuple[str, Any]:
        """Attributes marked as unsigned are aligned with the current power flow direction."""
        if attr_name.endswith(UNSIGNED_POWER_ATTR_SUFFIX):
            attr_name = attr_name.removesuffix(UNSIGNED_POWER_ATTR_SUFFIX)
            value = PowerFlow.align_value(value, self.power_flow)
        return attr_name, value

    def _suppress_reverse_power_flow(self, attr_name: str, value) -> Optional[Any]:
        """Returns 0 if suppress_reverse_flow is enabled for the channel and power flow is reverse."""
        if self.suppress_reverse_flow and (
            attr_name in self._EXTENSIVE_ATTRIBUTES
            and self.power_flow == PowerFlow.REVERSE
            or attr_name in self._CUMULATIVE_REVERSE_ATTRIBUTES
        ):
            value = 0
        return value

    def power_flow_handler(self, attr_name: str, value) -> Tuple[str, Any]:
        """Orchestrates processing of directional attributes."""
        attr_name, value = self._align_unsigned_attribute_with_power_flow(
            attr_name, value
        )
        value = self._suppress_reverse_power_flow(attr_name, value)
        return attr_name, value


class PowerFlowPreemptConfiguration:
    """Contains the parameters for preempting power_flow direction."""

    def __init__(
        self,
        source_channels: tuple = (),
        trigger_channel: Optional[Channel] = None,
        preempt_method: Optional[Callable] = None,
    ) -> None:
        self.source_channels = source_channels
        self.trigger_channel = trigger_channel
        self.preempt_method = preempt_method


class PowerFlowPreempt(EnergyMeterPowerFlow, EnergyMeterChannel):
    """Logic for preempting delayed power flow direction change on 2 channel devices."""

    HOLD = "hold"
    PREEMPT = "preempt"
    RELEASE = "release"

    @property
    def power_flow_preempt(self) -> bool:
        """Returns True if power_flow_preempt is enabled for the device."""
        return self.manufacturer_cluster.get_optional("power_flow_preempt", False)

    def __init__(self, *args, **kwargs):
        """Init."""
        self._preempt_values: Dict[str, Optional[int]] = {}
        super().__init__(*args, **kwargs)

    def _preempt_grid_plus_production(self, attr_name: str) -> None:
        """Power flow preempt method for grid_plus_production configured devices."""
        cluster_a = self.get_cluster(Channel.A)
        cluster_b = self.get_cluster(Channel.B)
        value_a = cluster_a._get_preempt_value(attr_name)
        value_b = cluster_b._get_preempt_value(attr_name)
        if None in (value_a, value_b):
            return
        cluster_a.power_flow = (
            PowerFlow.FORWARD
            if cluster_a.power_flow == PowerFlow.REVERSE and abs(value_a) > abs(value_b)
            else cluster_a.power_flow
        )
        cluster_b.power_flow = (
            PowerFlow.FORWARD
            if cluster_b.power_flow == PowerFlow.REVERSE and abs(value_b) > abs(value_a)
            else cluster_b.power_flow
        )

    _PREEMPT_CONFIGURATION: Dict[
        ChannelConfiguration, PowerFlowPreemptConfiguration
    ] = {
        ChannelConfiguration.GRID_PLUS_PRODUCTION: PowerFlowPreemptConfiguration(
            (Channel.A, Channel.B),
            Channel.B,
            _preempt_grid_plus_production,
        ),
    }

    def _preempt_action(
        self, attr_name: str, value: int, trigger_channel: Channel
    ) -> str:
        """Returns the action for the power flow preempt handler."""
        if self.channel == trigger_channel:
            return self.PREEMPT
        if self._get_preempt_value(attr_name) != value:
            return self.HOLD
        return self.RELEASE

    def _get_preempt_value(self, attr_name: str) -> Optional[int]:
        """Retrieves the value which was held for consideration in the preempt method."""
        return self._preempt_values.get(attr_name, None)

    def _store_preempt_value(self, attr_name: str, value: Optional[int]) -> None:
        """Stores the value for consideration in the preempt method."""
        self._preempt_values[attr_name] = value

    def _release_preempt_values(
        self, attr_name: str, source_channels: Tuple[Channel], trigger_channel: Channel
    ) -> None:
        """Releases held values to update the cluster attributes following the preempt method."""
        for channel in source_channels:
            cluster = self.get_cluster(channel)
            if channel != trigger_channel:
                value = cluster._get_preempt_value(attr_name)
                if value is not None:
                    cluster.update_attribute(attr_name, value)
            cluster._store_preempt_value(attr_name, None)

    def power_flow_preempt_handler(self, attr_name: str, value) -> Optional[str]:
        """Compensates for delay in reported power flow direction."""

        if (
            not self.power_flow_preempt
            or attr_name.removesuffix(UNSIGNED_POWER_ATTR_SUFFIX)
            not in self._EXTENSIVE_ATTRIBUTES
            or not self.attr_present(attr_name)
        ):
            return

        config = self._PREEMPT_CONFIGURATION.get(
            self.channel_configuration, PowerFlowPreemptConfiguration()
        )
        if not config.preempt_method or self.channel not in config.source_channels:
            return

        action = self._preempt_action(attr_name, value, config.trigger_channel)
        if action != self.RELEASE:
            self._store_preempt_value(attr_name, value)
        if action != self.PREEMPT:
            return action
        config.preempt_method(self, attr_name)
        self._release_preempt_values(
            attr_name, config.source_channels, config.trigger_channel
        )
        return action


class VirtualChannelConfiguration:
    """Contains the parameters for updating a virtual channel."""

    def __init__(
        self,
        virtual_channel: Optional[Channel] = None,
        source_channels: tuple = (),
        trigger_channel: Optional[Channel] = None,
        discrete_method: Optional[Callable] = None,
        cumulative_method: Optional[Callable] = None,
    ) -> None:
        self.virtual_channel = virtual_channel
        self.source_channels = source_channels
        self.trigger_channel = trigger_channel
        self.discrete_method = discrete_method
        self.cumulative_method = cumulative_method


class VirtualChannel(EnergyMeterPowerFlow, EnergyMeterChannel):
    """Methods and properties for updating virtual energy meter channel attributes."""

    @property
    def virtual_channel(self) -> Optional[Channel]:
        """Returns the virtual channel for the current configuration."""
        return self._VIRTUAL_CHANNEL_CONFIGURATION.get(
            self.channel_configuration,
            VirtualChannelConfiguration(),
        ).virtual_channel

    def __init__(self, *args, **kwargs):
        """Init."""
        self._virtual_channel_stored_values: Dict[str, Dict[str, int]] = {}
        super().__init__(*args, **kwargs)

    def _a_plus_b(self, attr_name: str) -> Optional[int]:
        """Method for calculating virtual channel values in a_plus_b configuration types."""

        cluster_a = self.get_cluster(Channel.A)
        cluster_b = self.get_cluster(Channel.B)
        value_a = cluster_a.get(attr_name)
        value_b = cluster_b.get(attr_name)

        if None in (value_a, value_b):
            return
        if attr_name in self._EXTENSIVE_ATTRIBUTES and is_type_uint(
            self.attr_type(attr_name)
        ):
            value_a = PowerFlow.align_value(value_a, cluster_a.power_flow)
            value_b = PowerFlow.align_value(value_b, cluster_b.power_flow)

        return value_a + value_b

    def _a_minus_b(self, attr_name: str) -> Optional[int]:
        """Method for calculating virtual channel values in a_minus_b configuration types."""

        cluster_a = self.get_cluster(Channel.A)
        cluster_b = self.get_cluster(Channel.B)
        value_a = cluster_a.get(attr_name)
        value_b = cluster_b.get(attr_name)

        if None in (value_a, value_b):
            return
        if attr_name in self._EXTENSIVE_ATTRIBUTES and is_type_uint(
            self.attr_type(attr_name)
        ):
            value_a = PowerFlow.align_value(value_a, cluster_a.power_flow)
            value_b = PowerFlow.align_value(value_b, cluster_b.power_flow)

        return value_a - value_b

    def _cumulative_grid_plus_production(self, attr_name: str) -> Optional[t.uint_t]:
        """Method for calculating cumulative virtual channel values in grid_plus_production configuration."""

        if attr_name in self._CUMULATIVE_REVERSE_ATTRIBUTES:
            return 0
        inv_attr_name = self._INVERSE_ATTRIBUTES.get(attr_name, None)
        assert (
            inv_attr_name is not None
        ), "An inverse attribute must be defined for cumulative values."

        cluster_a = self.get_cluster(Channel.A)
        cluster_b = self.get_cluster(Channel.B)
        value_a = cluster_a.get(attr_name)
        value_a_inv = cluster_a.get(inv_attr_name)
        value_b = cluster_b.get(attr_name)
        value_b_inv = cluster_b.get(inv_attr_name)

        if None in (value_a, value_a_inv, value_b, value_b_inv):
            return
        return (value_a + value_b) - (value_a_inv + value_b_inv)

    def _cumulative_consumption_minus_production(
        self, attr_name: str
    ) -> Optional[t.uint_t]:
        """Method for calculating cumulative virtual channel values in consumption_minus_production configuration."""

        inv_attr_name = self._INVERSE_ATTRIBUTES.get(attr_name, None)
        assert (
            inv_attr_name is not None
        ), "An inverse attribute must be defined for cumulative values."

        cluster_a = self.get_cluster(Channel.A)
        cluster_b = self.get_cluster(Channel.B)
        cluster_ab = self.get_cluster(Channel.AB)
        value_a = cluster_a.get(attr_name)
        value_a_inv = cluster_a.get(inv_attr_name)
        value_b = cluster_b.get(attr_name)
        value_b_inv = cluster_b.get(inv_attr_name)
        value_ab = cluster_ab.get(attr_name, 0)

        value_a_prev = cluster_a._get_previous_value(attr_name)
        value_a_inv_prev = cluster_a._get_previous_value(inv_attr_name, attr_name)
        value_b_prev = cluster_a._get_previous_value(attr_name)
        value_b_inv_prev = cluster_b._get_previous_value(inv_attr_name, attr_name)

        cluster_a._store_current_value(attr_name)
        cluster_a._store_current_value(inv_attr_name, attr_name)
        cluster_b._store_current_value(attr_name)
        cluster_b._store_current_value(inv_attr_name, attr_name)

        if None in (value_a, value_a_inv, value_b, value_b_inv):
            return

        delta = (value_a - value_a_prev) - (value_b - value_b_prev)
        delta_inv = (value_a_inv - value_a_inv_prev) - (value_b_inv - value_b_inv_prev)

        return (
            value_ab + (delta if delta > 0 else 0) - (delta_inv if delta_inv < 0 else 0)
        )

    _VIRTUAL_CHANNEL_CONFIGURATION: Dict[
        ChannelConfiguration, VirtualChannelConfiguration
    ] = {
        ChannelConfiguration.A_PLUS_B: VirtualChannelConfiguration(
            Channel.AB,
            (Channel.A, Channel.B),
            Channel.B,
            _a_plus_b,
            _a_plus_b,
        ),
        ChannelConfiguration.A_MINUS_B: VirtualChannelConfiguration(
            Channel.AB,
            (Channel.A, Channel.B),
            Channel.B,
            _a_minus_b,
            _a_minus_b,
        ),
        ChannelConfiguration.GRID_PLUS_PRODUCTION: VirtualChannelConfiguration(
            Channel.AB,
            (Channel.A, Channel.B),
            Channel.B,
            _a_plus_b,
            _cumulative_grid_plus_production,
        ),
        ChannelConfiguration.CONSUMPTION_MINUS_PRODUCTION: VirtualChannelConfiguration(
            Channel.AB,
            (Channel.A, Channel.B),
            Channel.B,
            _a_minus_b,
            _cumulative_consumption_minus_production,
        ),
    }

    def _get_previous_value(
        self, attr_name: str, child_key: Optional[str] = None
    ) -> Optional[int]:
        """Returns the stored value of the attribute."""
        child_key = child_key if child_key else attr_name
        if attr_name in self._virtual_channel_stored_values:
            return self._virtual_channel_stored_values[attr_name].get(
                child_key, self._virtual_channel_stored_values[attr_name][attr_name]
            )
        else:
            return self.get(attr_name)

    def _store_current_value(
        self, attr_name: str, child_key: Optional[str] = None
    ) -> None:
        """Stores the current value of the attribute."""
        child_key = child_key if child_key else attr_name
        value = self.get(attr_name)
        if attr_name in self._virtual_channel_stored_values:
            self._virtual_channel_stored_values[attr_name][child_key] = value
        else:
            self._virtual_channel_stored_values[attr_name] = {child_key: value}

    def virtual_channel_initial_values(self, attr_name: str, value):
        """Retains the initial attribute value for use in delta calculations."""
        if (
            attr_name in self._CUMULATIVE_ATTRIBUTES
            and ChannelConfiguration.CONSUMPTION_MINUS_PRODUCTION
            in self.channel_configuration_type
            and attr_name not in self._virtual_channel_stored_values
        ):
            self._store_current_value(attr_name)

    def virtual_channel_handler(self, attr_name: str) -> None:
        """Handles updates to a virtual energy meter channel."""

        config = self._VIRTUAL_CHANNEL_CONFIGURATION.get(
            self.channel_configuration,
            VirtualChannelConfiguration(),
        )

        if (
            self.channel not in config.source_channels
            or self.channel != config.trigger_channel
            and attr_name not in self._CUMULATIVE_ATTRIBUTES
        ):
            return

        method = None
        if attr_name in self._EXTENSIVE_ATTRIBUTES:
            method = config.discrete_method
        elif attr_name in self._CUMULATIVE_ATTRIBUTES:
            method = config.cumulative_method
        if not method:
            return

        virtual_value = method(self, attr_name)
        if virtual_value is None:
            return
        virtual_cluster = self.get_cluster(config.virtual_channel)
        virtual_cluster.update_attribute(attr_name, virtual_value)


class TuyaElectricalMeasurement(
    VirtualChannel,
    PowerFlowPreempt,
    EnergyMeterPowerFlow,
    EnergyMeterChannel,
    TuyaLocalCluster,
    TuyaZBElectricalMeasurement,
):
    """ElectricalMeasurement cluster for Tuya energy meter devices."""

    _CONSTANT_ATTRIBUTES: Dict[int, Any] = {
        **TuyaZBElectricalMeasurement._CONSTANT_ATTRIBUTES,
        TuyaZBElectricalMeasurement.AttributeDefs.ac_frequency_divisor.id: 100,
        TuyaZBElectricalMeasurement.AttributeDefs.ac_frequency_multiplier.id: 1,
        TuyaZBElectricalMeasurement.AttributeDefs.ac_power_divisor.id: 10,
        TuyaZBElectricalMeasurement.AttributeDefs.ac_power_multiplier.id: 1,
        TuyaZBElectricalMeasurement.AttributeDefs.ac_voltage_divisor.id: 10,
        TuyaZBElectricalMeasurement.AttributeDefs.ac_voltage_multiplier.id: 1,
    }

    _ATTRIBUTE_MEASUREMENT_TYPES: Dict[str, MeasurementType] = {
        "active_power": MeasurementType.Active_measurement_AC
        | MeasurementType.Phase_A_measurement,
        "active_power_ph_b": MeasurementType.Active_measurement_AC
        | MeasurementType.Phase_B_measurement,
        "active_power_ph_c": MeasurementType.Active_measurement_AC
        | MeasurementType.Phase_C_measurement,
        "reactive_power": MeasurementType.Reactive_measurement_AC
        | MeasurementType.Phase_A_measurement,
        "reactive_power_ph_b": MeasurementType.Reactive_measurement_AC
        | MeasurementType.Phase_B_measurement,
        "reactive_power_ph_c": MeasurementType.Reactive_measurement_AC
        | MeasurementType.Phase_C_measurement,
        "apparent_power": MeasurementType.Apparent_measurement_AC
        | MeasurementType.Phase_A_measurement,
        "apparent_power_ph_b": MeasurementType.Apparent_measurement_AC
        | MeasurementType.Phase_B_measurement,
        "apparent_power_ph_c": MeasurementType.Apparent_measurement_AC
        | MeasurementType.Phase_C_measurement,
    }

    _EXTENSIVE_ATTRIBUTES: Tuple[str] = (
        "active_power",
        "apparent_power",
        "reactive_power",
        "rms_current",
    )
    _INTENSIVE_ATTRIBUTES: Tuple[str] = ("rms_voltage",)

    def calculated_attributes(self, attr_name: str, value) -> None:
        """Calculates attributes that are not reported by the device."""

        if (
            self.channel == self.virtual_channel
        ):  # Attributes are not calculated for the virtual channel.
            return

        if attr_name == "apparent_power" and not self.attr_present("active_power"):
            self.update_calculated_attribute(
                "active_power",
                PowerCalculation.active_power_from_apparent_power_power_factor_and_power_flow(
                    value, self.get("power_factor"), self.power_flow
                ),
            )

        if attr_name == "apparent_power" and not self.attr_present("reactive_power"):
            self.update_calculated_attribute(
                "reactive_power",
                PowerCalculation.reactive_power_from_apparent_power_and_power_factor(
                    value, self.get("power_factor")
                ),
            )

        if attr_name == "active_power" and not self.attr_present(
            "apparent_power", "rms_current"
        ):
            self.update_calculated_attribute(
                "apparent_power",
                PowerCalculation.apparent_power_from_active_power_and_power_factor(
                    value, self.get("power_factor")
                ),
            )

        if attr_name == "rms_current" and not self.attr_present("apparent_power"):
            self.update_calculated_attribute(
                "apparent_power",
                PowerCalculation.apparent_power_from_rms_current_and_rms_voltage(
                    value,
                    self.get("rms_voltage")
                    or self.get_cluster(Channel.A).get("rms_voltage"),
                    self.get("ac_current_divisor", 1),
                    self.get("ac_current_multiplier", 1),
                    self.get("ac_voltage_divisor", 1),
                    self.get("ac_voltage_multiplier", 1),
                    self.get("ac_power_divisor", 1),
                    self.get("ac_power_multiplier", 1),
                ),
            )

    def update_attribute(self, attr_name: str, value) -> None:
        """Updates the cluster attribute."""
        if self.power_flow_preempt_handler(attr_name, value) == PowerFlowPreempt.HOLD:
            return
        attr_name, value = self.power_flow_handler(attr_name, value)
        self.update_measurement_type(attr_name)
        self.calculated_attributes(attr_name, value)
        self.virtual_channel_initial_values(attr_name, value)
        super().update_attribute(attr_name, value)
        self.virtual_channel_handler(attr_name)

    def update_measurement_type(self, attr_name: str) -> None:
        """Derives the measurement type from reported attributes."""
        if attr_name not in self._ATTRIBUTE_MEASUREMENT_TYPES:
            return
        measurement_type = 0
        for measurement, mask in self._ATTRIBUTE_MEASUREMENT_TYPES.items():
            if measurement == attr_name or self.get(measurement) is not None:
                measurement_type |= mask
        super().update_attribute("measurement_type", measurement_type)


class TuyaMetering(
    VirtualChannel,
    PowerFlowPreempt,
    EnergyMeterPowerFlow,
    EnergyMeterChannel,
    TuyaLocalCluster,
    TuyaZBMeteringClusterWithUnit,
):
    """Metering cluster for Tuya energy meter devices."""

    _CONSTANT_ATTRIBUTES: Dict[int, Any] = {
        **TuyaZBMeteringClusterWithUnit._CONSTANT_ATTRIBUTES,
        TuyaZBMeteringClusterWithUnit.AttributeDefs.status.id: 0x00,
        TuyaZBMeteringClusterWithUnit.AttributeDefs.multiplier.id: 1,
        TuyaZBMeteringClusterWithUnit.AttributeDefs.divisor.id: 10000,  # 1 decimal place after conversion from kW to W
        TuyaZBMeteringClusterWithUnit.AttributeDefs.summation_formatting.id: Metering.format(
            7, 2, True
        ),
        TuyaZBMeteringClusterWithUnit.AttributeDefs.demand_formatting.id: Metering.format(
            7, 1, True
        ),
    }

    _EXTENSIVE_ATTRIBUTES: Tuple[str] = ("instantaneous_demand",)
    _CUMULATIVE_FORWARD_ATTRIBUTES: Tuple[str] = ("current_summ_delivered",)
    _CUMULATIVE_REVERSE_ATTRIBUTES: Tuple[str] = ("current_summ_received",)
    _INVERSE_ATTRIBUTES: Dict[str, str] = {
        "current_summ_delivered": "current_summ_received",
    }

    def update_attribute(self, attr_name: str, value) -> None:
        """Updates the cluster attribute."""
        if self.power_flow_preempt_handler(attr_name, value) == PowerFlowPreempt.HOLD:
            return
        attr_name, value = self.power_flow_handler(attr_name, value)
        self.virtual_channel_initial_values(attr_name, value)
        super().update_attribute(attr_name, value)
        self.virtual_channel_handler(attr_name)


class TuyaEnergyMeterManufCluster_1CH(
    TuyaEnergyMeterManufCluster, configuration_type=ChannelConfiguration_1CH
):
    """Tuya 1 channel energy meter manufacturer cluster."""

    TUYA_DP_CURRENT_SUMM_DELIVERED = 101
    TUYA_DP_INSTANTANEOUS_DEMAND_UINT = 19
    TUYA_DP_RMS_CURRENT = 18
    TUYA_DP_RMS_VOLTAGE = 20

    dp_to_attribute: Dict[int, DPToAttributeMapping] = {
        TUYA_DP_CURRENT_SUMM_DELIVERED: DPToAttributeMapping(
            TuyaMetering.ep_attribute,
            "current_summ_delivered",
            converter=lambda x: x * 10,
        ),
        TUYA_DP_INSTANTANEOUS_DEMAND_UINT: DPToAttributeMapping(
            TuyaMetering.ep_attribute,
            "instantaneous_demand",
        ),
        TUYA_DP_RMS_CURRENT: DPToAttributeMapping(
            TuyaElectricalMeasurement.ep_attribute,
            "rms_current",
        ),
        TUYA_DP_RMS_VOLTAGE: DPToAttributeMapping(
            TuyaElectricalMeasurement.ep_attribute,
            "rms_voltage",
        ),
    }

    data_point_handlers = {
        TUYA_DP_CURRENT_SUMM_DELIVERED: "_dp_2_attr_update",
        TUYA_DP_INSTANTANEOUS_DEMAND_UINT: "_dp_2_attr_update",
        TUYA_DP_RMS_CURRENT: "_dp_2_attr_update",
        TUYA_DP_RMS_VOLTAGE: "_dp_2_attr_update",
    }


class TuyaEnergyMeterManufCluster_1CHB(
    TuyaEnergyMeterManufCluster,
    configuration_type=ChannelConfiguration_1CHB,
):
    """Tuya 1 channel bidirectional energy meter manufacturer cluster."""

    TUYA_DP_CURRENT_SUMM_DELIVERED = 1
    TUYA_DP_CURRENT_SUMM_RECEIVED = 2
    TUYA_DP_INSTANTANEOUS_DEMAND_UINT = 101
    TUYA_DP_POWER_FLOW = 102
    TUYA_DP_POWER_PHASE = 6

    dp_to_attribute: Dict[int, DPToAttributeMapping] = {
        TUYA_DP_CURRENT_SUMM_DELIVERED: DPToAttributeMapping(
            TuyaMetering.ep_attribute,
            "current_summ_delivered",
            converter=lambda x: x * 100,
        ),
        TUYA_DP_CURRENT_SUMM_RECEIVED: DPToAttributeMapping(
            TuyaMetering.ep_attribute,
            "current_summ_received",
            converter=lambda x: x * 100,
        ),
        TUYA_DP_INSTANTANEOUS_DEMAND_UINT: DPToAttributeMapping(
            TuyaMetering.ep_attribute,
            "instantaneous_demand" + UNSIGNED_POWER_ATTR_SUFFIX,
            converter=lambda x: x * 10,
        ),
        TUYA_DP_POWER_FLOW: DPToAttributeMapping(
            TuyaEnergyMeterManufCluster.ep_attribute,
            "power_flow",
            converter=lambda x: PowerFlow(x),
        ),
        TUYA_DP_POWER_PHASE: DPToAttributeMapping(
            TuyaElectricalMeasurement.ep_attribute,
            (
                "rms_voltage",
                "rms_current",
                "active_power" + UNSIGNED_POWER_ATTR_SUFFIX,
            ),
            converter=lambda x: TuyaPowerPhase.variant_3(x),
        ),
    }

    data_point_handlers = {
        TUYA_DP_CURRENT_SUMM_DELIVERED: "_dp_2_attr_update",
        TUYA_DP_CURRENT_SUMM_RECEIVED: "_dp_2_attr_update",
        TUYA_DP_INSTANTANEOUS_DEMAND_UINT: "_dp_2_attr_update",
        TUYA_DP_POWER_FLOW: "_dp_2_attr_update",
        TUYA_DP_POWER_PHASE: "_dp_2_attr_update",
    }


class TuyaEnergyMeterManufCluster_2CHB_MatSeePlus(
    TuyaEnergyMeterManufCluster, configuration_type=ChannelConfiguration_2CHB
):
    """MatSee Plus Tuya 2 channel bidirectional energy meter manufacturer cluster."""

    _ATTRIBUTE_DEFAULTS: Dict[int, Any] = {
        POWER_FLOW_PREEMPT: True,
    }

    TUYA_DP_AC_FREQUENCY = 111
    TUYA_DP_AC_FREQUENCY_COEF = 122
    TUYA_DP_CURRENT_SUMM_DELIVERED = 106
    TUYA_DP_CURRENT_SUMM_DELIVERED_COEF = 119
    TUYA_DP_CURRENT_SUMM_DELIVERED_B = 108
    TUYA_DP_CURRENT_SUMM_DELIVERED_COEF_B = 125
    TUYA_DP_CURRENT_SUMM_RECEIVED = 107
    TUYA_DP_CURRENT_SUMM_RECEIVED_COEF = 127
    TUYA_DP_CURRENT_SUMM_RECEIVED_B = 109
    TUYA_DP_CURRENT_SUMM_RECEIVED_COEF_B = 128
    TUYA_DP_INSTANTANEOUS_DEMAND_UINT = 101
    TUYA_DP_INSTANTANEOUS_DEMAND_UINT_B = 105
    TUYA_DP_INSTANTANEOUS_DEMAND_COEF = 118
    TUYA_DP_INSTANTANEOUS_DEMAND_COEF_B = 124
    TUYA_DP_POWER_FACTOR = 110
    TUYA_DP_POWER_FACTOR_B = 121
    TUYA_DP_POWER_FLOW = 102
    TUYA_DP_POWER_FLOW_B = 104
    TUYA_DP_UPDATE_PERIOD = 129
    TUYA_DP_RMS_CURRENT = 113
    TUYA_DP_RMS_CURRENT_COEF = 117
    TUYA_DP_RMS_CURRENT_B = 114
    TUYA_DP_RMS_CURRENT_COEF_B = 123
    TUYA_DP_RMS_VOLTAGE = 112
    TUYA_DP_RMS_VOLTAGE_COEF = 116

    dp_to_attribute: Dict[int, DPToAttributeMapping] = {
        TUYA_DP_AC_FREQUENCY: DPToAttributeMapping(
            TuyaElectricalMeasurement.ep_attribute,
            "ac_frequency",
        ),
        TUYA_DP_AC_FREQUENCY_COEF: DPToAttributeMapping(
            TuyaEnergyMeterManufCluster.ep_attribute,
            "ac_frequency_coefficient",
        ),
        TUYA_DP_CURRENT_SUMM_DELIVERED: DPToAttributeMapping(
            TuyaMetering.ep_attribute,
            "current_summ_delivered",
            converter=lambda x: x * 100,
        ),
        TUYA_DP_CURRENT_SUMM_DELIVERED_B: DPToAttributeMapping(
            TuyaMetering.ep_attribute,
            "current_summ_delivered",
            endpoint_id=2,
            converter=lambda x: x * 100,
        ),
        TUYA_DP_CURRENT_SUMM_DELIVERED_COEF: DPToAttributeMapping(
            TuyaEnergyMeterManufCluster.ep_attribute,
            "current_summ_delivered_coefficient",
        ),
        TUYA_DP_CURRENT_SUMM_DELIVERED_COEF_B: DPToAttributeMapping(
            TuyaEnergyMeterManufCluster.ep_attribute,
            "current_summ_delivered_coefficient_ch_b",
        ),
        TUYA_DP_CURRENT_SUMM_RECEIVED: DPToAttributeMapping(
            TuyaMetering.ep_attribute,
            "current_summ_received",
            converter=lambda x: x * 100,
        ),
        TUYA_DP_CURRENT_SUMM_RECEIVED_B: DPToAttributeMapping(
            TuyaMetering.ep_attribute,
            "current_summ_received",
            endpoint_id=2,
            converter=lambda x: x * 100,
        ),
        TUYA_DP_CURRENT_SUMM_RECEIVED_COEF: DPToAttributeMapping(
            TuyaEnergyMeterManufCluster.ep_attribute,
            "current_summ_received_coefficient",
        ),
        TUYA_DP_CURRENT_SUMM_RECEIVED_COEF_B: DPToAttributeMapping(
            TuyaEnergyMeterManufCluster.ep_attribute,
            "current_summ_received_coefficient_ch_b",
        ),
        TUYA_DP_INSTANTANEOUS_DEMAND_UINT: DPToAttributeMapping(
            TuyaMetering.ep_attribute,
            "instantaneous_demand" + UNSIGNED_POWER_ATTR_SUFFIX,
        ),
        TUYA_DP_INSTANTANEOUS_DEMAND_UINT_B: DPToAttributeMapping(
            TuyaMetering.ep_attribute,
            "instantaneous_demand" + UNSIGNED_POWER_ATTR_SUFFIX,
            endpoint_id=2,
        ),
        TUYA_DP_INSTANTANEOUS_DEMAND_COEF: DPToAttributeMapping(
            TuyaEnergyMeterManufCluster.ep_attribute,
            "instantaneous_demand_coefficient",
        ),
        TUYA_DP_INSTANTANEOUS_DEMAND_COEF_B: DPToAttributeMapping(
            TuyaEnergyMeterManufCluster.ep_attribute,
            "instantaneous_demand_coefficient_ch_b",
        ),
        TUYA_DP_POWER_FACTOR: DPToAttributeMapping(
            TuyaElectricalMeasurement.ep_attribute,
            "power_factor",
        ),
        TUYA_DP_POWER_FACTOR_B: DPToAttributeMapping(
            TuyaElectricalMeasurement.ep_attribute,
            "power_factor",
            endpoint_id=2,
        ),
        TUYA_DP_POWER_FLOW: DPToAttributeMapping(
            TuyaEnergyMeterManufCluster.ep_attribute,
            "power_flow",
            converter=lambda x: PowerFlow(x),
        ),
        TUYA_DP_POWER_FLOW_B: DPToAttributeMapping(
            TuyaEnergyMeterManufCluster.ep_attribute,
            "power_flow_ch_b",
            converter=lambda x: PowerFlow(x),
        ),
        TUYA_DP_RMS_CURRENT: DPToAttributeMapping(
            TuyaElectricalMeasurement.ep_attribute,
            "rms_current",
        ),
        TUYA_DP_RMS_CURRENT_B: DPToAttributeMapping(
            TuyaElectricalMeasurement.ep_attribute,
            "rms_current",
            endpoint_id=2,
        ),
        TUYA_DP_RMS_CURRENT_COEF: DPToAttributeMapping(
            TuyaEnergyMeterManufCluster.ep_attribute,
            "rms_current_coefficient",
        ),
        TUYA_DP_RMS_CURRENT_COEF_B: DPToAttributeMapping(
            TuyaEnergyMeterManufCluster.ep_attribute,
            "rms_current_coefficient_ch_b",
        ),
        TUYA_DP_RMS_VOLTAGE: DPToAttributeMapping(
            TuyaElectricalMeasurement.ep_attribute,
            "rms_voltage",
        ),
        TUYA_DP_RMS_VOLTAGE_COEF: DPToAttributeMapping(
            TuyaEnergyMeterManufCluster.ep_attribute,
            "rms_voltage_coefficient",
        ),
        TUYA_DP_UPDATE_PERIOD: DPToAttributeMapping(
            TuyaEnergyMeterManufCluster.ep_attribute,
            "update_period",
        ),
    }

    data_point_handlers = {
        TUYA_DP_AC_FREQUENCY: "_dp_2_attr_update",
        TUYA_DP_AC_FREQUENCY_COEF: "_dp_2_attr_update",
        TUYA_DP_CURRENT_SUMM_DELIVERED: "_dp_2_attr_update",
        TUYA_DP_CURRENT_SUMM_DELIVERED_COEF: "_dp_2_attr_update",
        TUYA_DP_CURRENT_SUMM_DELIVERED_B: "_dp_2_attr_update",
        TUYA_DP_CURRENT_SUMM_DELIVERED_COEF_B: "_dp_2_attr_update",
        TUYA_DP_CURRENT_SUMM_RECEIVED: "_dp_2_attr_update",
        TUYA_DP_CURRENT_SUMM_RECEIVED_COEF: "_dp_2_attr_update",
        TUYA_DP_CURRENT_SUMM_RECEIVED_B: "_dp_2_attr_update",
        TUYA_DP_CURRENT_SUMM_RECEIVED_COEF_B: "_dp_2_attr_update",
        TUYA_DP_INSTANTANEOUS_DEMAND_UINT: "_dp_2_attr_update",
        TUYA_DP_INSTANTANEOUS_DEMAND_UINT_B: "_dp_2_attr_update",
        TUYA_DP_INSTANTANEOUS_DEMAND_COEF: "_dp_2_attr_update",
        TUYA_DP_INSTANTANEOUS_DEMAND_COEF_B: "_dp_2_attr_update",
        TUYA_DP_POWER_FACTOR: "_dp_2_attr_update",
        TUYA_DP_POWER_FACTOR_B: "_dp_2_attr_update",
        TUYA_DP_POWER_FLOW: "_dp_2_attr_update",
        TUYA_DP_POWER_FLOW_B: "_dp_2_attr_update",
        TUYA_DP_RMS_CURRENT: "_dp_2_attr_update",
        TUYA_DP_RMS_CURRENT_B: "_dp_2_attr_update",
        TUYA_DP_RMS_CURRENT_COEF: "_dp_2_attr_update",
        TUYA_DP_RMS_CURRENT_COEF_B: "_dp_2_attr_update",
        TUYA_DP_RMS_VOLTAGE: "_dp_2_attr_update",
        TUYA_DP_RMS_VOLTAGE_COEF: "_dp_2_attr_update",
        TUYA_DP_UPDATE_PERIOD: "_dp_2_attr_update",
    }


class TuyaEnergyMeterManufCluster_2CHB_EARU(
    TuyaEnergyMeterManufCluster, configuration_type=ChannelConfiguration_2CHB
):
    """EARU Tuya 2 channel bidirectional energy meter manufacturer cluster."""

    TUYA_DP_AC_FREQUENCY = 113
    TUYA_DP_CURRENT_SUMM_DELIVERED = 101
    TUYA_DP_CURRENT_SUMM_DELIVERED_B = 103
    TUYA_DP_CURRENT_SUMM_RECEIVED = 102
    TUYA_DP_CURRENT_SUMM_RECEIVED_B = 104
    TUYA_DP_INSTANTANEOUS_DEMAND = 108
    TUYA_DP_INSTANTANEOUS_DEMAND_B = 111
    TUYA_DP_POWER_FACTOR = 109
    TUYA_DP_POWER_FACTOR_B = 112
    TUYA_DP_POWER_FLOW = 114
    TUYA_DP_POWER_FLOW_B = 115
    TUYA_DP_UPDATE_PERIOD = 116
    TUYA_DP_RMS_CURRENT = 107
    TUYA_DP_RMS_CURRENT_B = 110
    TUYA_DP_RMS_VOLTAGE = 106

    dp_to_attribute: Dict[int, DPToAttributeMapping] = {
        TUYA_DP_AC_FREQUENCY: DPToAttributeMapping(
            TuyaElectricalMeasurement.ep_attribute,
            "ac_frequency",
        ),
        TUYA_DP_CURRENT_SUMM_DELIVERED: DPToAttributeMapping(
            TuyaMetering.ep_attribute,
            "current_summ_delivered",
            converter=lambda x: x * 100,
        ),
        TUYA_DP_CURRENT_SUMM_DELIVERED_B: DPToAttributeMapping(
            TuyaMetering.ep_attribute,
            "current_summ_delivered",
            endpoint_id=2,
            converter=lambda x: x * 100,
        ),
        TUYA_DP_CURRENT_SUMM_RECEIVED: DPToAttributeMapping(
            TuyaMetering.ep_attribute,
            "current_summ_received",
            converter=lambda x: x * 100,
        ),
        TUYA_DP_CURRENT_SUMM_RECEIVED_B: DPToAttributeMapping(
            TuyaMetering.ep_attribute,
            "current_summ_received",
            endpoint_id=2,
            converter=lambda x: x * 100,
        ),
        TUYA_DP_INSTANTANEOUS_DEMAND: DPToAttributeMapping(
            TuyaMetering.ep_attribute,
            "instantaneous_demand",
        ),
        TUYA_DP_INSTANTANEOUS_DEMAND_B: DPToAttributeMapping(
            TuyaMetering.ep_attribute,
            "instantaneous_demand",
            endpoint_id=2,
        ),
        TUYA_DP_POWER_FACTOR: DPToAttributeMapping(
            TuyaElectricalMeasurement.ep_attribute,
            "power_factor",
        ),
        TUYA_DP_POWER_FACTOR_B: DPToAttributeMapping(
            TuyaElectricalMeasurement.ep_attribute,
            "power_factor",
            endpoint_id=2,
        ),
        TUYA_DP_POWER_FLOW: DPToAttributeMapping(
            TuyaEnergyMeterManufCluster.ep_attribute,
            "power_flow",
            converter=lambda x: PowerFlow(x),
        ),
        TUYA_DP_POWER_FLOW_B: DPToAttributeMapping(
            TuyaEnergyMeterManufCluster.ep_attribute,
            "power_flow_ch_b",
            converter=lambda x: PowerFlow(x),
        ),
        TUYA_DP_RMS_CURRENT: DPToAttributeMapping(
            TuyaElectricalMeasurement.ep_attribute,
            "rms_current",
        ),
        TUYA_DP_RMS_CURRENT_B: DPToAttributeMapping(
            TuyaElectricalMeasurement.ep_attribute,
            "rms_current",
            endpoint_id=2,
        ),
        TUYA_DP_RMS_VOLTAGE: DPToAttributeMapping(
            TuyaElectricalMeasurement.ep_attribute,
            "rms_voltage",
        ),
        TUYA_DP_UPDATE_PERIOD: DPToAttributeMapping(
            TuyaEnergyMeterManufCluster.ep_attribute,
            "update_period",
        ),
    }

    data_point_handlers = {
        TUYA_DP_AC_FREQUENCY: "_dp_2_attr_update",
        TUYA_DP_CURRENT_SUMM_DELIVERED: "_dp_2_attr_update",
        TUYA_DP_CURRENT_SUMM_DELIVERED_B: "_dp_2_attr_update",
        TUYA_DP_CURRENT_SUMM_RECEIVED: "_dp_2_attr_update",
        TUYA_DP_CURRENT_SUMM_RECEIVED_B: "_dp_2_attr_update",
        TUYA_DP_INSTANTANEOUS_DEMAND: "_dp_2_attr_update",
        TUYA_DP_INSTANTANEOUS_DEMAND_B: "_dp_2_attr_update",
        TUYA_DP_POWER_FACTOR: "_dp_2_attr_update",
        TUYA_DP_POWER_FACTOR_B: "_dp_2_attr_update",
        TUYA_DP_POWER_FLOW: "_dp_2_attr_update",
        TUYA_DP_POWER_FLOW_B: "_dp_2_attr_update",
        TUYA_DP_RMS_CURRENT: "_dp_2_attr_update",
        TUYA_DP_RMS_CURRENT_B: "_dp_2_attr_update",
        TUYA_DP_RMS_VOLTAGE: "_dp_2_attr_update",
        TUYA_DP_UPDATE_PERIOD: "_dp_2_attr_update",
    }


class TuyaEnergyMeter_1CH(CustomDevice):
    """Tuya PJ-MGW1203 1 channel energy meter."""

    signature = {
        MODELS_INFO: [
            ("_TZE204_cjbofhxw", "TS0601"),
            ("_TZE284_cjbofhxw", "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,
                    TuyaMCUCluster.cluster_id,
                    0xed00
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            }
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.METER_INTERFACE,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaEnergyMeterManufCluster_1CH,
                    TuyaElectricalMeasurement,
                    TuyaMetering,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            }
        }
    }


class TuyaEnergyMeter_1CHB(CustomDevice):
    """Tuya bidirectional 1 channel energy meter with Zigbee Green Power."""

    signature = {
        MODELS_INFO: [("_TZE204_ac0fhfiq", "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,
                    TuyaMCUCluster.cluster_id,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            # <SimpleDescriptor endpoint=242 profile=41440 device_type=97
            # input_clusters=[]
            # output_clusters=[33]
            242: {
                PROFILE_ID: zgp.PROFILE_ID,
                DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.METER_INTERFACE,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaEnergyMeterManufCluster_1CHB,
                    TuyaElectricalMeasurement,
                    TuyaMetering,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: {
                PROFILE_ID: zgp.PROFILE_ID,
                DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        }
    }


class TuyaEnergyMeter_2CHB_EARU(CustomDevice):
    """EARU Tuya PC311-Z-TY bidirectional 2 channel energy meter."""

    signature = {
        MODELS_INFO: [("_TZE200_rks0sgb7", "TS0601")],
        ENDPOINTS: {
            # <SimpleDescriptor endpoint=1 profile=260 device_type=51
            # device_version=1
            # input_clusters: [0, 4, 5, 61184, 65382]
            # 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,
                    TuyaMCUCluster.cluster_id,
                    EARU_MANUFACTURER_CLUSTER_ID,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.METER_INTERFACE,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaEnergyMeterManufCluster_2CHB_EARU,
                    TuyaElectricalMeasurement,
                    TuyaMetering,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            2: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.METER_INTERFACE,
                INPUT_CLUSTERS: [
                    TuyaElectricalMeasurement,
                    TuyaMetering,
                ],
                OUTPUT_CLUSTERS: [],
            },
            3: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.METER_INTERFACE,
                INPUT_CLUSTERS: [
                    TuyaElectricalMeasurement,
                    TuyaMetering,
                ],
                OUTPUT_CLUSTERS: [],
            },
        }
    }


class TuyaEnergyMeter_2CHB_MatSeePlus(CustomDevice):
    """MatSee Plus Tuya PJ-1203A 2 channel bidirectional energy meter with Zigbee Green Power."""

    signature = {
        MODELS_INFO: [("_TZE204_81yrt3lo", "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,
                    TuyaMCUCluster.cluster_id,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            # <SimpleDescriptor endpoint=242 profile=41440 device_type=97
            # input_clusters=[]
            # output_clusters=[33]
            242: {
                PROFILE_ID: zgp.PROFILE_ID,
                DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.METER_INTERFACE,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaEnergyMeterManufCluster_2CHB_MatSeePlus,
                    TuyaElectricalMeasurement,
                    TuyaMetering,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            2: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.METER_INTERFACE,
                INPUT_CLUSTERS: [
                    TuyaElectricalMeasurement,
                    TuyaMetering,
                ],
                OUTPUT_CLUSTERS: [],
            },
            3: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.METER_INTERFACE,
                INPUT_CLUSTERS: [
                    TuyaElectricalMeasurement,
                    TuyaMetering,
                ],
                OUTPUT_CLUSTERS: [],
            },
            242: {
                PROFILE_ID: zgp.PROFILE_ID,
                DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        }
    }

@mike-nani
Copy link

Thanks @nachogarcia! Finally some progress! It's now applying the quirk and I can manually read the rms_current and rms_voltage attributes, which seem to present realistic values.
Still no entities, though...

@nachogarcia
Copy link

nachogarcia commented Jun 18, 2024

Thanks @nachogarcia! Finally some progress! It's now applying the quirk and I can manually read the rms_current and rms_voltage attributes, which seem to present realistic values. Still no entities, though...

Try reseting (I just cut the power to the whole house I think). This is mine (I don't think it is reporting properly though).
image

@apapalillo
Copy link

apapalillo commented Jun 19, 2024

I used the quirk I found in #1768 (edit: yeah, that PR @JHurk ) but needed to edit the signature of the 1CH, because it has an extra in put cluster (0xed00, I don't know which kind it is). Disclaimer: I needed to restart it after adding it, and it always had power through the clamp.

class TuyaEnergyMeter_1CH(CustomDevice):
    """Tuya PJ-MGW1203 1 channel energy meter."""

    signature = {
        MODELS_INFO: [
            ("_TZE204_cjbofhxw", "TS0601"),
            ("_TZE284_cjbofhxw", "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,
                    TuyaMCUCluster.cluster_id,
                    0xed00
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            }
        },
    }

Full file to just paste it for now ;)

Great! It's something worth! I can see the entities like yours now
Still no values read :(

It doesn't even seem to reset if I do the long press thing. It just doesn't blink as it should

This is what I got:
Screenshot 2024-06-19 alle 19 14 51

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Tuya Request/PR regarding a Tuya device
Projects
None yet
Development

No branches or pull requests

8 participants