"""Test zha sensor."""

import asyncio
from collections.abc import Awaitable, Callable
from datetime import UTC, datetime
from functools import partial
import math
from typing import Any
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from zhaquirks.danfoss import thermostat as danfoss_thermostat
from zigpy.device import Device as ZigpyDevice
from zigpy.profiles import zha
import zigpy.profiles.zha
from zigpy.quirks import CustomCluster, DeviceRegistry, get_device
from zigpy.quirks.v2 import CustomDeviceV2, QuirkBuilder, ReportingConfig
from zigpy.quirks.v2.homeassistant import EntityPlatform, EntityType, UnitOfMass
from zigpy.quirks.v2.homeassistant.sensor import (
    SensorDeviceClass as SensorDeviceClassV2,
)
import zigpy.types as t
from zigpy.zcl import Cluster
from zigpy.zcl.clusters import general, homeautomation, hvac, measurement, smartenergy
from zigpy.zcl.clusters.general import AnalogInput, PowerConfiguration
from zigpy.zcl.clusters.general_const import AnalogInputType, ApplicationType
from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster

from tests.common import (
    SIG_EP_INPUT,
    SIG_EP_OUTPUT,
    SIG_EP_PROFILE,
    SIG_EP_TYPE,
    create_mock_zigpy_device,
    get_entity,
    join_zigpy_device,
    send_attributes_report,
    zigpy_device_from_json,
)
from zha.application import Platform
from zha.application.const import ZCL_INIT_ATTRS, ZHA_CLUSTER_HANDLER_READS_PER_REQ
from zha.application.gateway import Gateway
from zha.application.platforms import PlatformEntity, sensor
from zha.application.platforms.sensor import (
    AnalogInputSensor,
    DanfossSoftwareErrorCode,
    Temperature,
)
from zha.application.platforms.sensor.const import SensorDeviceClass, SensorStateClass
from zha.application.platforms.sensor.helpers import resolution_to_decimal_precision
from zha.units import (
    PERCENTAGE,
    UnitOfElectricPotential,
    UnitOfEnergy,
    UnitOfPressure,
    UnitOfVolume,
)
from zha.zigbee.cluster_handlers import AttrReportConfig
from zha.zigbee.cluster_handlers.manufacturerspecific import OppleRemoteClusterHandler
from zha.zigbee.device import Device

EMAttrs = homeautomation.ElectricalMeasurement.AttributeDefs


def elec_measurement_zigpy_device_mock(
    zha_gateway: Gateway,
) -> ZigpyDevice:
    """Electric Measurement zigpy device."""

    zigpy_device = create_mock_zigpy_device(
        zha_gateway,
        {
            1: {
                SIG_EP_INPUT: [
                    general.Basic.cluster_id,
                    homeautomation.ElectricalMeasurement.cluster_id,
                ],
                SIG_EP_OUTPUT: [],
                SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.SIMPLE_SENSOR,
                SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID,
            }
        },
    )
    zigpy_device.node_desc.mac_capability_flags |= 0b_0000_0100
    zigpy_device.endpoints[1].electrical_measurement.PLUGGED_ATTR_READS = {
        "ac_current_divisor": 10,
        "ac_current_multiplier": 1,
        "ac_power_divisor": 10,
        "ac_power_multiplier": 1,
        "ac_voltage_divisor": 10,
        "ac_voltage_multiplier": 1,
        "measurement_type": 0x48,  # PHASE_A_MEASUREMENT | DC_MEASUREMENT
        "power_divisor": 10,
        "power_multiplier": 1,
        "dc_voltage_divisor": 10,
        "dc_voltage_multiplier": 1,
        "dc_current_divisor": 10,
        "dc_current_multiplier": 1,
        "dc_power_divisor": 10,
        "dc_power_multiplier": 1,
    }
    return zigpy_device


def metering_zigpy_device_mock(
    zha_gateway: Gateway,
) -> ZigpyDevice:
    """Metering zigpy device."""

    zigpy_device = create_mock_zigpy_device(
        zha_gateway,
        {
            1: {
                SIG_EP_INPUT: [
                    general.Basic.cluster_id,
                    smartenergy.Metering.cluster_id,
                ],
                SIG_EP_OUTPUT: [],
                SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.SIMPLE_SENSOR,
                SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID,
            }
        },
        model="TS011F",  # polling is enabled for this model
    )
    zigpy_device.node_desc.mac_capability_flags |= 0b_0000_0100
    zigpy_device.endpoints[1].smartenergy_metering.PLUGGED_ATTR_READS = {
        "unit_of_measure": 0,  # kWh
        "multiplier": 1,
        "divisor": 1000,
        "summation_formatting": 0b0_0100_011,  # read from plug
        "metering_device_type": 0,  # electric
    }
    return zigpy_device


async def async_test_humidity(
    zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity
) -> None:
    """Test humidity sensor."""
    await send_attributes_report(zha_gateway, cluster, {1: 1, 0: 1000, 2: 100})
    assert_state(entity, 10.0, "%")


async def async_test_flow(
    zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity
) -> None:
    """Test flow sensor."""
    await send_attributes_report(zha_gateway, cluster, {1: 1, 0: 40})
    assert_state(entity, 4.0, "m³/h")

    await send_attributes_report(zha_gateway, cluster, {1: 1, 0: 0xFFFF})
    assert_state(entity, None, "m³/h")


async def async_test_temperature(
    zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity
) -> None:
    """Test temperature sensor."""
    assert entity.extra_state_attribute_names is None
    await send_attributes_report(zha_gateway, cluster, {1: 1, 0: 2900, 2: 100})
    assert_state(entity, 29.0, "°C")


async def async_test_pressure(
    zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity
) -> None:
    """Test pressure sensor."""
    await send_attributes_report(zha_gateway, cluster, {1: 1, 0: 1000, 2: 10000})
    assert_state(entity, 1000, "hPa")

    await send_attributes_report(zha_gateway, cluster, {0: 1000, 20: -1, 16: 10000})
    assert_state(entity, 1000, "hPa")


async def async_test_illuminance(
    zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity
) -> None:
    """Test illuminance sensor."""
    await send_attributes_report(zha_gateway, cluster, {1: 1, 0: 10, 2: 20})
    assert_state(entity, 1.0, "lx")

    await send_attributes_report(zha_gateway, cluster, {0: 0xFFFF})
    assert_state(entity, None, "lx")


async def async_test_metering(
    zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity
) -> None:
    """Test Smart Energy metering sensor."""
    assert entity.extra_state_attribute_names == {
        "status",
        "device_type",
        "zcl_unit_of_measurement",
    }
    await send_attributes_report(
        zha_gateway, cluster, {1025: 1, 1024: 12345, 1026: 100}
    )
    assert_state(entity, 12345.0, None)
    assert entity.state["status"] == "NO_ALARMS"
    assert entity.state["device_type"] == "Electric Metering"

    await send_attributes_report(zha_gateway, cluster, {1024: 12346, "status": 64 + 8})
    assert_state(entity, 12346.0, None)
    assert entity.state["status"] in (
        "SERVICE_DISCONNECT|POWER_FAILURE",
        "POWER_FAILURE|SERVICE_DISCONNECT",
    )

    await send_attributes_report(
        zha_gateway, cluster, {"status": 64 + 8, "metering_device_type": 1}
    )
    assert entity.state["status"] in (
        "SERVICE_DISCONNECT|NOT_DEFINED",
        "NOT_DEFINED|SERVICE_DISCONNECT",
    )

    await send_attributes_report(
        zha_gateway, cluster, {"status": 64 + 8, "metering_device_type": 2}
    )
    assert entity.state["status"] in (
        "SERVICE_DISCONNECT|PIPE_EMPTY",
        "PIPE_EMPTY|SERVICE_DISCONNECT",
    )

    await send_attributes_report(
        zha_gateway, cluster, {"status": 64 + 8, "metering_device_type": 5}
    )
    assert entity.state["status"] in (
        "SERVICE_DISCONNECT|TEMPERATURE_SENSOR",
        "TEMPERATURE_SENSOR|SERVICE_DISCONNECT",
    )

    # Status for other meter types
    await send_attributes_report(
        zha_gateway, cluster, {"status": 32, "metering_device_type": 4}
    )
    assert entity.state["status"] in ("<bitmap8.32: 32>", "32")


async def async_test_smart_energy_summation_delivered(
    zha_gateway: Gateway, cluster, entity
):
    """Test SmartEnergy Summation delivered sensor."""
    assert entity.extra_state_attribute_names == {
        "status",
        "device_type",
        "zcl_unit_of_measurement",
    }
    await send_attributes_report(
        zha_gateway, cluster, {1025: 1, "current_summ_delivered": 12321, 1026: 100}
    )
    assert_state(entity, 12.321, UnitOfEnergy.KILO_WATT_HOUR)
    assert entity.state["status"] == "NO_ALARMS"
    assert entity.state["device_type"] == "Electric Metering"
    assert entity.info_object.device_class == SensorDeviceClass.ENERGY


async def async_test_smart_energy_summation_received(
    zha_gateway: Gateway, cluster, entity
):
    """Test SmartEnergy Summation received sensor."""

    await send_attributes_report(
        zha_gateway, cluster, {1025: 1, "current_summ_received": 12321, 1026: 100}
    )
    assert_state(entity, 12.321, UnitOfEnergy.KILO_WATT_HOUR)
    assert entity.state["status"] == "NO_ALARMS"
    assert entity.state["device_type"] == "Electric Metering"
    assert entity.info_object.device_class == SensorDeviceClass.ENERGY


async def async_test_smart_energy_summation(
    zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity
) -> None:
    """Test SmartEnergy Summation delivered sensro."""

    await send_attributes_report(
        zha_gateway, cluster, {1025: 1, "current_summ_delivered": 12321, 1026: 100}
    )
    assert_state(entity, 12.32, "m³")
    assert entity.state["status"] == "NO_ALARMS"
    assert entity.state["device_type"] == "Electric Metering"


async def async_test_electrical_measurement(
    zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity
) -> None:
    """Test electrical measurement sensor."""
    # update divisor cached value
    await send_attributes_report(zha_gateway, cluster, {"ac_power_divisor": 1})
    await send_attributes_report(
        zha_gateway, cluster, {0: 1, EMAttrs.active_power.id: 100}
    )
    assert_state(entity, 100, "W")

    await send_attributes_report(
        zha_gateway, cluster, {0: 1, EMAttrs.active_power.id: 99}
    )
    assert_state(entity, 99, "W")

    await send_attributes_report(zha_gateway, cluster, {"ac_power_divisor": 10})
    await send_attributes_report(
        zha_gateway, cluster, {0: 1, EMAttrs.active_power.id: 1000}
    )
    assert_state(entity, 100, "W")

    await send_attributes_report(
        zha_gateway, cluster, {0: 1, EMAttrs.active_power.id: 99}
    )
    assert_state(entity, 9.9, "W")

    await send_attributes_report(zha_gateway, cluster, {0: 1, 0x050D: 88})
    assert entity.state["active_power_max"] == 8.8


async def async_test_em_apparent_power(
    zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity
) -> None:
    """Test electrical measurement Apparent Power sensor."""
    # update divisor cached value
    await send_attributes_report(zha_gateway, cluster, {"ac_power_divisor": 1})
    await send_attributes_report(zha_gateway, cluster, {0: 1, 0x050F: 100})
    assert_state(entity, 100, "VA")

    await send_attributes_report(zha_gateway, cluster, {0: 1, 0x050F: 99})
    assert_state(entity, 99, "VA")

    await send_attributes_report(zha_gateway, cluster, {"ac_power_divisor": 10})
    await send_attributes_report(zha_gateway, cluster, {0: 1, 0x050F: 1000})
    assert_state(entity, 100, "VA")

    await send_attributes_report(zha_gateway, cluster, {0: 1, 0x050F: 99})
    assert_state(entity, 9.9, "VA")


async def async_test_em_power_factor(
    zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity
):
    """Test electrical measurement Power Factor sensor."""
    assert entity.extra_state_attribute_names == {"measurement_type"}

    # update divisor cached value
    await send_attributes_report(zha_gateway, cluster, {"ac_power_divisor": 1})
    await send_attributes_report(zha_gateway, cluster, {0: 1, 0x0510: 100, 10: 1000})
    assert_state(entity, 100, PERCENTAGE)

    await send_attributes_report(zha_gateway, cluster, {0: 1, 0x0510: 99, 10: 1000})
    assert_state(entity, 99, PERCENTAGE)

    await send_attributes_report(zha_gateway, cluster, {"ac_power_divisor": 10})
    await send_attributes_report(zha_gateway, cluster, {0: 1, 0x0510: 100, 10: 5000})
    assert_state(entity, 100, PERCENTAGE)

    await send_attributes_report(zha_gateway, cluster, {0: 1, 0x0510: 99, 10: 5000})
    assert_state(entity, 99, PERCENTAGE)


async def async_test_em_rms_current(
    current_attrid: int,
    current_max_attrid: int,
    current_max_attr_name: str,
    zha_gateway: Gateway,
    cluster: Cluster,
    entity: PlatformEntity,
) -> None:
    """Test electrical measurement RMS Current sensor."""

    await send_attributes_report(zha_gateway, cluster, {0: 1, current_attrid: 1234})
    assert_state(entity, 1.234, "A")

    await send_attributes_report(zha_gateway, cluster, {"ac_current_divisor": 10})
    await send_attributes_report(zha_gateway, cluster, {0: 1, current_attrid: 236})
    assert_state(entity, 23.6, "A")

    await send_attributes_report(zha_gateway, cluster, {0: 1, current_attrid: 1236})
    assert_state(entity, 123.6, "A")

    await send_attributes_report(zha_gateway, cluster, {0: 1, current_max_attrid: 88})
    assert entity.state[current_max_attr_name] == 8.8


async def async_test_em_rms_voltage(
    zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity
) -> None:
    """Test electrical measurement RMS Voltage sensor."""
    assert entity.extra_state_attribute_names == {"measurement_type", "rms_voltage_max"}

    await send_attributes_report(zha_gateway, cluster, {0: 1, 0x0505: 1234})
    assert_state(entity, 123.4, "V")

    await send_attributes_report(zha_gateway, cluster, {0: 1, 0x0505: 234})
    assert_state(entity, 23.4, "V")

    await send_attributes_report(zha_gateway, cluster, {"ac_voltage_divisor": 100})
    await send_attributes_report(zha_gateway, cluster, {0: 1, 0x0505: 2236})
    assert_state(entity, 22.36, "V")

    await send_attributes_report(zha_gateway, cluster, {0: 1, 0x0507: 888})
    assert entity.state["rms_voltage_max"] == 8.88


async def async_test_powerconfiguration(
    zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity
) -> None:
    """Test powerconfiguration/battery sensor."""
    assert entity.extra_state_attribute_names == {
        "battery_voltage",
        "battery_quantity",
        "battery_size",
    }
    await send_attributes_report(zha_gateway, cluster, {33: 98})
    assert_state(entity, 49, "%")
    assert entity.state["battery_voltage"] == 2.9
    assert entity.state["battery_quantity"] == 3
    assert entity.state["battery_size"] == "AAA"
    await send_attributes_report(zha_gateway, cluster, {32: 20})
    assert entity.state["battery_voltage"] == 2.0


async def async_test_powerconfiguration2(
    zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity
):
    """Test powerconfiguration/battery sensor."""
    await send_attributes_report(zha_gateway, cluster, {33: 255})
    assert_state(entity, None, "%")

    await send_attributes_report(zha_gateway, cluster, {33: 98})
    assert_state(entity, 49, "%")


async def async_test_device_temperature(
    zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity
) -> None:
    """Test temperature sensor."""
    await send_attributes_report(zha_gateway, cluster, {0: 2900})
    assert_state(entity, 29.0, "°C")


async def async_test_setpoint_change_source(
    zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity
):
    """Test the translation of numerical state into enum text."""
    await send_attributes_report(
        zha_gateway,
        cluster,
        {hvac.Thermostat.AttributeDefs.setpoint_change_source.id: 0x01},
    )
    assert entity.state["state"] == "Schedule"


async def async_test_pi_heating_demand(
    zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity
):
    """Test pi heating demand is correctly returned."""
    await send_attributes_report(
        zha_gateway, cluster, {hvac.Thermostat.AttributeDefs.pi_heating_demand.id: 1}
    )
    assert_state(entity, 1, "%")


async def async_test_change_source_timestamp(
    zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity
):
    """Test change source timestamp is correctly returned."""
    await send_attributes_report(
        zha_gateway,
        cluster,
        {hvac.Thermostat.AttributeDefs.setpoint_change_source_timestamp.id: 781355715},
    )
    assert entity.state["state"] == datetime(2024, 10, 4, 11, 15, 15, tzinfo=UTC)


async def async_test_em_dc_voltage(
    zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity
) -> None:
    """Test electrical measurement DC Voltage sensor."""
    assert entity.extra_state_attribute_names == {"measurement_type"}

    await send_attributes_report(zha_gateway, cluster, {0: 1, 0x0100: 1234})
    assert_state(entity, 123.4, "V")

    await send_attributes_report(zha_gateway, cluster, {0: 1, 0x0100: 234})
    assert_state(entity, 23.4, "V")

    await send_attributes_report(zha_gateway, cluster, {"dc_voltage_divisor": 100})
    await send_attributes_report(zha_gateway, cluster, {0: 1, 0x0100: 2236})
    assert_state(entity, 22.36, "V")


async def async_test_em_dc_current(
    zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity
) -> None:
    """Test electrical measurement DC Current sensor."""

    await send_attributes_report(zha_gateway, cluster, {0: 1, 0x0103: 1234})
    assert_state(entity, 1.234, "A")

    await send_attributes_report(zha_gateway, cluster, {"dc_current_divisor": 10})
    await send_attributes_report(zha_gateway, cluster, {0: 1, 0x0103: 236})
    assert_state(entity, 23.6, "A")

    await send_attributes_report(zha_gateway, cluster, {0: 1, 0x0103: 1236})
    assert_state(entity, 123.6, "A")


async def async_test_em_dc_power(
    zha_gateway: Gateway, cluster: Cluster, entity: PlatformEntity
) -> None:
    """Test electrical measurement DC Power sensor."""
    # update divisor cached value
    await send_attributes_report(zha_gateway, cluster, {"dc_power_divisor": 1})
    await send_attributes_report(zha_gateway, cluster, {0: 1, 0x0106: 100})
    assert_state(entity, 100, "W")

    await send_attributes_report(zha_gateway, cluster, {0: 1, 0x0106: 99})
    assert_state(entity, 99, "W")

    await send_attributes_report(zha_gateway, cluster, {"dc_power_divisor": 10})
    await send_attributes_report(zha_gateway, cluster, {0: 1, 0x0106: 1000})
    assert_state(entity, 100, "W")

    await send_attributes_report(zha_gateway, cluster, {0: 1, 0x0106: 99})
    assert_state(entity, 9.9, "W")


@pytest.mark.parametrize(
    "cluster_id, entity_type, test_func, read_plug, unsupported_attrs",
    (
        (
            measurement.RelativeHumidity.cluster_id,
            sensor.Humidity,
            async_test_humidity,
            None,
            None,
        ),
        (
            measurement.FlowMeasurement.cluster_id,
            sensor.Flow,
            async_test_flow,
            None,
            None,
        ),
        (
            measurement.TemperatureMeasurement.cluster_id,
            sensor.Temperature,
            async_test_temperature,
            None,
            None,
        ),
        (
            measurement.PressureMeasurement.cluster_id,
            sensor.Pressure,
            async_test_pressure,
            None,
            None,
        ),
        (
            measurement.IlluminanceMeasurement.cluster_id,
            sensor.Illuminance,
            async_test_illuminance,
            None,
            None,
        ),
        (
            smartenergy.Metering.cluster_id,
            sensor.SmartEnergyMetering,
            async_test_metering,
            {
                "demand_formatting": 0xF9,
                "divisor": 1,
                "metering_device_type": 0x00,
                "multiplier": 1,
                "status": 0x00,
            },
            {"current_summ_delivered"},
        ),
        (
            smartenergy.Metering.cluster_id,
            sensor.SmartEnergySummation,
            async_test_smart_energy_summation,
            {
                "demand_formatting": 0xF9,
                "divisor": 1000,
                "metering_device_type": 0x00,
                "multiplier": 1,
                "status": 0x00,
                "summation_formatting": 0b1_0111_010,
                "unit_of_measure": 0x01,
            },
            {"instantaneous_demand"},
        ),
        (
            smartenergy.Metering.cluster_id,
            sensor.SmartEnergySummationReceived,
            async_test_smart_energy_summation_received,
            {
                "demand_formatting": 0xF9,
                "divisor": 1000,
                "metering_device_type": 0x00,
                "multiplier": 1,
                "status": 0x00,
                "summation_formatting": 0b1_0111_010,
                "unit_of_measure": 0x00,
                "current_summ_received": 0,
            },
            {"instantaneous_demand", "current_summ_delivered"},
        ),
        (
            homeautomation.ElectricalMeasurement.cluster_id,
            sensor.PolledElectricalMeasurement,
            async_test_electrical_measurement,
            {"ac_power_divisor": 1000, "ac_power_multiplier": 1},
            {"apparent_power", "rms_current", "rms_voltage"},
        ),
        (
            homeautomation.ElectricalMeasurement.cluster_id,
            sensor.ElectricalMeasurementApparentPower,
            async_test_em_apparent_power,
            {"ac_power_divisor": 1000, "ac_power_multiplier": 1},
            {"active_power", "rms_current", "rms_voltage"},
        ),
        (
            homeautomation.ElectricalMeasurement.cluster_id,
            sensor.ElectricalMeasurementPowerFactor,
            async_test_em_power_factor,
            {"ac_power_divisor": 1000, "ac_power_multiplier": 1},
            {"active_power", "apparent_power", "rms_current", "rms_voltage"},
        ),
        (
            homeautomation.ElectricalMeasurement.cluster_id,
            sensor.ElectricalMeasurementRMSCurrent,
            partial(async_test_em_rms_current, 0x0508, 0x050A, "rms_current_max"),
            {"ac_current_divisor": 1000, "ac_current_multiplier": 1},
            {"active_power", "apparent_power", "rms_voltage"},
        ),
        (
            homeautomation.ElectricalMeasurement.cluster_id,
            sensor.ElectricalMeasurementRMSCurrentPhB,
            partial(async_test_em_rms_current, 0x0908, 0x090A, "rms_current_max_ph_b"),
            {
                "ac_current_divisor": 1000,
                "ac_current_multiplier": 1,
                "rms_current_ph_b": 0,
            },
            {"active_power", "apparent_power", "rms_voltage"},
        ),
        (
            homeautomation.ElectricalMeasurement.cluster_id,
            sensor.ElectricalMeasurementRMSCurrentPhC,
            partial(async_test_em_rms_current, 0x0A08, 0x0A0A, "rms_current_max_ph_c"),
            {
                "ac_current_divisor": 1000,
                "ac_current_multiplier": 1,
                "rms_current_ph_c": 0,
            },
            {"active_power", "apparent_power", "rms_voltage"},
        ),
        (
            homeautomation.ElectricalMeasurement.cluster_id,
            sensor.ElectricalMeasurementRMSVoltage,
            async_test_em_rms_voltage,
            {"ac_voltage_divisor": 10, "ac_voltage_multiplier": 1},
            {"active_power", "apparent_power", "rms_current"},
        ),
        (
            general.PowerConfiguration.cluster_id,
            sensor.Battery,
            async_test_powerconfiguration,
            {
                "battery_size": 4,  # AAA
                "battery_voltage": 29,
                "battery_quantity": 3,
            },
            None,
        ),
        (
            general.PowerConfiguration.cluster_id,
            sensor.Battery,
            async_test_powerconfiguration2,
            {
                "battery_size": 4,  # AAA
                "battery_voltage": 29,
                "battery_quantity": 3,
            },
            None,
        ),
        (
            general.DeviceTemperature.cluster_id,
            sensor.DeviceTemperature,
            async_test_device_temperature,
            None,
            None,
        ),
        (
            hvac.Thermostat.cluster_id,
            sensor.SetpointChangeSource,
            async_test_setpoint_change_source,
            None,
            None,
        ),
        (
            hvac.Thermostat.cluster_id,
            sensor.PiHeatingDemand,
            async_test_pi_heating_demand,
            None,
            None,
        ),
        (
            hvac.Thermostat.cluster_id,
            sensor.SetpointChangeSourceTimestamp,
            async_test_change_source_timestamp,
            None,
            None,
        ),
        (
            homeautomation.ElectricalMeasurement.cluster_id,
            sensor.ElectricalMeasurementDCVoltage,
            async_test_em_dc_voltage,
            {"dc_voltage_divisor": 10, "dc_voltage_multiplier": 1, "dc_voltage": 0},
            {"active_power", "apparent_power", "rms_current", "rms_voltage"},
        ),
        (
            homeautomation.ElectricalMeasurement.cluster_id,
            sensor.ElectricalMeasurementDCCurrent,
            async_test_em_dc_current,
            {"dc_current_divisor": 1000, "dc_current_multiplier": 1, "dc_current": 0},
            {"active_power", "apparent_power", "rms_current", "rms_voltage"},
        ),
        (
            homeautomation.ElectricalMeasurement.cluster_id,
            sensor.ElectricalMeasurementDCPower,
            async_test_em_dc_power,
            {"dc_power_divisor": 1000, "dc_power_multiplier": 1, "dc_power": 0},
            {"active_power", "apparent_power", "rms_current", "rms_voltage"},
        ),
    ),
)
async def test_sensor(
    zha_gateway: Gateway,
    cluster_id: int,
    entity_type: type[PlatformEntity],
    test_func: Callable[[Cluster, Cluster, PlatformEntity], Awaitable[None]],
    read_plug: dict | None,
    unsupported_attrs: set | None,
) -> None:
    """Test zha sensor platform."""

    zigpy_device = create_mock_zigpy_device(
        zha_gateway,
        {
            1: {
                SIG_EP_INPUT: [cluster_id, general.Basic.cluster_id],
                SIG_EP_OUTPUT: [],
                SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH,
                SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID,
            }
        },
    )
    cluster = zigpy_device.endpoints[1].in_clusters[cluster_id]
    if unsupported_attrs:
        for attr in unsupported_attrs:
            cluster.add_unsupported_attribute(attr)
    if cluster_id in (
        smartenergy.Metering.cluster_id,
        homeautomation.ElectricalMeasurement.cluster_id,
    ):
        # this one is mains powered
        zigpy_device.node_desc.mac_capability_flags |= 0b_0000_0100
    cluster.PLUGGED_ATTR_READS = read_plug or {}

    zha_device = await join_zigpy_device(zha_gateway, zigpy_device)
    entity = get_entity(
        zha_device, platform=Platform.SENSOR, exact_entity_type=entity_type
    )

    await zha_gateway.async_block_till_done()

    # test sensor associated logic
    await test_func(zha_gateway, cluster, entity)
    assert entity.fallback_name is None


async def test_analog_input_simple(zha_gateway: Gateway) -> None:
    """Test analog input sensors."""
    zigpy_dev = await zigpy_device_from_json(
        zha_gateway.application_controller,
        "tests/data/devices/isilentllc-masterbed-light-controller.json",
    )

    # Pretend this device has a proper description for the attribute
    # TODO: replace this unit test with one that uses a real device
    zigpy_dev.endpoints[2].analog_input.update_attribute(
        AnalogInput.AttributeDefs.description.id, "Some description"
    )

    zha_dev = await join_zigpy_device(zha_gateway, zigpy_dev)
    entity = get_entity(
        zha_dev, platform=Platform.SENSOR, exact_entity_type=AnalogInputSensor
    )

    assert entity.state["available"] is True
    assert entity.state["state"] == 2.1322579383850098
    assert entity.info_object.fallback_name == "Some description"
    assert entity.info_object.translation_key is None
    assert entity.info_object.unit == UnitOfElectricPotential.VOLT
    assert entity.info_object.device_class is None
    assert entity.info_object.suggested_display_precision is None


async def test_analog_input_ignored(zha_gateway: Gateway) -> None:
    """Test analog input sensors."""
    zigpy_dev = await zigpy_device_from_json(
        zha_gateway.application_controller,
        "tests/data/devices/isilentllc-masterbed-light-controller.json",
    )

    # Pretend this device has a proper description for the attribute
    # TODO: replace this unit test with one that uses a real device
    zigpy_dev.endpoints[2].analog_input.update_attribute(
        AnalogInput.AttributeDefs.description.id, "Some description"
    )

    # No units or application type are defined, however
    zigpy_dev.endpoints[2].analog_input.update_attribute(
        AnalogInput.AttributeDefs.engineering_units.id, 5
    )
    zigpy_dev.endpoints[2].analog_input.update_attribute(
        AnalogInput.AttributeDefs.engineering_units.id, None
    )
    zigpy_dev.endpoints[2].analog_input.add_unsupported_attribute(
        AnalogInput.AttributeDefs.engineering_units.id
    )
    # Also remove from PLUGGED_ATTR_READS so read_attributes doesn't restore the value
    zigpy_dev.endpoints[2].analog_input.PLUGGED_ATTR_READS.pop(
        AnalogInput.AttributeDefs.engineering_units.id, None
    )

    zha_dev = await join_zigpy_device(zha_gateway, zigpy_dev)

    # The entity is not created
    with pytest.raises(KeyError):
        get_entity(
            zha_dev, platform=Platform.SENSOR, exact_entity_type=AnalogInputSensor
        )


async def test_analog_input_complex(zha_gateway: Gateway) -> None:
    """Test analog input sensors."""
    zigpy_dev = await zigpy_device_from_json(
        zha_gateway.application_controller,
        "tests/data/devices/isilentllc-masterbed-light-controller.json",
    )

    # Pretend this device has a proper description for the attribute
    # TODO: replace this unit test with one that uses a real device
    zigpy_dev.endpoints[2].analog_input.update_attribute(
        AnalogInput.AttributeDefs.description.id, "Some description"
    )
    zigpy_dev.endpoints[2].analog_input.update_attribute(
        AnalogInput.AttributeDefs.application_type.id,
        int(
            ApplicationType(
                group=0, type=AnalogInputType.Relative_Humidity_Percent, index=0
            )
        ),
    )
    zigpy_dev.endpoints[2].analog_input.update_attribute(
        AnalogInput.AttributeDefs.resolution.id, 0.01
    )

    zha_dev = await join_zigpy_device(zha_gateway, zigpy_dev)
    entity = get_entity(
        zha_dev, platform=Platform.SENSOR, exact_entity_type=AnalogInputSensor
    )

    assert entity.state["available"] is True
    assert entity.state["state"] == 2.1322579383850098
    assert entity.info_object.fallback_name == "Some description"
    assert entity.info_object.translation_key is None
    assert entity.info_object.unit is PERCENTAGE  # overridden!
    assert entity.info_object.device_class is SensorDeviceClass.HUMIDITY  # overridden!
    assert entity.info_object.suggested_display_precision == 2


def assert_state(entity: PlatformEntity, state: Any, unit_of_measurement: str) -> None:
    """Check that the state is what is expected.

    This is used to ensure that the logic in each sensor class handled the
    attribute report it received correctly.
    """
    assert entity.state["state"] == state
    assert entity.info_object.unit == unit_of_measurement


async def test_electrical_measurement_init(
    zha_gateway: Gateway,
    caplog: pytest.LogCaptureFixture,
) -> None:
    """Test proper initialization of the electrical measurement cluster."""

    cluster_id = homeautomation.ElectricalMeasurement.cluster_id
    zigpy_device = create_mock_zigpy_device(
        zha_gateway,
        {
            1: {
                SIG_EP_INPUT: [cluster_id, general.Basic.cluster_id],
                SIG_EP_OUTPUT: [],
                SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH,
                SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID,
            }
        },
    )

    cluster = zigpy_device.endpoints[1].in_clusters[cluster_id]
    zha_device = await join_zigpy_device(zha_gateway, zigpy_device)

    entity = get_entity(
        zha_device,
        platform=Platform.SENSOR,
        exact_entity_type=sensor.PolledElectricalMeasurement,
    )

    await send_attributes_report(
        zha_gateway,
        cluster,
        {EMAttrs.active_power.id: 100},
    )
    assert entity.state["state"] == 100

    cluster_handler = list(zha_device._endpoints.values())[0].all_cluster_handlers[
        "1:0x0b04"
    ]
    assert cluster_handler.ac_power_divisor == 1
    assert cluster_handler.ac_power_multiplier == 1

    # update power divisor
    await send_attributes_report(
        zha_gateway,
        cluster,
        {EMAttrs.active_power.id: 20, EMAttrs.power_divisor.id: 5},
    )
    assert cluster_handler.ac_power_divisor == 5
    assert cluster_handler.ac_power_multiplier == 1
    assert entity.state["state"] == 4.0

    zha_device.on_network = False

    await asyncio.sleep(entity.__polling_interval + 1)
    await zha_gateway.async_block_till_done(wait_background_tasks=True)
    assert (
        "-1-2820: skipping polling for updated state, available: False, allow polled requests: True"
        in caplog.text
    )

    zha_device.on_network = True

    await send_attributes_report(
        zha_gateway,
        cluster,
        {EMAttrs.active_power.id: 30, EMAttrs.ac_power_divisor.id: 10},
    )
    assert cluster_handler.ac_power_divisor == 10
    assert cluster_handler.ac_power_multiplier == 1
    assert entity.state["state"] == 3.0

    # update power multiplier
    await send_attributes_report(
        zha_gateway,
        cluster,
        {EMAttrs.active_power.id: 20, EMAttrs.power_multiplier.id: 6},
    )
    assert cluster_handler.ac_power_divisor == 10
    assert cluster_handler.ac_power_multiplier == 6
    assert entity.state["state"] == 12.0

    await send_attributes_report(
        zha_gateway,
        cluster,
        {EMAttrs.active_power.id: 30, EMAttrs.ac_power_multiplier.id: 20},
    )
    assert cluster_handler.ac_power_divisor == 10
    assert cluster_handler.ac_power_multiplier == 20
    assert entity.state["state"] == 60.0

    entity._refresh = AsyncMock(wraps=entity._refresh)

    assert entity._refresh.await_count == 0

    entity.disable()

    assert entity.enabled is False

    await asyncio.sleep(entity.__polling_interval + 1)
    await zha_gateway.async_block_till_done(wait_background_tasks=True)

    assert entity._refresh.await_count == 0

    entity.enable()

    assert entity.enabled is True

    await asyncio.sleep(entity.__polling_interval + 1)
    await zha_gateway.async_block_till_done(wait_background_tasks=True)

    assert entity._refresh.await_count == 1


@pytest.mark.parametrize(
    (
        "cluster_id",
        "unsupported_attributes",
        "included_entity_types",
        "excluded_entity_types",
    ),
    (
        (
            homeautomation.ElectricalMeasurement.cluster_id,
            {
                "apparent_power",
                "rms_voltage",
                "rms_current",
            },
            {
                sensor.PolledElectricalMeasurement,
                sensor.ElectricalMeasurementFrequency,
                sensor.ElectricalMeasurementPowerFactor,
            },
            {
                sensor.ElectricalMeasurementApparentPower,
                sensor.ElectricalMeasurementRMSVoltage,
                sensor.ElectricalMeasurementRMSCurrent,
            },
        ),
        (
            homeautomation.ElectricalMeasurement.cluster_id,
            {
                "apparent_power",
                "rms_current",
                "ac_frequency",
                "power_factor",
            },
            {
                sensor.ElectricalMeasurementRMSVoltage,
                sensor.PolledElectricalMeasurement,
            },
            {
                sensor.ElectricalMeasurementApparentPower,
                sensor.ElectricalMeasurementRMSCurrent,
                sensor.ElectricalMeasurementFrequency,
                sensor.ElectricalMeasurementPowerFactor,
            },
        ),
        (
            homeautomation.ElectricalMeasurement.cluster_id,
            set(),
            {
                sensor.ElectricalMeasurementRMSVoltage,
                sensor.PolledElectricalMeasurement,
                sensor.ElectricalMeasurementApparentPower,
                sensor.ElectricalMeasurementRMSCurrent,
                sensor.ElectricalMeasurementFrequency,
                sensor.ElectricalMeasurementPowerFactor,
            },
            set(),
        ),
        (
            smartenergy.Metering.cluster_id,
            {
                "instantaneous_demand",
            },
            {
                sensor.SmartEnergySummation,
            },
            {
                sensor.SmartEnergyMetering,
            },
        ),
        (
            smartenergy.Metering.cluster_id,
            {
                "instantaneous_demand",
                "current_summ_delivered",
            },
            set(),
            {
                sensor.SmartEnergyMetering,
                sensor.SmartEnergySummation,
            },
        ),
        (
            smartenergy.Metering.cluster_id,
            set(),
            {
                sensor.SmartEnergyMetering,
                sensor.SmartEnergySummation,
            },
            set(),
        ),
    ),
)
async def test_unsupported_attributes_sensor(
    zha_gateway: Gateway,
    cluster_id: int,
    unsupported_attributes: set,
    included_entity_types: set,
    excluded_entity_types: set,
) -> None:
    """Test zha sensor platform."""

    zigpy_device = create_mock_zigpy_device(
        zha_gateway,
        {
            1: {
                SIG_EP_INPUT: [cluster_id, general.Basic.cluster_id],
                SIG_EP_OUTPUT: [],
                SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH,
                SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID,
            }
        },
    )
    cluster = zigpy_device.endpoints[1].in_clusters[cluster_id]
    if cluster_id == smartenergy.Metering.cluster_id:
        # this one is mains powered
        zigpy_device.node_desc.mac_capability_flags |= 0b_0000_0100
    for attr in unsupported_attributes:
        cluster.add_unsupported_attribute(attr)

    zha_device = await join_zigpy_device(zha_gateway, zigpy_device)

    present_entity_types = {
        type(e)
        for e in zha_device.platform_entities.values()
        if e.PLATFORM == Platform.SENSOR
        and ("lqi" not in e.unique_id and "rssi" not in e.unique_id)
    }

    assert included_entity_types <= present_entity_types
    assert not excluded_entity_types & present_entity_types


@pytest.mark.parametrize(
    "raw_uom, raw_value, expected_state, expected_uom",
    (
        (
            1,
            12320,
            1.23,
            UnitOfVolume.CUBIC_METERS,
        ),
        (
            1,
            1232000,
            123.2000,
            UnitOfVolume.CUBIC_METERS,
        ),
        (
            3,
            2340,
            0.23,
            UnitOfVolume.CUBIC_FEET,
        ),
        (
            3,
            2360,
            0.24,
            UnitOfVolume.CUBIC_FEET,
        ),
        (
            8,
            23660,
            2.37,
            UnitOfPressure.KPA,
        ),
        (
            0,
            9366,
            0.9366,
            UnitOfEnergy.KILO_WATT_HOUR,
        ),
        (
            0,
            999,
            0.0999,
            UnitOfEnergy.KILO_WATT_HOUR,
        ),
        (
            0,
            10091,
            1.0091,
            UnitOfEnergy.KILO_WATT_HOUR,
        ),
        (
            0,
            10099,
            1.0099,
            UnitOfEnergy.KILO_WATT_HOUR,
        ),
        (
            0,
            100999,
            10.0999,
            UnitOfEnergy.KILO_WATT_HOUR,
        ),
        (
            0,
            100023,
            10.0023,
            UnitOfEnergy.KILO_WATT_HOUR,
        ),
        (
            0,
            102456,
            10.2456,
            UnitOfEnergy.KILO_WATT_HOUR,
        ),
        (
            5,
            102456,
            10.25,
            "IMP gal",
        ),
        (
            7,
            50124,
            5.01,
            UnitOfVolume.LITERS,
        ),
    ),
)
async def test_se_summation_uom(
    zha_gateway: Gateway,
    raw_uom: int,
    raw_value: int,
    expected_state: str,
    expected_uom: str,
) -> None:
    """Test zha smart energy summation."""

    zigpy_device = create_mock_zigpy_device(
        zha_gateway,
        {
            1: {
                SIG_EP_INPUT: [
                    smartenergy.Metering.cluster_id,
                    general.Basic.cluster_id,
                ],
                SIG_EP_OUTPUT: [],
                SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.SIMPLE_SENSOR,
                SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID,
            }
        },
    )
    zigpy_device.node_desc.mac_capability_flags |= 0b_0000_0100

    cluster = zigpy_device.endpoints[1].in_clusters[smartenergy.Metering.cluster_id]
    for attr in ("instantaneous_demand",):
        cluster.add_unsupported_attribute(attr)
    cluster.PLUGGED_ATTR_READS = {
        "current_summ_delivered": raw_value,
        "demand_formatting": 0xF9,
        "divisor": 10000,
        "metering_device_type": 0x00,
        "multiplier": 1,
        "status": 0x00,
        "summation_formatting": 0b1_0111_010,
        "unit_of_measure": raw_uom,
    }
    zha_device = await join_zigpy_device(zha_gateway, zigpy_device)

    entity = get_entity(
        zha_device, platform=Platform.SENSOR, qualifier="summation_delivered"
    )

    assert_state(entity, expected_state, expected_uom)


@pytest.mark.parametrize(
    "raw_measurement_type, expected_type",
    (
        (1, "ACTIVE_MEASUREMENT"),
        (8, "PHASE_A_MEASUREMENT"),
        (9, "ACTIVE_MEASUREMENT, PHASE_A_MEASUREMENT"),
        (
            15,
            "ACTIVE_MEASUREMENT, REACTIVE_MEASUREMENT, APPARENT_MEASUREMENT, PHASE_A_MEASUREMENT",
        ),
    ),
)
async def test_elec_measurement_sensor_type(
    raw_measurement_type: int,
    expected_type: str,
    zha_gateway: Gateway,
) -> None:
    """Test zha electrical measurement sensor type."""

    zigpy_dev = elec_measurement_zigpy_device_mock(zha_gateway)
    zigpy_dev.endpoints[1].electrical_measurement.PLUGGED_ATTR_READS[
        "measurement_type"
    ] = raw_measurement_type

    zha_dev = await join_zigpy_device(zha_gateway, zigpy_dev)

    entity = get_entity(
        zha_dev,
        platform=Platform.SENSOR,
        entity_type=sensor.ElectricalMeasurementApparentPower,
    )
    assert entity.state["measurement_type"] == expected_type


async def test_elec_measurement_sensor_polling(zha_gateway: Gateway) -> None:
    """Test ZHA electrical measurement sensor polling."""

    zigpy_dev = elec_measurement_zigpy_device_mock(zha_gateway)
    zigpy_dev.endpoints[1].electrical_measurement.PLUGGED_ATTR_READS["active_power"] = (
        20
    )

    zha_dev = await join_zigpy_device(zha_gateway, zigpy_dev)

    # test that the sensor has an initial state of 2.0
    entity = get_entity(
        zha_dev,
        platform=Platform.SENSOR,
        exact_entity_type=sensor.PolledElectricalMeasurement,
    )
    assert entity.state["state"] == 2.0

    # update the value for the power reading
    zigpy_dev.endpoints[1].electrical_measurement.PLUGGED_ATTR_READS["active_power"] = (
        60
    )

    # ensure the state is still 2.0
    assert entity.state["state"] == 2.0

    # let the polling happen
    await asyncio.sleep(90)
    await zha_gateway.async_block_till_done(wait_background_tasks=True)

    # ensure the state has been updated to 6.0
    assert entity.state["state"] == 6.0


async def test_metering_sensor_polling(zha_gateway: Gateway) -> None:
    """Test ZHA metering sensor polling."""

    zigpy_dev = metering_zigpy_device_mock(zha_gateway)
    zigpy_dev.endpoints[1].smartenergy_metering.PLUGGED_ATTR_READS[
        "current_summ_delivered"
    ] = 2000

    zha_dev = await join_zigpy_device(zha_gateway, zigpy_dev)

    # test that the sensor has an initial state of 2.0
    entity = get_entity(
        zha_dev,
        platform=Platform.SENSOR,
        exact_entity_type=sensor.PolledSmartEnergySummation,
    )
    assert entity.state["state"] == 2.0

    # update the value for the power reading
    zigpy_dev.endpoints[1].smartenergy_metering.PLUGGED_ATTR_READS[
        "current_summ_delivered"
    ] = 6000

    # ensure the state is still 2.0
    assert entity.state["state"] == 2.0

    # let the polling happen
    await asyncio.sleep(90)
    await zha_gateway.async_block_till_done(wait_background_tasks=True)

    # ensure the state has been updated to 6.0
    assert entity.state["state"] == 6.0


@pytest.mark.parametrize(
    "supported_attributes",
    (
        set(),
        {
            "active_power",
            "active_power_max",
            "rms_current",
            "rms_current_max",
            "rms_voltage",
            "rms_voltage_max",
        },
        {
            "active_power",
        },
        {
            "active_power",
            "active_power_max",
        },
        {
            "rms_current",
            "rms_current_max",
        },
        {
            "rms_voltage",
            "rms_voltage_max",
        },
    ),
)
async def test_elec_measurement_skip_unsupported_attribute(
    zha_gateway: Gateway,
    supported_attributes: set[str],
) -> None:
    """Test zha electrical measurement skipping update of unsupported attributes."""

    elec_measurement_zigpy_dev = elec_measurement_zigpy_device_mock(zha_gateway)
    zha_dev = await join_zigpy_device(zha_gateway, elec_measurement_zigpy_dev)

    cluster = zha_dev.device.endpoints[1].electrical_measurement

    all_attrs = {
        "active_power",
        "active_power_ph_b",
        "active_power_ph_c",
        "active_power_max",
        "active_power_max_ph_b",
        "active_power_max_ph_c",
        "total_active_power",
        "apparent_power",
        "rms_current",
        "rms_current_ph_b",
        "rms_current_ph_c",
        "rms_current_max",
        "rms_current_max_ph_b",
        "rms_current_max_ph_c",
        "rms_voltage",
        "rms_voltage_ph_b",
        "rms_voltage_ph_c",
        "rms_voltage_max",
        "rms_voltage_max_ph_b",
        "rms_voltage_max_ph_c",
        "power_factor",
        "power_factor_ph_b",
        "power_factor_ph_c",
        "ac_frequency",
        "ac_frequency_max",
        "ac_voltage_divisor",
        "ac_current_divisor",
        "ac_power_divisor",
        "ac_voltage_multiplier",
        "ac_power_multiplier",
        "ac_current_multiplier",
        "power_divisor",
        "power_multiplier",
        "dc_voltage",
        "dc_voltage_divisor",
        "dc_voltage_multiplier",
        "dc_current",
        "dc_current_divisor",
        "dc_current_multiplier",
        "dc_power",
        "dc_power_divisor",
        "dc_power_multiplier",
    }
    for attr in all_attrs - supported_attributes:
        cluster.add_unsupported_attribute(attr)
    cluster.read_attributes.reset_mock()

    entity = get_entity(
        zha_dev,
        platform=Platform.SENSOR,
        exact_entity_type=sensor.PolledElectricalMeasurement,
    )
    await entity.async_update()
    await zha_dev.gateway.async_block_till_done()
    assert cluster.read_attributes.call_count == math.ceil(
        len(supported_attributes) / ZHA_CLUSTER_HANDLER_READS_PER_REQ
    )
    read_attrs = {
        a for call in cluster.read_attributes.call_args_list for a in call[0][0]
    }
    assert read_attrs == supported_attributes


class TimestampCluster(CustomCluster, ManufacturerSpecificCluster):
    """Timestamp Quirk V2 Cluster."""

    cluster_id = 0xEF00
    ep_attribute = "time_test_cluster"
    attributes = {
        0xEF65: ("start_time", t.uint32_t, True),
    }

    def __init__(self, *args, **kwargs) -> None:
        """Initialize."""
        super().__init__(*args, **kwargs)
        # populate cache to create config entity
        self._attr_cache.update({0xEF65: 10})


(
    QuirkBuilder("Fake_Timestamp_sensor", "Fake_Model_sensor")
    .replaces(TimestampCluster)
    .sensor(
        "start_time",
        TimestampCluster.cluster_id,
        device_class=SensorDeviceClassV2.TIMESTAMP,  # Use the zigpy enum
        attribute_converter=lambda x: datetime.fromtimestamp(x + 946684800, tz=UTC),
        translation_key="start_time",
        fallback_name="Start Time",
    )
    .add_to_registry()
)


async def zigpy_device_timestamp_sensor_v2_mock(
    zha_gateway: Gateway,  # pylint: disable=unused-argument
):
    """Timestamp Test device."""

    zigpy_device = create_mock_zigpy_device(
        zha_gateway,
        {
            1: {
                SIG_EP_INPUT: [
                    general.Basic.cluster_id,
                    TimestampCluster.cluster_id,
                ],
                SIG_EP_OUTPUT: [],
                SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH,
                SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID,
            }
        },
        manufacturer="Fake_Timestamp_sensor",
        model="Fake_Model_sensor",
    )

    zigpy_device = get_device(zigpy_device)

    zha_device = await join_zigpy_device(zha_gateway, zigpy_device)
    return zha_device, zigpy_device.endpoints[1].time_test_cluster


async def test_timestamp_sensor_v2(zha_gateway: Gateway) -> None:
    """Test quirks defined sensor."""

    zha_device, cluster = await zigpy_device_timestamp_sensor_v2_mock(zha_gateway)
    assert isinstance(zha_device.device, CustomDeviceV2)
    entity = get_entity(zha_device, platform=Platform.SENSOR, qualifier="start_time")

    await send_attributes_report(zha_gateway, cluster, {0xEF65: 781355715})
    assert entity.state["state"] == datetime(2024, 10, 4, 11, 15, 15, tzinfo=UTC)


class OppleCluster(CustomCluster, ManufacturerSpecificCluster):
    """Aqara manufacturer specific cluster."""

    cluster_id = 0xFCC0
    ep_attribute = "opple_cluster"
    attributes = {
        0x010C: ("last_feeding_size", t.uint16_t, True),
    }

    def __init__(self, *args, **kwargs) -> None:
        """Initialize."""
        super().__init__(*args, **kwargs)
        # populate cache to create config entity
        self._attr_cache.update({0x010C: 10})


(
    QuirkBuilder("Fake_Manufacturer_sensor", "Fake_Model_sensor")
    .replaces(OppleCluster)
    .sensor(
        "last_feeding_size",
        OppleCluster.cluster_id,
        divisor=1,
        multiplier=1,
        unit=UnitOfMass.GRAMS,
        translation_key="last_feeding_size",
        fallback_name="Last feeding size",
        reporting_config=ReportingConfig(
            min_interval=0, max_interval=60, reportable_change=1
        ),
    )
    .sensor(
        "power",
        OppleCluster.cluster_id,
        state_class=SensorStateClass.MEASUREMENT,
        device_class=SensorDeviceClass.POWER,
        fallback_name="Measurement",
    )
    .sensor(
        "energy",
        OppleCluster.cluster_id,
        state_class=SensorStateClass.TOTAL,
        device_class=SensorDeviceClass.ENERGY,
        fallback_name="Measurement",
    )
    .sensor(
        "energy_delivered",
        OppleCluster.cluster_id,
        state_class=SensorStateClass.TOTAL_INCREASING,
        device_class=SensorDeviceClass.ENERGY,
        fallback_name="Measurement",
    )
    .sensor(
        "energy_invalid_state_class",
        OppleCluster.cluster_id,
        state_class=SensorDeviceClass.ENERGY,
        device_class=SensorDeviceClass.ENERGY,
        fallback_name="Measurement",
    )
    .add_to_registry()
)


async def zigpy_device_aqara_sensor_v2_mock(
    zha_gateway: Gateway,  # pylint: disable=unused-argument
):
    """Device tracker zigpy Aqara motion sensor device."""

    zigpy_device = create_mock_zigpy_device(
        zha_gateway,
        {
            1: {
                SIG_EP_INPUT: [
                    general.Basic.cluster_id,
                    OppleCluster.cluster_id,
                ],
                SIG_EP_OUTPUT: [],
                SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.OCCUPANCY_SENSOR,
                SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID,
            }
        },
        manufacturer="Fake_Manufacturer_sensor",
        model="Fake_Model_sensor",
    )

    zigpy_device = get_device(zigpy_device)

    zha_device = await join_zigpy_device(zha_gateway, zigpy_device)
    return zha_device, zigpy_device.endpoints[1].opple_cluster


async def test_last_feeding_size_sensor_v2(zha_gateway: Gateway) -> None:
    """Test quirks defined sensor."""

    zha_device, cluster = await zigpy_device_aqara_sensor_v2_mock(zha_gateway)
    assert isinstance(zha_device.device, CustomDeviceV2)
    entity = get_entity(
        zha_device, platform=Platform.SENSOR, qualifier="last_feeding_size"
    )

    await send_attributes_report(zha_gateway, cluster, {0x010C: 1})
    assert_state(entity, 1.0, "g")

    await send_attributes_report(zha_gateway, cluster, {0x010C: 5})
    assert_state(entity, 5.0, "g")


async def test_state_class(
    zha_gateway: Gateway, caplog: pytest.LogCaptureFixture
) -> None:
    """Test quirks defined sensor."""

    zha_device, cluster = await zigpy_device_aqara_sensor_v2_mock(zha_gateway)
    assert isinstance(zha_device.device, CustomDeviceV2)
    power_entity = get_entity(
        zha_device,
        platform=Platform.SENSOR,
        qualifier_func=lambda e: e.info_object.unique_id.endswith("power"),
    )
    energy_entity = get_entity(
        zha_device,
        platform=Platform.SENSOR,
        qualifier_func=lambda e: e.info_object.unique_id.endswith("energy"),
    )
    energy_delivered_entity = get_entity(
        zha_device, platform=Platform.SENSOR, qualifier="energy_delivered"
    )
    energy_invalid_state_class = get_entity(
        zha_device, platform=Platform.SENSOR, qualifier="energy_invalid_state_class"
    )

    assert power_entity.state_class == SensorStateClass.MEASUREMENT
    assert energy_entity.state_class == SensorStateClass.TOTAL
    assert energy_delivered_entity.state_class == SensorStateClass.TOTAL_INCREASING
    assert energy_invalid_state_class.state_class is None
    assert "Quirks provided an invalid state class: energy" in caplog.text


async def test_cluster_handler_quirks_attribute_reporting(zha_gateway: Gateway) -> None:
    """Test quirks sensor setting up ZCL_INIT_ATTRS and REPORT_CONFIG correctly."""

    # Suppress normal endpoint probing, as this will claim the Opple cluster handler
    # already due to it being in the "CLUSTER_HANDLER_ONLY_CLUSTERS" registry.
    # We want to test the handler also gets claimed via quirks v2 reporting config.
    with patch("zha.application.discovery.discover_entities_for_endpoint"):
        zha_device, cluster = await zigpy_device_aqara_sensor_v2_mock(zha_gateway)

    assert isinstance(zha_device.device, CustomDeviceV2)

    # get cluster handler of OppleCluster
    opple_ch = zha_device.endpoints[1].all_cluster_handlers["1:0xfcc0"]
    assert isinstance(opple_ch, OppleRemoteClusterHandler)

    # make sure the cluster handler was claimed due to reporting config, so ZHA binds it
    assert opple_ch in zha_device.endpoints[1].claimed_cluster_handlers.values()

    # check that BIND is not set to False, as reporting is configured
    assert opple_ch.BIND is True

    # check ZCL_INIT_ATTRS contains sensor attributes that are not in REPORT_CONFIG
    assert opple_ch.ZCL_INIT_ATTRS == {
        "energy": True,
        "energy_delivered": True,
        "energy_invalid_state_class": True,
        "power": True,
    }
    # check that ZCL_INIT_ATTRS is an instance variable and not a class variable now
    assert opple_ch.ZCL_INIT_ATTRS is opple_ch.__dict__[ZCL_INIT_ATTRS]
    assert opple_ch.ZCL_INIT_ATTRS is not OppleRemoteClusterHandler.ZCL_INIT_ATTRS

    # double check we didn't modify the class variable
    assert OppleRemoteClusterHandler.ZCL_INIT_ATTRS == {}

    # check if REPORT_CONFIG is set correctly
    assert (
        (
            AttrReportConfig(
                attr="last_feeding_size",
                config=(0, 60, 1),
            ),
        )
    ) == opple_ch.REPORT_CONFIG

    # this cannot be wrong, as REPORT_CONFIG is an immutable tuple and not a list/dict,
    # but let's check it anyway in case the type changes in the future
    assert opple_ch.REPORT_CONFIG is not OppleRemoteClusterHandler.REPORT_CONFIG
    assert OppleRemoteClusterHandler.REPORT_CONFIG == ()


async def test_cluster_handler_quirks_attribute_reading(zha_gateway: Gateway) -> None:
    """Test quirks sensor setting up ZCL_INIT_ATTRS, claiming cluster handler."""

    registry = DeviceRegistry()
    (
        QuirkBuilder(
            "Fake_Manufacturer_sensor_2", "Fake_Model_sensor_2", registry=registry
        )
        .replaces(OppleCluster)
        .sensor(
            "last_feeding_size",
            OppleCluster.cluster_id,
            translation_key="last_feeding_size",
            fallback_name="Last feeding size",
        )
        .add_to_registry()
    )

    zigpy_device = create_mock_zigpy_device(
        zha_gateway,
        {
            1: {
                SIG_EP_INPUT: [
                    general.Basic.cluster_id,
                    OppleCluster.cluster_id,
                ],
                SIG_EP_OUTPUT: [],
                SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.OCCUPANCY_SENSOR,
                SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID,
            }
        },
        manufacturer="Fake_Manufacturer_sensor_2",
        model="Fake_Model_sensor_2",
    )
    zigpy_device = registry.get_device(zigpy_device)

    # Suppress normal endpoint probing, as this will claim the Opple cluster handler
    # already due to it being in the "CLUSTER_HANDLER_ONLY_CLUSTERS" registry.
    # We want to test the handler also gets claimed via quirks v2 attributes init.
    with patch("zha.application.discovery.discover_entities_for_endpoint"):
        zha_device = await join_zigpy_device(zha_gateway, zigpy_device)

    assert isinstance(zha_device.device, CustomDeviceV2)

    # get cluster handler of OppleCluster
    opple_ch = zha_device.endpoints[1].all_cluster_handlers["1:0xfcc0"]
    assert isinstance(opple_ch, OppleRemoteClusterHandler)

    # make sure the cluster handler was claimed due to attributes to be initialized
    # otherwise, ZHA won't configure the cluster handler, so attributes are not read
    assert opple_ch in zha_device.endpoints[1].claimed_cluster_handlers.values()

    # check that BIND is set to False, as no reporting is configured
    assert opple_ch.BIND is False

    # check ZCL_INIT_ATTRS contains sensor attributes that are not in REPORT_CONFIG
    assert opple_ch.ZCL_INIT_ATTRS == {
        "last_feeding_size": True,
    }
    # check that ZCL_INIT_ATTRS is an instance variable and not a class variable now
    assert opple_ch.ZCL_INIT_ATTRS is opple_ch.__dict__[ZCL_INIT_ATTRS]
    assert opple_ch.ZCL_INIT_ATTRS is not OppleRemoteClusterHandler.ZCL_INIT_ATTRS

    # double check we didn't modify the class variable
    assert OppleRemoteClusterHandler.ZCL_INIT_ATTRS == {}

    # check if REPORT_CONFIG is empty, both instance and class variable
    assert opple_ch.REPORT_CONFIG == ()
    assert OppleRemoteClusterHandler.REPORT_CONFIG == ()


async def test_device_counter_sensors(zha_gateway: Gateway) -> None:
    """Test coordinator counter sensor."""

    coordinator = zha_gateway.coordinator_zha_device
    assert coordinator.is_coordinator
    entity = get_entity(
        coordinator,
        platform=Platform.SENSOR,
        qualifier_func=lambda e: e.info_object.unique_id.endswith(
            "ezsp_counters_counter_1"
        ),
    )

    assert entity.state["state"] == 1

    # simulate counter increment on application
    coordinator.device.application.state.counters["ezsp_counters"][
        "counter_1"
    ].increment()

    await asyncio.sleep(zha_gateway.global_updater.__polling_interval + 2)
    await zha_gateway.async_block_till_done(wait_background_tasks=True)

    assert entity.state["state"] == 2

    # test disabling the entity disables it and removes it from the updater
    assert len(zha_gateway.global_updater._update_listeners) == 3
    assert entity.enabled is True

    entity.disable()

    assert entity.enabled is False
    assert len(zha_gateway.global_updater._update_listeners) == 2

    # test enabling the entity enables it and adds it to the updater
    entity.enable()

    assert entity.enabled is True
    assert len(zha_gateway.global_updater._update_listeners) == 3

    # make sure we don't get multiple listeners for the same entity in the updater
    entity.enable()

    assert len(zha_gateway.global_updater._update_listeners) == 3


async def test_device_unavailable_or_disabled_skips_entity_polling(
    zha_gateway: Gateway,
    caplog: pytest.LogCaptureFixture,
) -> None:
    """Test polling is skipped for unavailable devices."""

    elec_measurement_zigpy_dev = elec_measurement_zigpy_device_mock(zha_gateway)
    elec_measurement_zha_dev = await join_zigpy_device(
        zha_gateway, elec_measurement_zigpy_dev
    )
    assert not elec_measurement_zha_dev.is_coordinator
    assert not elec_measurement_zha_dev.is_active_coordinator
    entity = get_entity(
        elec_measurement_zha_dev,
        platform=Platform.SENSOR,
        exact_entity_type=sensor.RSSISensor,
    )

    assert entity.state["state"] is None

    elec_measurement_zha_dev.device.rssi = 60

    await asyncio.sleep(zha_gateway.global_updater.__polling_interval + 2)
    await zha_gateway.async_block_till_done(wait_background_tasks=True)

    assert entity.state["state"] == 60
    assert entity.enabled is True
    assert len(zha_gateway.global_updater._update_listeners) == 5

    # let's drop the normal update method from the updater
    entity.disable()

    assert entity.enabled is False
    assert len(zha_gateway.global_updater._update_listeners) == 4

    # wrap the update method so we can count how many times it was called
    entity.update = MagicMock(wraps=entity.update)
    await asyncio.sleep(zha_gateway.global_updater.__polling_interval + 2)
    await zha_gateway.async_block_till_done(wait_background_tasks=True)

    assert entity.update.call_count == 0

    # re-enable the entity and ensure it is back in the updater and that update is called
    entity.enable()
    assert len(zha_gateway.global_updater._update_listeners) == 5
    assert entity.enabled is True

    await asyncio.sleep(zha_gateway.global_updater.__polling_interval + 2)
    await zha_gateway.async_block_till_done(wait_background_tasks=True)

    assert entity.update.call_count == 1

    # knock it off the network and ensure the polling is skipped
    assert (
        "00:0d:6f:00:0a:90:69:e7-1-0-rssi: skipping polling for updated state, "
        "available: False, allow polled requests: True" not in caplog.text
    )

    elec_measurement_zha_dev.on_network = False
    await asyncio.sleep(zha_gateway.global_updater.__polling_interval + 2)
    await zha_gateway.async_block_till_done(wait_background_tasks=True)

    assert entity.update.call_count == 2

    assert (
        "00:0d:6f:00:0a:90:69:e7-1-0-rssi: skipping polling for updated state, "
        "available: False, allow polled requests: True" in caplog.text
    )


async def zigpy_device_danfoss_thermostat_mock(
    zha_gateway: Gateway,
) -> tuple[Device, zigpy.device.Device]:
    """Danfoss thermostat device."""

    zigpy_device = create_mock_zigpy_device(
        zha_gateway,
        {
            1: {
                SIG_EP_INPUT: [
                    general.Basic.cluster_id,
                    general.PowerConfiguration.cluster_id,
                    general.Identify.cluster_id,
                    general.Time.cluster_id,
                    general.PollControl.cluster_id,
                    hvac.Thermostat.cluster_id,
                    hvac.UserInterface.cluster_id,
                    homeautomation.Diagnostic.cluster_id,
                ],
                SIG_EP_OUTPUT: [general.Basic.cluster_id, general.Ota.cluster_id],
                SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.THERMOSTAT,
                SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID,
            }
        },
        manufacturer="Danfoss",
        model="eTRV0100",
        quirk=danfoss_thermostat.DanfossThermostat,
    )

    zha_device = await join_zigpy_device(zha_gateway, zigpy_device)
    return zha_device, zigpy_device


async def test_danfoss_thermostat_sw_error(zha_gateway: Gateway) -> None:
    """Test quirks defined thermostat."""

    zha_device, zigpy_device = await zigpy_device_danfoss_thermostat_mock(zha_gateway)

    entity = get_entity(
        zha_device,
        platform=Platform.SENSOR,
        exact_entity_type=DanfossSoftwareErrorCode,
        qualifier="sw_error_code",
    )
    assert entity is not None

    cluster = zigpy_device.endpoints[1].diagnostic

    await send_attributes_report(
        zha_gateway,
        cluster,
        {
            danfoss_thermostat.DanfossDiagnosticCluster.AttributeDefs.sw_error_code.id: 0x0001
        },
    )

    assert entity.state["state"] == "something"
    assert entity.extra_state_attribute_names
    assert "Top_pcb_sensor_error" in entity.extra_state_attribute_names
    assert entity.state["Top_pcb_sensor_error"]


async def test_quirks_sensor_attr_converter(zha_gateway: Gateway) -> None:
    """Test ZHA quirks v2 sensor with attribute_converter."""

    registry = DeviceRegistry()
    zigpy_dev = create_mock_zigpy_device(
        zha_gateway,
        {
            1: {
                SIG_EP_INPUT: [
                    general.Basic.cluster_id,
                    general.AnalogInput.cluster_id,
                ],
                SIG_EP_OUTPUT: [],
                SIG_EP_TYPE: zha.DeviceType.SIMPLE_SENSOR,
                SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID,
            }
        },
        manufacturer="manufacturer",
        model="model",
    )

    (
        QuirkBuilder(zigpy_dev.manufacturer, zigpy_dev.model, registry=registry)
        .sensor(
            AnalogInput.AttributeDefs.present_value.name,
            AnalogInput.cluster_id,
            translation_key="quirks_sensor",
            fallback_name="Quirks sensor",
            attribute_converter=lambda x: x + 100,
        )
        .add_to_registry()
    )

    zigpy_device_ = registry.get_device(zigpy_dev)

    assert isinstance(zigpy_device_, CustomDeviceV2)
    cluster = zigpy_device_.endpoints[1].analog_input

    zha_device = await join_zigpy_device(zha_gateway, zigpy_device_)
    entity = get_entity(zha_device, platform=Platform.SENSOR, qualifier="present_value")

    # send updated value, check if the value is converted
    await send_attributes_report(zha_gateway, cluster, {"present_value": 100})
    assert entity.state["state"] == 200.0

    await send_attributes_report(zha_gateway, cluster, {"present_value": 0})
    assert entity.state["state"] == 100.0


async def test_ignore_non_value(zha_gateway: Gateway) -> None:
    """Test sensor updates ignoring ZCL datatype non-values."""

    zigpy_dev = await zigpy_device_from_json(
        zha_gateway.application_controller,
        "tests/data/devices/third-reality-inc-3rsm0147z.json",
    )

    zha_device = await join_zigpy_device(zha_gateway, zigpy_dev)
    cluster = zha_device.device.endpoints[1].temperature
    entity = get_entity(zha_device, platform=Platform.SENSOR, entity_type=Temperature)

    assert entity.state["state"] == 22.3

    # Normal attribute report
    await send_attributes_report(
        zha_gateway,
        cluster,
        {measurement.TemperatureMeasurement.AttributeDefs.measured_value.id: 3000},
    )
    assert entity.state["state"] == 30.0

    # Invalid attribute value, ignored
    await send_attributes_report(
        zha_gateway,
        cluster,
        {measurement.TemperatureMeasurement.AttributeDefs.measured_value.id: -0x8000},
    )
    assert entity.state["state"] is None


@pytest.mark.parametrize(
    ("resolution", "precision"),
    [
        (0.0009999999310821295, 3),
        (0.001, 3),
        (0.001000001, 3),
        (0.01, 2),
        (0.05, 2),
        (0.1, 1),
        (1.0, 0),
        (2.0, 0),
        (0.9, 1),
        (0.7, 1),
        (0.2, 1),
        (0.25, 2),
        (0.33, 2),
        (0.44, 2),
        (0.55, 2),
        (0.66, 2),
        (0.77, 2),
        (0.88, 2),
        (1 / 3, 7),
        (0.125, 3),
        (0.5, 1),
        (1.5, 1),
        (10.5, 1),
        (0.000000000000001, 15),
        (0.0000000001, 10),
        (0.000000001, 9),
        (0.00000001, 8),
        (0.0000001, 7),
        (0.000001, 6),
    ],
)
def test_sensor_precision(resolution: float, precision: int) -> None:
    """Test converting ZCL `resolution` into precision."""

    # Force the values to be float32, not float64
    resolution, _ = t.Single.deserialize(t.Single(resolution).serialize())
    assert resolution_to_decimal_precision(resolution) == precision


async def test_enum_sensor(zha_gateway: Gateway) -> None:
    """Test enum sensor creation and handling of missing values."""
    registry = DeviceRegistry()
    zigpy_dev = await zigpy_device_from_json(
        zha_gateway.application_controller,
        "tests/data/devices/third-reality-inc-3rsm0147z.json",
    )

    zigpy_dev.endpoints[1].power.update_attribute(
        PowerConfiguration.AttributeDefs.battery_size.id,
        PowerConfiguration.BatterySize.AAA,
    )

    (
        QuirkBuilder(zigpy_dev.manufacturer, zigpy_dev.model, registry=registry)
        .enum(
            entity_platform=EntityPlatform.SENSOR,
            entity_type=EntityType.DIAGNOSTIC,
            attribute_name=PowerConfiguration.AttributeDefs.battery_size.name,
            enum_class=PowerConfiguration.BatterySize,
            cluster_id=PowerConfiguration.cluster_id,
            endpoint_id=1,
            translation_key="battery_size",
            fallback_name="Battery size",
        )
        .add_to_registry()
    )

    zigpy_dev = registry.get_device(zigpy_dev)

    zha_device = await join_zigpy_device(zha_gateway, zigpy_dev)
    entity = get_entity(zha_device, platform=Platform.SENSOR, qualifier="battery_size")

    assert entity.state["state"] == "AAA"

    zigpy_dev.endpoints[1].power.update_attribute(
        PowerConfiguration.AttributeDefs.battery_size.id,
        0xAB,  # unknown
    )

    assert entity.state["state"] == "undefined_0xab"  # TODO: should this be `None`?
