# pylint: disable=unused-argument
""" Implements the VersatileThermostat sensors component """
import logging
from vtherm_api.log_collector import get_vtherm_logger
import math
from collections.abc import Callable

from homeassistant.core import HomeAssistant, callback, Event, State

from homeassistant.const import (
    UnitOfTime,
    UnitOfPower,
    UnitOfEnergy,
    PERCENTAGE,
)

from homeassistant.components.sensor import (
    SensorEntity,
    SensorDeviceClass,
    SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry

from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.event import (
    async_track_state_change_event,
)

from homeassistant.components.climate import (
    ClimateEntity,
    DOMAIN as CLIMATE_DOMAIN,
)


from .base_thermostat import BaseThermostat
from .vtherm_central_api import VersatileThermostatAPI
from .base_entity import VersatileThermostatBaseEntity
from .commons import cleanup_orphan_entity
from .const import (
    DOMAIN,
    DEVICE_MANUFACTURER,
    CONF_NAME,
    CONF_DEVICE_POWER,
    CONF_PROP_FUNCTION,
    PROPORTIONAL_FUNCTION_TPI,
    CONF_THERMOSTAT_SWITCH,
    CONF_THERMOSTAT_VALVE,
    CONF_THERMOSTAT_CLIMATE,
    CONF_THERMOSTAT_TYPE,
    CONF_THERMOSTAT_CENTRAL_CONFIG,
    CONF_USE_CENTRAL_BOILER_FEATURE,
    CONF_AUTO_REGULATION_VALVE,
    CONF_AUTO_REGULATION_MODE,
    CONF_TPI_COEF_INT,
    CONF_TPI_COEF_EXT,
    CONF_AUTO_TPI_MODE,
    overrides,
)

THRESHOLD_WATT_KILO = 100

_LOGGER = get_vtherm_logger(__name__)


async def async_setup_entry(
    hass: HomeAssistant,
    entry: ConfigEntry,
    async_add_entities: AddEntitiesCallback,
) -> None:
    """Set up the VersatileThermostat sensors with config flow."""
    unique_id = entry.entry_id
    name = entry.data.get(CONF_NAME)
    _LOGGER.debug("%s - Calling async_setup_entry entry=%s, data=%s", name, entry.entry_id, entry.data)

    vt_type = entry.data.get(CONF_THERMOSTAT_TYPE)
    have_valve_regulation = (
        entry.data.get(CONF_AUTO_REGULATION_MODE) == CONF_AUTO_REGULATION_VALVE
    )

    entities = None

    if vt_type == CONF_THERMOSTAT_CENTRAL_CONFIG:
        if entry.data.get(CONF_USE_CENTRAL_BOILER_FEATURE):
            entities = [
                NbActiveDeviceForBoilerSensor(hass, unique_id, name, entry.data),
                TotalPowerActiveDeviceForBoilerSensor(hass, unique_id, name, entry.data),
            ]
    else:
        entities = [
            LastTemperatureSensor(hass, unique_id, name, entry.data),
            LastExtTemperatureSensor(hass, unique_id, name, entry.data),
            TemperatureSlopeSensor(hass, unique_id, name, entry.data),
            EMATemperatureSensor(hass, unique_id, name, entry.data),
        ]
        if entry.data.get(CONF_DEVICE_POWER):
            entities.append(EnergySensor(hass, unique_id, name, entry.data))
            if have_valve_regulation or entry.data.get(CONF_THERMOSTAT_TYPE) in [
                CONF_THERMOSTAT_SWITCH,
                CONF_THERMOSTAT_VALVE,
            ]:
                entities.append(MeanPowerSensor(hass, unique_id, name, entry.data))

        is_tpi_capable = entry.data.get(CONF_THERMOSTAT_TYPE) in [
            CONF_THERMOSTAT_SWITCH,
            CONF_THERMOSTAT_VALVE,
        ] or (
            entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE
            and have_valve_regulation
        )
        has_proportional_algorithm = (
            is_tpi_capable and entry.data.get(CONF_PROP_FUNCTION) is not None
        )

        if has_proportional_algorithm:
            entities.append(OnPercentSensor(hass, unique_id, name, entry.data))
            entities.append(OnTimeSensor(hass, unique_id, name, entry.data))
            entities.append(OffTimeSensor(hass, unique_id, name, entry.data))

        if (
            entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_VALVE
            or have_valve_regulation
        ):
            entities.append(ValveOpenPercentSensor(hass, unique_id, name, entry.data))

        if (
            entry.data.get(CONF_THERMOSTAT_TYPE) == CONF_THERMOSTAT_CLIMATE
            and not have_valve_regulation
        ):
            entities.append(
                RegulatedTemperatureSensor(hass, unique_id, name, entry.data)
            )

        # Add Auto TPI Sensor only if:
        # 1. Thermostat is TPI-capable
        # 2. TPI algorithm is selected
        # 3. Auto TPI mode is enabled in configuration
        should_have_auto_tpi = (
            is_tpi_capable
            and entry.data.get(CONF_PROP_FUNCTION) == PROPORTIONAL_FUNCTION_TPI
            and entry.data.get(CONF_AUTO_TPI_MODE, False)
        )

        if should_have_auto_tpi:
            entities.append(AutoTpiSensor(hass, unique_id, name, entry.data))
        else:
            # Cleanup: Remove orphan Auto TPI sensor from registry if it exists
            await cleanup_orphan_entity(hass, entry, "sensor", name, "auto_tpi_learning")

    if entities:
        async_add_entities(entities, True)


class EnergySensor(VersatileThermostatBaseEntity, SensorEntity):
    """Representation of a Energy sensor which exposes the energy"""

    def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
        """Initialize the energy sensor"""
        super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
        self._attr_name = "Energy"
        self._attr_unique_id = f"{self._device_name}_energy"

    @callback
    async def async_my_climate_changed(self, event: Event = None):
        """Called when my climate have change"""
        # _LOGGER.debug("%s - climate state change", self._attr_unique_id)

        energy = self.my_climate.total_energy
        if energy is None:
            return

        if math.isnan(energy) or math.isinf(energy):
            raise ValueError(f"Sensor has illegal state {self.my_climate.total_energy}")

        old_state = self._attr_native_value
        self._attr_native_value = round(energy, self.suggested_display_precision)
        if old_state != self._attr_native_value:
            self.async_write_ha_state()
        return

    @property
    def icon(self) -> str | None:
        return "mdi:lightning-bolt"

    @property
    def device_class(self) -> SensorDeviceClass | None:
        if not self.my_climate:
            return None
        return SensorDeviceClass.ENERGY

    @property
    def state_class(self) -> SensorStateClass | None:
        if not self.my_climate:
            return None
        return SensorStateClass.TOTAL_INCREASING

    @property
    def native_unit_of_measurement(self) -> str | None:
        if not self.my_climate:
            return None

        if self.my_climate.power_manager.device_power > THRESHOLD_WATT_KILO:
            return UnitOfEnergy.WATT_HOUR
        else:
            return UnitOfEnergy.KILO_WATT_HOUR

    @property
    def suggested_display_precision(self) -> int | None:
        """Return the suggested number of decimal digits for display."""
        return 3


class MeanPowerSensor(VersatileThermostatBaseEntity, SensorEntity):
    """Representation of a power sensor which exposes the mean power in a cycle"""

    def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
        """Initialize the energy sensor"""
        super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
        self._attr_name = "Mean power cycle"
        self._attr_unique_id = f"{self._device_name}_mean_power_cycle"

    @callback
    async def async_my_climate_changed(self, event: Event = None):
        """Called when my climate have change"""
        # _LOGGER.debug("%s - climate state change", self._attr_unique_id)

        mean_cycle_power = self.my_climate.power_manager.mean_cycle_power
        if mean_cycle_power is None:
            return

        if math.isnan(float(mean_cycle_power)) or math.isinf(float(mean_cycle_power)):
            raise ValueError(f"Sensor has illegal state {mean_cycle_power}")

        mean_cycle_power = float(mean_cycle_power)

        old_state = self._attr_native_value
        self._attr_native_value = round(
            mean_cycle_power,
            self.suggested_display_precision,
        )
        if old_state != self._attr_native_value:
            self.async_write_ha_state()
        return

    @property
    def icon(self) -> str | None:
        return "mdi:flash-outline"

    @property
    def device_class(self) -> SensorDeviceClass | None:
        return SensorDeviceClass.POWER

    @property
    def state_class(self) -> SensorStateClass | None:
        return SensorStateClass.MEASUREMENT

    @property
    def native_unit_of_measurement(self) -> str | None:
        if not self.my_climate:
            return None

        if self.my_climate.power_manager.device_power > THRESHOLD_WATT_KILO:
            return UnitOfPower.WATT
        else:
            return UnitOfPower.KILO_WATT

    @property
    def suggested_display_precision(self) -> int | None:
        """Return the suggested number of decimal digits for display."""
        return 3


class OnPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
    """Representation of a on percent sensor which exposes the on_percent in a cycle"""

    def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
        """Initialize the energy sensor"""
        super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
        self._attr_name = "Power percent"
        self._attr_unique_id = f"{self._device_name}_power_percent"

    @callback
    async def async_my_climate_changed(self, event: Event = None):
        """Called when my climate have change"""
        # _LOGGER.debug("%s - climate state change", self._attr_unique_id)

        raw_on_percent = (
            self.my_climate.proportional_algorithm.on_percent
            if self.my_climate
            and self.my_climate.has_prop
            and self.my_climate.proportional_algorithm is not None
            else None
        )
        if raw_on_percent is None:
            return
        on_percent = float(raw_on_percent)

        if math.isnan(on_percent) or math.isinf(on_percent):
            raise ValueError(f"Sensor has illegal state {on_percent}")

        old_state = self._attr_native_value
        self._attr_native_value = round(
            on_percent * 100.0, self.suggested_display_precision
        )
        if old_state != self._attr_native_value:
            self.async_write_ha_state()
        return

    @property
    def icon(self) -> str | None:
        return "mdi:meter-electric-outline"

    @property
    def device_class(self) -> SensorDeviceClass | None:
        return SensorDeviceClass.POWER_FACTOR

    @property
    def state_class(self) -> SensorStateClass | None:
        return SensorStateClass.MEASUREMENT

    @property
    def native_unit_of_measurement(self) -> str | None:
        return PERCENTAGE

    @property
    def suggested_display_precision(self) -> int | None:
        """Return the suggested number of decimal digits for display."""
        return 1


class AutoTpiSensor(VersatileThermostatBaseEntity, SensorEntity):
    """Representation of the Auto TPI Learning state"""

    def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
        """Initialize the Auto TPI sensor"""
        super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
        self._attr_name = "Auto TPI Learning State"
        self._attr_unique_id = f"{self._device_name}_auto_tpi_learning"
        self._attr_icon = "mdi:brain"

    @callback
    async def async_my_climate_changed(self, event: Event = None):
        """Called when my climate have change"""

        # Verify has_prop and proportional_algorithm
        # proportional_algorithm can be None during initialization even if has_prop is True
        if not self.my_climate or not self.my_climate.has_prop:
            self._attr_native_value = "disabled"
            self.async_write_ha_state()
            return

        if not hasattr(self.my_climate, "auto_tpi_manager") or not self.my_climate.auto_tpi_manager:
            self._attr_native_value = "disabled"
            self.async_write_ha_state()
            return

        manager = self.my_climate.auto_tpi_manager

        # Determine state
        if manager.learning_active:
            if manager.is_in_bootstrap:
                self._attr_native_value = "Bootstrap"
            else:
                self._attr_native_value = "Active"
        else:
            self._attr_native_value = "Off" # Or "Completed" / "Idle" depending on context, but "Off" implies not learning.

        # Update attributes
        # Update attributes
        self._attr_extra_state_attributes = {
            "coeff_int_cycles": manager.int_cycles,
            "coeff_ext_cycles": manager.ext_cycles,
            "heating_cycles_count": manager.heating_cycles_count,  # Total heating cycles
            "thermal_time_constant_hours": manager.time_constant,  # Building's thermal time constant τ
            "model_confidence": manager.confidence,  # Model confidence (R² or percentage)
            "last_learning_status": manager.state.last_learning_status,  # Reason for last learning outcome
            "learning_start_dt": manager.state.learning_start_date,
            "allow_kint_boost_on_stagnation": manager.state.allow_kint_boost,
            "allow_kext_compensation_on_overshoot": manager.state.allow_kext_overshoot,
            "bootstrap_failure_count": manager.state.bootstrap_failure_count,
        }

        # Add mode-specific attributes
        if manager._current_hvac_mode == "cool":
            self._attr_extra_state_attributes.update({
                "max_capacity_cool": manager.state.max_capacity_cool,
            })
        else:
            self._attr_extra_state_attributes.update({
                "max_capacity_heat": manager.state.max_capacity_heat,
                "capacity_heat_status": "learning" if manager.is_in_bootstrap else "learned",
                "capacity_heat_value": manager.state.max_capacity_heat,
                "capacity_heat_count": manager.state.capacity_heat_learn_count,
            })

        # Add calculated TPI coefficients
        calculated = manager.get_calculated_params()
        self._attr_extra_state_attributes.update({
            "calculated_coef_int": calculated.get(CONF_TPI_COEF_INT),
            "calculated_coef_ext": calculated.get(CONF_TPI_COEF_EXT),
        })

        self.async_write_ha_state()


class ValveOpenPercentSensor(VersatileThermostatBaseEntity, SensorEntity):
    """Representation of a on percent sensor which exposes the on_percent in a cycle"""

    def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
        """Initialize the energy sensor"""
        super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
        self._attr_name = "Valve open percent"
        self._attr_unique_id = f"{self._device_name}_valve_open_percent"

    @callback
    async def async_my_climate_changed(self, event: Event = None):
        """Called when my climate have change"""
        # _LOGGER.debug("%s - climate state change", self._attr_unique_id)

        if not self.my_climate or not hasattr(self.my_climate, "valve_open_percent"):
            _LOGGER.warning("%s - my_climate not found or no valve_open_percent property found. This could be normal at startup. Ignore the underlying device change.", self)
            return

        old_state = self._attr_native_value
        self._attr_native_value = self.my_climate.valve_open_percent
        if old_state != self._attr_native_value:
            self.async_write_ha_state()
        return

    @property
    def icon(self) -> str | None:
        return "mdi:pipe-valve"

    @property
    def device_class(self) -> SensorDeviceClass | None:
        return SensorDeviceClass.POWER_FACTOR

    @property
    def state_class(self) -> SensorStateClass | None:
        return SensorStateClass.MEASUREMENT

    @property
    def native_unit_of_measurement(self) -> str | None:
        return PERCENTAGE

    @property
    def suggested_display_precision(self) -> int | None:
        """Return the suggested number of decimal digits for display."""
        return 0


class OnTimeSensor(VersatileThermostatBaseEntity, SensorEntity):
    """Representation of a on time sensor which exposes the on_time_sec in a cycle"""

    def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
        """Initialize the energy sensor"""
        super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
        self._attr_name = "On time"
        self._attr_unique_id = f"{self._device_name}_on_time"

    @callback
    async def async_my_climate_changed(self, event: Event = None):
        """Called when my climate have change"""
        # _LOGGER.debug("%s - climate state change", self._attr_unique_id)

        on_time = (
            float(self.my_climate.on_time_sec)
            if self.my_climate and hasattr(self.my_climate, "on_time_sec")
            else None
        )

        if on_time is None:
            return

        if math.isnan(on_time) or math.isinf(on_time):
            raise ValueError(f"Sensor has illegal state {on_time}")

        old_state = self._attr_native_value
        self._attr_native_value = round(on_time)
        if old_state != self._attr_native_value:
            self.async_write_ha_state()
        return

    @property
    def icon(self) -> str | None:
        return "mdi:timer-play"

    @property
    def device_class(self) -> SensorDeviceClass | None:
        return SensorDeviceClass.DURATION

    @property
    def state_class(self) -> SensorStateClass | None:
        return SensorStateClass.MEASUREMENT

    @property
    def native_unit_of_measurement(self) -> str | None:
        return UnitOfTime.SECONDS


class OffTimeSensor(VersatileThermostatBaseEntity, SensorEntity):
    """Representation of a on time sensor which exposes the off_time_sec in a cycle"""

    def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
        """Initialize the energy sensor"""
        super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
        self._attr_name = "Off time"
        self._attr_unique_id = f"{self._device_name}_off_time"

    @callback
    async def async_my_climate_changed(self, event: Event = None):
        """Called when my climate have change"""
        # _LOGGER.debug("%s - climate state change", self._attr_unique_id)

        off_time = (
            float(self.my_climate.off_time_sec)
            if self.my_climate and hasattr(self.my_climate, "off_time_sec")
            else None
        )
        if off_time is None:
            return

        if math.isnan(off_time) or math.isinf(off_time):
            raise ValueError(f"Sensor has illegal state {off_time}")

        old_state = self._attr_native_value
        self._attr_native_value = round(off_time)
        if old_state != self._attr_native_value:
            self.async_write_ha_state()
        return

    @property
    def icon(self) -> str | None:
        return "mdi:timer-off-outline"

    @property
    def device_class(self) -> SensorDeviceClass | None:
        return SensorDeviceClass.DURATION

    @property
    def state_class(self) -> SensorStateClass | None:
        return SensorStateClass.MEASUREMENT

    @property
    def native_unit_of_measurement(self) -> str | None:
        return UnitOfTime.SECONDS


class LastTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
    """Representation of a last temperature datetime sensor"""

    def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
        """Initialize the last temperature datetime sensor"""
        super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
        self._attr_name = "Last temperature date"
        self._attr_entity_registry_enabled_default = False
        self._attr_unique_id = f"{self._device_name}_last_temp_datetime"

    @callback
    async def async_my_climate_changed(self, event: Event = None):
        """Called when my climate have change"""
        # _LOGGER.debug("%s - climate state change", self._attr_unique_id)

        old_state = self._attr_native_value
        self._attr_native_value = self.my_climate.last_temperature_measure
        if old_state != self._attr_native_value:
            self.async_write_ha_state()
        return

    @property
    def icon(self) -> str | None:
        return "mdi:home-clock"

    @property
    def device_class(self) -> SensorDeviceClass | None:
        return SensorDeviceClass.TIMESTAMP


class LastExtTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
    """Representation of a last external temperature datetime sensor"""

    def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
        """Initialize the last temperature datetime sensor"""
        super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
        self._attr_name = "Last external temperature date"
        self._attr_entity_registry_enabled_default = False
        self._attr_unique_id = f"{self._device_name}_last_ext_temp_datetime"

    @callback
    async def async_my_climate_changed(self, event: Event = None):
        """Called when my climate have change"""
        # _LOGGER.debug("%s - climate state change", self._attr_unique_id)

        old_state = self._attr_native_value
        self._attr_native_value = self.my_climate.last_ext_temperature_measure
        if old_state != self._attr_native_value:
            self.async_write_ha_state()
        return

    @property
    def icon(self) -> str | None:
        return "mdi:sun-clock"

    @property
    def device_class(self) -> SensorDeviceClass | None:
        return SensorDeviceClass.TIMESTAMP


class TemperatureSlopeSensor(VersatileThermostatBaseEntity, SensorEntity):
    """Representation of a sensor which exposes the temperature slope curve"""

    def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
        """Initialize the slope sensor"""
        super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
        self._attr_name = "Temperature slope"
        self._attr_unique_id = f"{self._device_name}_temperature_slope"

    @callback
    async def async_my_climate_changed(self, event: Event = None):
        """Called when my climate have change"""
        # _LOGGER.debug("%s - climate state change", self._attr_unique_id)

        last_slope = self.my_climate.last_temperature_slope
        if last_slope is None:
            return

        if math.isnan(last_slope) or math.isinf(last_slope):
            raise ValueError(f"Sensor has illegal state {last_slope}")

        old_state = self._attr_native_value
        self._attr_native_value = round(last_slope, self.suggested_display_precision)
        if old_state != self._attr_native_value:
            self.async_write_ha_state()
        return

    @property
    def icon(self) -> str | None:
        if (
            self._attr_native_value is None
            or not isinstance(self._attr_native_value, (int, float))
            or self._attr_native_value == 0
        ):
            return "mdi:thermometer"
        elif self._attr_native_value > 0:
            return "mdi:thermometer-chevron-up"
        else:
            return "mdi:thermometer-chevron-down"

    @property
    def state_class(self) -> SensorStateClass | None:
        return SensorStateClass.MEASUREMENT

    @property
    def native_unit_of_measurement(self) -> str | None:
        if not self.my_climate:
            return None

        return self.my_climate.temperature_unit + "/hour"

    @property
    def suggested_display_precision(self) -> int | None:
        """Return the suggested number of decimal digits for display."""
        return 2


class RegulatedTemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
    """Representation of a Energy sensor which exposes the energy"""

    def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
        """Initialize the regulated temperature sensor"""
        super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
        self._attr_name = "Regulated temperature"
        self._attr_unique_id = f"{self._device_name}_regulated_temperature"

    @callback
    async def async_my_climate_changed(self, event: Event = None):
        """Called when my climate have change"""
        # _LOGGER.debug("%s - climate state change", self._attr_unique_id)

        new_temp = self.my_climate.regulated_target_temp
        if new_temp is None:
            return

        if math.isnan(new_temp) or math.isinf(new_temp):
            raise ValueError(f"Sensor has illegal state {new_temp}")

        old_state = self._attr_native_value
        self._attr_native_value = round(new_temp, self.suggested_display_precision)
        if old_state != self._attr_native_value:
            self.async_write_ha_state()
        return

    @property
    def icon(self) -> str | None:
        return "mdi:thermometer-auto"

    @property
    def device_class(self) -> SensorDeviceClass | None:
        return SensorDeviceClass.TEMPERATURE

    @property
    def state_class(self) -> SensorStateClass | None:
        return SensorStateClass.MEASUREMENT

    @property
    def native_unit_of_measurement(self) -> str | None:
        if not self.my_climate:
            return self.hass.config.units.temperature_unit
        return self.my_climate.temperature_unit

    @property
    def suggested_display_precision(self) -> int | None:
        """Return the suggested number of decimal digits for display."""
        return 1


class EMATemperatureSensor(VersatileThermostatBaseEntity, SensorEntity):
    """Representation of a Exponential Moving Average temp"""

    def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
        """Initialize the regulated temperature sensor"""
        super().__init__(hass, unique_id, entry_infos.get(CONF_NAME))
        self._attr_name = "EMA temperature"
        self._attr_unique_id = f"{self._device_name}_ema_temperature"

    @callback
    async def async_my_climate_changed(self, event: Event = None):
        """Called when my climate have change"""
        # _LOGGER.debug("%s - climate state change", self._attr_unique_id)

        new_ema = self.my_climate.ema_temperature
        if new_ema is None:
            return

        if math.isnan(new_ema) or math.isinf(new_ema):
            raise ValueError(f"Sensor has illegal state {new_ema}")

        old_state = self._attr_native_value
        self._attr_native_value = new_ema
        if old_state != self._attr_native_value:
            self.async_write_ha_state()
        return

    @property
    def icon(self) -> str | None:
        return "mdi:thermometer-lines"

    @property
    def device_class(self) -> SensorDeviceClass | None:
        return SensorDeviceClass.TEMPERATURE

    @property
    def state_class(self) -> SensorStateClass | None:
        return SensorStateClass.MEASUREMENT

    @property
    def native_unit_of_measurement(self) -> str | None:
        if not self.my_climate:
            return self.hass.config.units.temperature_unit
        return self.my_climate.temperature_unit

    @property
    def suggested_display_precision(self) -> int | None:
        """Return the suggested number of decimal digits for display."""
        return 2


class NbActiveDeviceForBoilerSensor(SensorEntity):
    """Representation of the  number of VTherm
    which are active and configured to activate the boiler"""

    # TODO remove all listener mecanisms
    _entity_component_unrecorded_attributes = SensorEntity._entity_component_unrecorded_attributes.union(  # pylint: disable=protected-access
        frozenset({"active_device_ids"})
    )

    def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
        """Initialize the energy sensor"""
        self._hass = hass
        self._config_id = unique_id
        self._device_name = entry_infos.get(CONF_NAME)
        self._attr_name = "Nb device active for boiler"
        self._attr_unique_id = "nb_device_active_boiler"
        self._attr_value = self._attr_native_value = None  # default value
        self._entities = []
        self._attr_active_device_ids = []  # Holds the entity ids of active devices``
        self._cancel_listener_nb_active: Callable | None = None

    @property
    def extra_state_attributes(self) -> dict:
        """Return additional attributes for the sensor."""
        return {
            "active_device_ids": self._attr_active_device_ids,
        }

    @property
    def icon(self) -> str | None:
        return "mdi:heat-wave"

    @property
    def device_info(self) -> DeviceInfo:
        """Return the device info."""
        return DeviceInfo(
            entry_type=None,
            identifiers={(DOMAIN, self._config_id)},
            name=self._device_name,
            manufacturer=DEVICE_MANUFACTURER,
            model=DOMAIN,
        )

    @property
    def state_class(self) -> SensorStateClass | None:
        return SensorStateClass.MEASUREMENT

    @property
    def suggested_display_precision(self) -> int | None:
        """Return the suggested number of decimal digits for display."""
        return 0

    @overrides
    async def async_added_to_hass(self) -> None:
        await super().async_added_to_hass()

        api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self._hass)
        api.central_boiler_manager.register_nb_device_active_boiler(self)

    async def listen_vtherms_entities(self):
        """Initialize the listening of state change of VTherms"""

        # Listen to all VTherm underlying state change
        self._entities = []
        # underlying_entities_id = []

        component: EntityComponent[ClimateEntity] = self.hass.data.get(CLIMATE_DOMAIN)
        if component is None:
            _LOGGER.warning("%s - No climate component found in hass.data", self)
            return

        self.cancel_listening_nb_active()

        for entity in list(component.entities):
            if isinstance(entity, BaseThermostat) and entity.is_used_by_central_boiler:
                self._entities.append(entity)
                # for under in entity.activable_underlying_entities:
                #    underlying_entities_id.append(under.entity_id)
        # if len(underlying_entities_id) > 0:
        # Arme l'écoute de la première entité
        # self._cancel_listener_nb_active = async_track_state_change_event(
        #     self._hass,
        #     underlying_entities_id,
        #     self.calculate_nb_active_devices,
        # )
        #    _LOGGER.info(
        #        "%s - the underlyings that could control the central boiler are %s",
        #        self,
        #        underlying_entities_id,
        #     )
        # Fix 1406
        # self.async_on_remove(self._cancel_listener_nb_active)
        # else:
        #    _LOGGER.debug("%s - no VTherm could control the central boiler", self)

        await self.calculate_nb_active_devices(None)

    async def calculate_nb_active_devices(self, event: Event):
        """Calculate the number of active VTherm and the total active power that have an
        influence on the central boiler and update the list of active device names."""

        # _LOGGER.debug("%s- calculate_nb_active_devices_or_power - the event is %s ", self, event)

        if event is not None:
            new_state: State = event.data.get("new_state")
            # _LOGGER.debug(
            #     "%s - calculate_nb_active_devices_or_power new_state is %s", self, new_state
            # )
            if not new_state:
                return

            old_state: State = event.data.get("old_state")

            # For underlying climate, we need to observe also the hvac_action if available
            new_hvac_action = new_state.attributes.get("hvac_action")
            old_hvac_action = old_state.attributes.get("hvac_action") if old_state is not None else None

            # Filter events that are not interesting for us
            if (
                old_state is not None
                and new_state.state == old_state.state
                and new_hvac_action == old_hvac_action
                # issue 698 - force recalculation when underlying climate doesn't have any hvac_action
                and new_hvac_action is not None
            ):
                # A false state change
                return

            _LOGGER.debug(
                "%s - calculating the number of active underlying device for boiler activation. change change from %s to %s",
                self,
                old_state,
                new_state,
            )
        else:
            _LOGGER.debug(
                "%s - calculating the number of active underlying device for boiler activation. First time calculation",
                self,
            )

        nb_active = 0
        active_device_ids = []

        for entity in self._entities:
            device_actives = entity.device_actives
            _LOGGER.debug(
                "%s - After examining the device_actives, device_actives is %s",
                entity.name,
                device_actives,
            )

            nb_active += len(device_actives)
            active_device_ids.extend(device_actives)

        self._attr_native_value = nb_active
        self._attr_active_device_ids = active_device_ids

        self.async_write_ha_state()
        await VersatileThermostatAPI.get_vtherm_api(self._hass).central_boiler_manager.refresh_central_boiler_custom_attributes()

    @property
    def active_device_ids(self) -> list:
        """Get the list of active device id"""
        return self._attr_active_device_ids

    def __str__(self):
        return f"{self.name}"

    def cancel_listening_nb_active(self):
        """Cancel the listening of underlying VTherm state changes"""
        if self._cancel_listener_nb_active is not None:
            try:
                self._cancel_listener_nb_active()  # pylint: disable=not-callable
            except (ValueError, TypeError):  # the listener could be already cancelled
                pass
            self._cancel_listener_nb_active = None

    @overrides
    async def async_will_remove_from_hass(self) -> None:
        """Called when entity is about to be removed from hass"""
        self.cancel_listening_nb_active()


class TotalPowerActiveDeviceForBoilerSensor(NbActiveDeviceForBoilerSensor):
    """Representation of the total power of VTherm
    which are active and configured to activate the boiler"""

    def __init__(self, hass: HomeAssistant, unique_id, name, entry_infos) -> None:
        """Initialize the energy sensor"""
        super().__init__(hass, unique_id, name, entry_infos)
        self._attr_name = "Total power active for boiler"
        self._attr_unique_id = "total_power_active_boiler"
        self._attr_value = self._attr_native_value = None  # default value
        self._cancel_listener_power: callable | None = None

    @property
    def icon(self) -> str | None:
        return "mdi:flash-auto"

    @property
    def device_class(self) -> SensorDeviceClass | None:
        return SensorDeviceClass.POWER

    @property
    def suggested_display_precision(self) -> int | None:
        """Return the suggested number of decimal digits for display."""
        return 2

    @overrides
    async def async_added_to_hass(self) -> None:
        # do not ! await super().async_added_to_hass()

        api: VersatileThermostatAPI = VersatileThermostatAPI.get_vtherm_api(self._hass)
        api.central_boiler_manager.register_total_power_active_boiler(self)

    async def listen_vtherms_entities(self):
        """Initialize the listening of state change of VTherms"""

        # Listen to all VTherm state change
        self._entities = []
        entities_id = []

        component: EntityComponent[ClimateEntity] = self.hass.data.get(CLIMATE_DOMAIN)
        if component is None:
            _LOGGER.warning("%s - No climate component found in hass.data", self)
            return

        self.cancel_listening_power()

        for entity in list(component.entities):
            if isinstance(entity, BaseThermostat) and entity.is_used_by_central_boiler:
                self._entities.append(entity)
                entities_id.append(entity.entity_id)

        if len(self._entities) > 0:
            # Arme l'écoute de la première entité
            self._cancel_listener_power = async_track_state_change_event(
                self._hass,
                entities_id,
                self.calculate_total_power,
            )
            _LOGGER.info(
                "%s - the VTherm that could control the central boiler are %s",
                self,
                entities_id,
            )
            # Fix 1406
            # self.async_on_remove(self._cancel_listener_power)
        else:
            _LOGGER.debug("%s - no VTherm could control the central boiler", self)

        await self.calculate_total_power(None)

    async def calculate_total_power(self, event: Event):
        """Calculate the total active power that have an
        influence on the central boiler and update the list of active device names."""

        # _LOGGER.debug("%s- calculate_nb_active_devices_or_power - the event is %s ", self, event)

        if event is not None:
            new_state: State = event.data.get("new_state")
            # _LOGGER.debug(
            #     "%s - calculate_nb_active_devices_or_power new_state is %s", self, new_state
            # )
            if not new_state:
                return

            old_state: State = event.data.get("old_state")

            # For underlying climate, we need to observe also the hvac_action if available
            new_mean_cycle_power = new_state.attributes.get("power_manager", {}).get("mean_cycle_power")
            old_mean_cycle_power = old_state.attributes.get("power_manager", {}).get("mean_cycle_power") if old_state is not None else None

            # Filter events that are not interesting for us
            if old_state is not None and new_mean_cycle_power == old_mean_cycle_power:
                # A false state change
                return

            _LOGGER.debug(
                "%s - calculating the total power of active underlying device for boiler activation. mean_power_cycle change from %s to %s",
                self,
                old_mean_cycle_power,
                new_mean_cycle_power,
            )
        else:
            _LOGGER.debug(
                "%s - calculating the total power of active underlying device for boiler activation. First time calculation",
                self,
            )

        total_active_power = 0
        active_device_ids = []

        for entity in self._entities:
            mean_cycle_power = entity.power_manager.mean_cycle_power
            if mean_cycle_power is None or mean_cycle_power <= 0:
                continue

            _LOGGER.debug(
                "%s - After examining the mean_cycle_power, mean_cycle_power is %s",
                entity.name,
                mean_cycle_power,
            )

            active_device_ids.extend([under.entity_id for under in entity.activable_underlying_entities])

            total_active_power += mean_cycle_power

        self._attr_native_value = total_active_power
        self._attr_active_device_ids = active_device_ids

        self.async_write_ha_state()
        await VersatileThermostatAPI.get_vtherm_api(self._hass).central_boiler_manager.refresh_central_boiler_custom_attributes()

    @property
    def active_device_ids(self) -> list:
        """Get the list of active device id"""
        return self._attr_active_device_ids

    def __str__(self):
        return f"{self.name}"

    @overrides
    async def async_will_remove_from_hass(self) -> None:
        """Called when entity is about to be removed from hass"""
        self.cancel_listening_power()

    def cancel_listening_power(self):
        """Cancel the listening of underlying VTherm state changes"""
        if self._cancel_listener_power is not None:
            self._cancel_listener_power()
            self._cancel_listener_power = None
