"""Support for various Hilo sensors."""

from __future__ import annotations

import asyncio
from datetime import datetime, timedelta, timezone
from os.path import isfile

import aiofiles
from homeassistant.components.integration.sensor import METHOD_LEFT, IntegrationSensor
from homeassistant.components.sensor import (
    SensorDeviceClass,
    SensorEntity,
    SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
    CONF_SCAN_INTERVAL,
    CURRENCY_DOLLAR,
    PERCENTAGE,
    SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
    STATE_UNKNOWN,
    Platform,
    UnitOfEnergy,
    UnitOfPower,
    UnitOfSoundPressure,
    UnitOfTemperature,
    __short_version__ as current_version,
)

# This is to add backward compatibility
try:
    from homeassistant.const import UnitOfRatio

    _PARTS_PER_MILLION = UnitOfRatio.PARTS_PER_MILLION
except ImportError:
    from homeassistant.const import CONCENTRATION_PARTS_PER_MILLION

    _PARTS_PER_MILLION = CONCENTRATION_PARTS_PER_MILLION

from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_state_change_event
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.util import Throttle, slugify
import homeassistant.util.dt as dt_util
from packaging.version import Version
from pyhilo.const import UNMONITORED_DEVICES
from pyhilo.device import HiloDevice
from pyhilo.event import Event
from pyhilo.util import from_utc_timestamp
import yaml
from yaml.scanner import ScannerError

from . import Hilo
from .const import (
    CONF_ENERGY_METER_PERIOD,
    CONF_GENERATE_ENERGY_METERS,
    CONF_HQ_PLAN_NAME,
    CONF_TARIFF,
    CONF_UNTARIFICATED_DEVICES,
    DEFAULT_ENERGY_METER_PERIOD,
    DEFAULT_GENERATE_ENERGY_METERS,
    DEFAULT_HQ_PLAN_NAME,
    DEFAULT_SCAN_INTERVAL,
    DEFAULT_UNTARIFICATED_DEVICES,
    DOMAIN,
    EVENT_SCAN_INTERVAL_REDUCTION,
    HILO_ENERGY_TOTAL,
    HILO_SENSOR_CLASSES,
    LOG,
    MAX_SUB_INTERVAL,
    MIN_SCAN_INTERVAL,
    NOTIFICATION_SCAN_INTERVAL,
    REWARD_SCAN_INTERVAL,
    TARIFF_LIST,
    WEATHER_CONDITIONS,
    WEATHER_SCAN_INTERVAL,
)
from .entity import HiloEntity
from .managers import EnergyManager, UtilityManager

WIFI_STRENGTH = {
    "Low": 1,
    "Medium": 2,
    "High": 3,
    "Full": 4,
}


# From netatmo integration
def process_wifi(strength: int) -> str:
    """Process Wi-Fi signal strength and return string for display."""
    if strength >= 86:
        return "Low"
    if strength >= 71:
        return "Medium"
    if strength >= 56:
        return "High"
    return "Full"


def validate_tariff_list(tariff_config):
    """Validate the tariff list from the configuration."""
    return TARIFF_LIST


def generate_entities_from_device(device, hilo, scan_interval):
    """Generate the entities from the device description."""
    entities = []
    if device.type == "Gateway":
        entities.append(
            HiloChallengeSensor(hilo, device, scan_interval),
        )
        entities.append(
            HiloRewardSensor(hilo, device, scan_interval),
        )
        entities.append(
            HiloNotificationSensor(hilo, device, scan_interval),
        )
        entities.append(
            HiloOutdoorTempSensor(hilo, device, scan_interval),
        )
    if device.has_attribute("battery"):
        entities.append(BatterySensor(hilo, device))
    if device.has_attribute("co2"):
        entities.append(Co2Sensor(hilo, device))
    if device.has_attribute("current_temperature"):
        entities.append(TemperatureSensor(hilo, device))
    if device.type in HILO_SENSOR_CLASSES:
        entities.append(DeviceSensor(hilo, device))
    if device.has_attribute("noise"):
        entities.append(NoiseSensor(hilo, device))
    if device.has_attribute("power") and device.model not in UNMONITORED_DEVICES:
        entities.append(PowerSensor(hilo, device))
    if device.has_attribute("target_temperature"):
        entities.append(TargetTemperatureSensor(hilo, device))
    if device.has_attribute("wifi_status"):
        entities.append(WifiStrengthSensor(hilo, device))
    return entities


# noinspection GrazieInspection
async def async_setup_entry(
    hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
    """Set up Hilo sensors based on a config entry."""
    hilo = hass.data[DOMAIN][entry.entry_id]
    new_entities = []
    cost_entities = []
    hq_plan_name = entry.options.get(CONF_HQ_PLAN_NAME, DEFAULT_HQ_PLAN_NAME)
    untarificated_devices = entry.options.get(
        CONF_UNTARIFICATED_DEVICES, DEFAULT_UNTARIFICATED_DEVICES
    )
    energy_meter_period = entry.options.get(
        CONF_ENERGY_METER_PERIOD, DEFAULT_ENERGY_METER_PERIOD
    )
    scan_interval = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
    generate_energy_meters = entry.options.get(
        CONF_GENERATE_ENERGY_METERS, DEFAULT_GENERATE_ENERGY_METERS
    )
    tariff_config = CONF_TARIFF.get(hq_plan_name)
    if untarificated_devices:
        default_tariff_list = ["total"]
    else:
        default_tariff_list = validate_tariff_list(tariff_config)
    if generate_energy_meters:
        energy_manager = await EnergyManager().init(hass, energy_meter_period)
        utility_manager = UtilityManager(hass, energy_meter_period, default_tariff_list)

    def create_energy_entity(hilo, device):
        device._energy_entity = EnergySensor(hilo, device, hass)
        new_entities.append(device._energy_entity)
        energy_entity = f"{slugify(device.name)}_hilo_energy"
        if energy_entity == HILO_ENERGY_TOTAL:
            LOG.error(
                "An hilo entity can't be named 'total' because it conflicts "
                "with the generated name for the smart energy meter"
            )
            return
        tariff_list = default_tariff_list
        if device.type == "Meter":
            energy_entity = HILO_ENERGY_TOTAL
            tariff_list = validate_tariff_list(tariff_config)
        net_consumption = device.net_consumption
        utility_manager.add_meter(energy_entity, tariff_list, net_consumption)

    for d in hilo.devices.all:
        LOG.debug("Adding device %s", d)
        new_entities.extend(generate_entities_from_device(d, hilo, scan_interval))
        if d.has_attribute("power") and d.model not in UNMONITORED_DEVICES:
            # If we opt out the generation of meters we just create the power sensors
            if generate_energy_meters:
                create_energy_entity(hilo, d)

    async_add_entities(new_entities)
    if not generate_energy_meters:
        return
    # Creating cost sensors based on plan
    # This will generate hilo_cost_(low|medium|high) sensors which can be
    # referred later in the energy dashboard based on the tarif selected
    hilo.cost_sensors = {}
    for tarif, amount in tariff_config.items():
        if amount > 0:
            sensor_name = f"Hilo rate {tarif}"
            sensor = HiloCostSensor(hilo, sensor_name, hq_plan_name, amount)
            cost_entities.append(sensor)
            hilo.cost_sensors[tarif] = sensor  # Stores reference for check_tarif

    hilo_rate_current = HiloCostSensor(hilo, "Hilo rate current", hq_plan_name)
    cost_entities.append(hilo_rate_current)

    # Create hilo_rate_current_total sensor that includes access rate per hour
    access_rate = tariff_config.get("access", 0)
    if access_rate > 0:
        hilo_cost_total = HiloCostTotalSensor(
            hilo,
            "Hilo cost total",
            hq_plan_name,
            tariff_config,
            energy_meter_period,
        )
        cost_entities.append(hilo_cost_total)

    hilo.cost_sensors["current"] = hilo_rate_current
    async_add_entities(cost_entities)
    async_track_state_change_event(
        hilo._hass, ["sensor.hilo_rate_current"], hilo_rate_current._handle_state_change
    )

    # This setups the utility_meter platform
    await utility_manager.update(async_add_entities)
    # This sends the entities to the energy dashboard
    await energy_manager.update()
    hilo.check_tarif()


class BatterySensor(HiloEntity, SensorEntity):
    """Define a Battery sensor entity."""

    _attr_device_class = SensorDeviceClass.BATTERY
    _attr_native_unit_of_measurement = PERCENTAGE
    _attr_state_class = SensorStateClass.MEASUREMENT

    def __init__(self, hilo, device):
        """Hilo battery sensor initialization."""
        self._attr_name = f"{device.name} Battery"
        super().__init__(hilo, name=self._attr_name, device=device)
        old_unique_id = f"{slugify(device.name)}-battery"
        self._attr_unique_id = f"{device.identifier.lower()}-battery"
        hilo.async_migrate_unique_id(
            old_unique_id, self._attr_unique_id, Platform.SENSOR
        )
        hilo.async_migrate_unique_id(
            f"{slugify(device.identifier)}-battery",
            self._attr_unique_id,
            Platform.SENSOR,
        )
        LOG.debug("Setting up BatterySensor entity: %s", self._attr_name)

    @property
    def state(self):
        """Return the battery level."""
        return str(int(self._device.get_value("battery", 0)))

    @property
    def icon(self):
        """Return the icon representing the battery level."""
        if not self._device.available:
            return "mdi:lan-disconnect"
        level = round(int(self._device.get_value("battery", 0)) / 10) * 10
        if level < 10:
            return "mdi:battery-alert"
        return f"mdi:battery-{level}"


class Co2Sensor(HiloEntity, SensorEntity):
    """Define a Co2 sensor entity."""

    _attr_device_class = SensorDeviceClass.CO2
    _attr_native_unit_of_measurement = _PARTS_PER_MILLION
    _attr_state_class = SensorStateClass.MEASUREMENT

    def __init__(self, hilo, device):
        """Hilo CO2 sensor initialization."""
        self._attr_name = f"{device.name} CO2"
        super().__init__(hilo, name=self._attr_name, device=device)
        old_unique_id = f"{slugify(device.name)}-co2"
        self._attr_unique_id = f"{device.identifier.lower()}-co2"
        hilo.async_migrate_unique_id(
            old_unique_id, self._attr_unique_id, Platform.SENSOR
        )
        hilo.async_migrate_unique_id(
            f"{slugify(device.identifier)}-co2", self._attr_unique_id, Platform.SENSOR
        )
        LOG.debug("Setting up CO2Sensor entity: %s", self._attr_name)

    @property
    def state(self):
        """Return the CO2 level."""
        return str(int(self._device.get_value("co2", 0)))

    @property
    def icon(self):
        """Return the icon representing the CO2 level."""
        if not self._device.available:
            return "mdi:lan-disconnect"
        return "mdi:molecule-co2"


class EnergySensor(IntegrationSensor):
    """Define a Hilo energy sensor entity."""

    _attr_device_class = SensorDeviceClass.ENERGY
    _attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
    _attr_state_class = SensorStateClass.TOTAL_INCREASING
    _attr_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
    _attr_suggested_display_precision = 2
    _attr_icon = "mdi:lightning-bolt"

    def __init__(self, hilo, device, hass):
        """Hilo Energy sensor initialization."""
        self._device = device
        self._attr_name = f"{device.name} Hilo Energy"
        old_unique_id = f"hilo_energy_{slugify(device.name)}"
        self._attr_unique_id = f"{device.identifier.lower()}-energy"
        hilo.async_migrate_unique_id(
            old_unique_id, self._attr_unique_id, Platform.SENSOR
        )
        hilo.async_migrate_unique_id(
            f"{slugify(device.identifier)}-energy",
            self._attr_unique_id,
            Platform.SENSOR,
        )
        self._unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
        self._suggested_display_precision = 2

        if device.type == "Meter":
            self._attr_name = HILO_ENERGY_TOTAL

        power_unique_id = f"{device.identifier.lower()}-power"
        entity_registry = er.async_get(hass)
        self._source = next(
            (
                entry.entity_id
                for entry in entity_registry.entities.values()
                if entry.unique_id == power_unique_id and entry.platform == DOMAIN
            ),
            f"sensor.{slugify(device.name)}_power",  # fallback
        )
        # ic-dev21: Set initial state and last_valid_state, removes log errors and unavailable states
        initial_state = 0
        self._attr_native_value = initial_state
        self._attr_last_valid_state = initial_state
        self._device_info = DeviceInfo(
            identifiers={(DOMAIN, self._device.identifier)},
        )

        if Version(current_version) >= Version("2026.8"):
            super().__init__(
                integration_method=METHOD_LEFT,
                max_sub_interval=timedelta(seconds=MAX_SUB_INTERVAL),
                name=self._attr_name,
                round_digits=2,
                source_entity=self._source,
                unique_id=self._attr_unique_id,
                unit_prefix="k",
                unit_time="h",
            )
        elif Version(current_version) >= Version("2025.8"):
            super().__init__(
                hass,
                integration_method=METHOD_LEFT,
                max_sub_interval=timedelta(seconds=MAX_SUB_INTERVAL),
                name=self._attr_name,
                round_digits=2,
                source_entity=self._source,
                unique_id=self._attr_unique_id,
                unit_prefix="k",
                unit_time="h",
            )
        elif Version(current_version) >= Version("2024.7"):
            super().__init__(
                integration_method=METHOD_LEFT,
                max_sub_interval=timedelta(seconds=MAX_SUB_INTERVAL),
                name=self._attr_name,
                round_digits=2,
                source_entity=self._source,
                unique_id=self._attr_unique_id,
                unit_prefix="k",
                unit_time="h",
                device_info=self._device_info,
            )
        else:
            super().__init__(
                integration_method=METHOD_LEFT,
                name=self._attr_name,
                round_digits=2,
                source_entity=self._source,
                unique_id=self._attr_unique_id,
                unit_prefix="k",
                unit_time="h",
                device_info=self._device_info,
            )
        self._unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
        self._suggested_display_precision = 2

        self._attr_icon = "mdi:lightning-bolt"
        LOG.debug(
            "Setting up EnergySensor entity: %s with source %s",
            self._attr_name,
            self._source,
        )

    @property
    def unit_of_measurement(self):
        """Return the unit of measurement."""
        return self._attr_unit_of_measurement

    @property
    def suggested_display_precision(self):
        """Return the suggested display precision."""
        return self._attr_suggested_display_precision

    async def async_added_to_hass(self) -> None:
        """Handle entity which will be added."""
        LOG.debug("Added to hass: %s", self._attr_name)
        await super().async_added_to_hass()


class NoiseSensor(HiloEntity, SensorEntity):
    """Define a Netatmo noise sensor entity."""

    _attr_native_unit_of_measurement = UnitOfSoundPressure.DECIBEL
    _attr_state_class = SensorStateClass.MEASUREMENT

    def __init__(self, hilo, device):
        """Hilo Noise sensor initialization."""
        self._attr_name = f"{device.name} Noise"
        super().__init__(hilo, name=self._attr_name, device=device)
        old_unique_id = f"{slugify(device.name)}-noise"
        self._attr_unique_id = f"{device.identifier.lower()}-noise"
        hilo.async_migrate_unique_id(
            old_unique_id, self._attr_unique_id, Platform.SENSOR
        )
        hilo.async_migrate_unique_id(
            f"{slugify(device.identifier)}-noise", self._attr_unique_id, Platform.SENSOR
        )
        LOG.debug("Setting up NoiseSensor entity: %s", self._attr_name)

    @property
    def state(self):
        """Return the current noise level."""
        return str(int(self._device.get_value("noise", 0)))

    @property
    def icon(self):
        """Return the icon representing the noise level."""
        if not self._device.available:
            return "mdi:lan-disconnect"
        if int(self._device.get_value("noise", 0)) > 0:
            return "mdi:volume-vibrate"
        return "mdi:volume-mute"


class PowerSensor(HiloEntity, SensorEntity):
    """Define a Hilo power sensor entity."""

    _attr_device_class = SensorDeviceClass.POWER
    _attr_native_unit_of_measurement = UnitOfPower.WATT
    _attr_state_class = SensorStateClass.MEASUREMENT

    def __init__(self, hilo: Hilo, device: HiloDevice) -> None:
        """Initialize."""
        self._attr_name = f"{device.name} Power"
        super().__init__(hilo, name=self._attr_name, device=device)
        old_unique_id = f"{slugify(device.name)}-power"
        self._attr_unique_id = f"{device.identifier.lower()}-power"
        hilo.async_migrate_unique_id(
            old_unique_id, self._attr_unique_id, Platform.SENSOR
        )
        hilo.async_migrate_unique_id(
            f"{slugify(device.identifier)}-power", self._attr_unique_id, Platform.SENSOR
        )
        LOG.debug("Setting up PowerSensor entity: %s", self._attr_name)

    @property
    def state(self):
        """Return the current power state."""
        return str(int(self._device.get_value("power", 0)))

    @property
    def icon(self):
        """Return the icon representing the power state."""
        if not self._device.available:
            return "mdi:lan-disconnect"
        power = int(self._device.get_value("power", 0))
        if power > 0:
            return "mdi:power-plug"
        return "mdi:power-plug-off"


class TemperatureSensor(HiloEntity, SensorEntity):
    """Define a Hilo temperature sensor entity."""

    _attr_device_class = SensorDeviceClass.TEMPERATURE
    _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
    _attr_state_class = SensorStateClass.MEASUREMENT

    def __init__(self, hilo, device):
        """Hilo Temperature sensor initialization."""
        self._attr_name = f"{device.name} Temperature"
        super().__init__(hilo, name=self._attr_name, device=device)
        old_unique_id = f"{slugify(device.name)}-temperature"
        self._attr_unique_id = f"{device.identifier.lower()}-temperature"
        hilo.async_migrate_unique_id(
            old_unique_id, self._attr_unique_id, Platform.SENSOR
        )
        hilo.async_migrate_unique_id(
            f"{slugify(device.identifier)}-temperature",
            self._attr_unique_id,
            Platform.SENSOR,
        )
        LOG.debug("Setting up TemperatureSensor entity: %s", self._attr_name)

    @property
    def state(self):
        """Return the current temperature."""
        return str(float(self._device.get_value("current_temperature", 0)))

    @property
    def icon(self):
        """Return the icon representing the current temperature."""
        current_temperature = int(self._device.get_value("current_temperature", 0))
        if not self._device.available:
            thermometer = "off"
        elif current_temperature >= 22:
            thermometer = "high"
        elif current_temperature >= 18:
            thermometer = "low"
        else:
            thermometer = "alert"
        return f"mdi:thermometer-{thermometer}"


class TargetTemperatureSensor(HiloEntity, SensorEntity):
    """Define a Hilo target temperature sensor entity."""

    _attr_device_class = SensorDeviceClass.TEMPERATURE
    _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
    _attr_state_class = SensorStateClass.MEASUREMENT

    def __init__(self, hilo, device):
        """Hilo Target Temperature sensor initialization."""
        self._attr_name = f"{device.name} Target Temperature"
        super().__init__(hilo, name=self._attr_name, device=device)
        old_unique_id = f"{slugify(device.name)}-target-temperature"
        self._attr_unique_id = f"{device.identifier.lower()}-target-temperature"
        hilo.async_migrate_unique_id(
            old_unique_id, self._attr_unique_id, Platform.SENSOR
        )
        hilo.async_migrate_unique_id(
            f"{slugify(device.identifier)}-target-temperature",
            self._attr_unique_id,
            Platform.SENSOR,
        )
        LOG.debug("Setting up TargetTemperatureSensor entity: %s", self._attr_name)

    @property
    def state(self):
        """Return the target temperature."""
        return str(float(self._device.get_value("target_temperature", 0)))

    @property
    def icon(self):
        """Return the icon representing the target temperature."""
        target_temperature = int(self._device.get_value("target_temperature", 0))
        if not self._device.available:
            thermometer = "off"
        elif target_temperature >= 22:
            thermometer = "high"
        elif target_temperature >= 18:
            thermometer = "low"
        else:
            thermometer = "alert"
        return f"mdi:thermometer-{thermometer}"


class WifiStrengthSensor(HiloEntity, SensorEntity):
    """Define a Wi-Fi strength sensor entity."""

    _attr_device_class = SensorDeviceClass.SIGNAL_STRENGTH
    _attr_native_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT
    _attr_state_class = SensorStateClass.MEASUREMENT

    def __init__(self, hilo, device):
        """Hilo Wi-Fi strength sensor initialization."""
        self._attr_name = f"{device.name} WifiStrength"
        super().__init__(hilo, name=self._attr_name, device=device)
        old_unique_id = f"{slugify(device.name)}-wifistrength"
        self._attr_unique_id = f"{device.identifier.lower()}-wifistrength"
        hilo.async_migrate_unique_id(
            old_unique_id, self._attr_unique_id, Platform.SENSOR
        )
        LOG.debug("Setting up WifiStrengthSensor entity: %s", self._attr_name)

    @property
    def state(self):
        """Return the Wi-Fi signal strength."""
        return process_wifi(self._device.get_value("wifi_status", 0))

    @property
    def icon(self):
        """Return the icon representing the Wi-Fi strength."""
        if not self._device.available or self._device.get_value("wifi_status", 0) == 0:
            return "mdi:wifi-strength-off"
        return f"mdi:wifi-strength-{WIFI_STRENGTH[self.state]}"

    @property
    def extra_state_attributes(self):
        """Return the Wi-Fi signal strength."""
        return {"wifi_signal": self._device.get_value("wifi_status", 0)}


class HiloNotificationSensor(HiloEntity, RestoreEntity, SensorEntity):
    """Hilo Notification sensor.

    Its state will be the number of notification waiting in the Hilo app.
    Notifications only used for OneLink's alerts & Low-battery warnings.
    We should consider having this sensor enabled only if a smoke detector is in use.
    """

    def __init__(self, hilo, device, scan_interval):
        """Hilo Notification sensor initialization."""
        self._attr_name = "Notifications Hilo"
        super().__init__(hilo, name=self._attr_name, device=device)
        old_unique_id = slugify(self._attr_name)
        self._attr_unique_id = f"{device.identifier.lower()}-{slugify(self._attr_name)}"
        hilo.async_migrate_unique_id(
            old_unique_id, self._attr_unique_id, Platform.SENSOR
        )
        hilo.async_migrate_unique_id(
            f"{slugify(device.identifier)}-{slugify(self._attr_name)}",
            self._attr_unique_id,
            Platform.SENSOR,
        )
        LOG.debug("Setting up NotificationSensor entity: %s", self._attr_name)
        self.scan_interval = timedelta(seconds=NOTIFICATION_SCAN_INTERVAL)
        self._state = 0
        self._notifications = []
        self.async_update = Throttle(self.scan_interval)(self._async_update)

    @property
    def state(self):
        """Return the number of notifications."""
        try:
            return int(self._state)
        except ValueError:
            return 0

    @property
    def icon(self):
        """Return the icon based on the notification state."""
        if not self._device.available:
            return "mdi:lan-disconnect"
        if self.state > 0:
            return "mdi:bell-alert"
        return "mdi:bell-outline"

    @property
    def should_poll(self):
        """Enable polling."""
        return True

    @property
    def extra_state_attributes(self):
        """Return the notifications."""
        return {"notifications": self._notifications}

    async def async_added_to_hass(self):
        """Handle entity about to be added to hass event."""
        await super().async_added_to_hass()
        last_state = await self.async_get_last_state()
        if last_state:
            self._last_update = dt_util.utcnow()
            self._state = last_state.state

    async def _async_update(self):
        self._notifications = []
        for notification in await self._hilo._api.get_event_notifications(
            self._hilo.devices.location_id
        ):
            if notification.get("viewed"):
                continue
            self._notifications.append(
                {
                    "type_id": notification.get("eventTypeId"),
                    "event_id": notification.get("eventId"),
                    "device_id": notification.get("deviceId"),
                    "date": from_utc_timestamp(notification.get("notificationDateUTC")),
                    "title": notification.get("notificationTitle"),
                    "body": notification.get("notificationBody"),
                }
            )
        self._state = len(self._notifications)


class HiloRewardSensor(HiloEntity, RestoreEntity, SensorEntity):
    """Hilo Reward sensor.

    Its state will be either 0 or the total amount rewarded this season.
    """

    _attr_device_class = SensorDeviceClass.MONETARY
    _attr_state_class = SensorStateClass.TOTAL_INCREASING
    _entity_component_unrecorded_attributes = frozenset({"history"})

    def __init__(self, hilo, device, scan_interval):
        """Hilo Reward sensor initialization."""
        self._attr_name = "Recompenses Hilo"

        # Check if currency is configured, set a default if not
        currency = hilo._hass.config.currency
        if currency:
            self._attr_native_unit_of_measurement = currency
        else:
            self._attr_native_unit_of_measurement = "CAD"

        super().__init__(hilo, name=self._attr_name, device=device)
        old_unique_id = slugify(self._attr_name)
        self._attr_unique_id = f"{device.identifier.lower()}-{slugify(self._attr_name)}"
        hilo.async_migrate_unique_id(
            old_unique_id, self._attr_unique_id, Platform.SENSOR
        )
        hilo.async_migrate_unique_id(
            f"{slugify(device.identifier)}-{slugify(self._attr_name)}",
            self._attr_unique_id,
            Platform.SENSOR,
        )
        LOG.debug("Setting up RewardSensor entity: %s", self._attr_name)
        self._history_state_yaml: str = "hilo_eventhistory_state.yaml"
        self.scan_interval = timedelta(seconds=REWARD_SCAN_INTERVAL)
        self._state = 0
        self._history = []
        self._events_to_poll = dict()
        self.async_update = Throttle(self.scan_interval)(self._async_update)
        hilo.register_signalr_listener(self)

        # When we update the list of reward history, we can end up making
        # hundreds of calls to _save_history in a very short amount of time.
        # With a debouncer, we can reduce this to a single save, which is more
        # efficient (save can become pretty slow when the history is long) and
        # makes sure Home Assistant is not slowed down. Some websocket could
        # even lose connection due to the delay introduced by saving many times.
        LOG.debug("Setting up debouncer for history saver")
        self._save_history_debouncer = Debouncer(
            hilo._hass,
            LOG,
            cooldown=5,  # Wait for 5 seconds before writing to file
            immediate=False,
            function=self._save_history,
        )

    @property
    def state(self):
        """Return the total reward amount for the current season."""
        return self._state

    @property
    def icon(self):
        """Set the icon based on the current state."""
        if not self._device.available:
            return "mdi:lan-disconnect"
        return "mdi:cash-plus"

    @property
    def should_poll(self):
        """Enable polling."""
        return True

    @property
    def extra_state_attributes(self):
        """Return the history attributes."""
        return {"history": self._history}

    async def async_added_to_hass(self):
        """Handle entity about to be added to hass event."""
        await super().async_added_to_hass()
        last_state = await self.async_get_last_state()
        if last_state:
            self._last_update = dt_util.utcnow()
            self._state = last_state.state
        cached = await self._load_history()
        if cached:
            self._history = cached
        else:
            await self._async_update()

    async def handle_challenge_details_update(self, challenge):
        """Handle challenge details update from websocket."""
        challenge = challenge[0] if isinstance(challenge, list) else challenge
        LOG.debug("UPDATING challenge in reward: %s", challenge)

        # We're getting events but didn't request any, do not process them
        if len(self._events_to_poll.items()) == 0:
            return

        # Only process events that contain an id and phases
        if challenge.get("id") is None or challenge.get("phases") is None:
            return

        # Skip messages about upcoming events since they don't contain useful info about rewards
        report = challenge.get("report")
        if report and report.get("status") == "Upcoming":
            LOG.debug(
                "Skipping upcoming challenge event in reward: %s", challenge.get("id")
            )
            return

        event = Event(**challenge).as_dict()
        corresponding_season = self._events_to_poll[event["event_id"]]
        del self._events_to_poll[event["event_id"]]

        for season in self._history:
            if season.get("season") == corresponding_season:
                for i, season_event in enumerate(season["events"]):
                    if season_event["event_id"] == event["event_id"]:
                        LOG.debug(
                            "ChallengeId matched, replacing: %s", event["event_id"]
                        )

                        # Some events from the websocket don't contain reward info. Copying it from history (API) if it's there
                        event["reward"] = season_event.get("reward", 0.0)
                        season["events"][i] = event  # On update
                        season["events"] = [
                            item
                            for item in sorted(
                                season["events"], key=lambda x: int(x["event_id"])
                            )
                        ]
                        await self._save_history_debouncer.async_call()
                        return
                LOG.debug("ChallengeId did not match, appending: %s", event["event_id"])
                season["events"].append(event)
                season["events"] = [
                    item
                    for item in sorted(
                        season["events"], key=lambda x: int(x["event_id"])
                    )
                ]

        await self._save_history_debouncer.async_call()

    async def _async_update(self):
        seasons = await self._hilo._api.get_seasons(self._hilo.devices.location_id)
        self._events_to_poll = dict()
        seasons = sorted(seasons, key=lambda x: x["season"], reverse=True)

        # Re-add the totalReward that was present in the legacy API
        for season_data in seasons:
            total = sum(
                event["reward"]
                for event in season_data["events"]
                if "reward" in event and not event.get("isPreseasonEvent")
            )
            season_data["totalReward"] = total

        if seasons:
            current_history = await self._load_history()
            new_history = []

            for idx, season in enumerate(seasons):
                current_history_season = next(
                    (
                        item
                        for item in current_history
                        if item.get("season") == season.get("season")
                    ),
                    None,
                )

                if idx == 0:
                    self._state = season.get("totalReward", 0)
                events = []
                for raw_event in season.get("events", []):
                    current_history_event = None
                    event = None

                    if current_history_season:
                        current_history_event = next(
                            (
                                ev
                                for ev in current_history_season["events"]
                                if ev["event_id"] == raw_event["id"]
                            ),
                            None,
                        )

                    start_date_utc = datetime.fromisoformat(raw_event["startDateUtc"])
                    event_age = datetime.now(timezone.utc) - start_date_utc
                    if (
                        current_history_event
                        and current_history_event.get("state") == "off"
                        and event_age > timedelta(days=1)
                    ):
                        # No point updating events for previously completed events, they won't change.
                        event = current_history_event
                    else:
                        # Save the event to poll in a dict so that we can easily lookup the season when the websocket event comes in
                        self._events_to_poll[raw_event["id"]] = season.get("season")
                        event = Event(**raw_event).as_dict()

                        # details = await self._hilo.get_event_details(raw_event["id"])
                        # event = Event(**details).as_dict()

                    if event:
                        events.append(event)

                season["events"] = events
                new_history.append(season)

            self._history = new_history
            await self._save_history_debouncer.async_call()
            for eventId in self._events_to_poll:
                await self._hilo.subscribe_to_challenge(eventId)

    async def _load_history(self) -> list:
        history: list = []
        if isfile(self._history_state_yaml):
            async with aiofiles.open(self._history_state_yaml, mode="r") as yaml_file:
                LOG.debug("Loading history state from yaml")
                content = await yaml_file.read()
                try:
                    history = await asyncio.get_running_loop().run_in_executor(
                        None, yaml.safe_load, content
                    )
                except ScannerError:
                    LOG.error("History state YAML is corrupted, resetting to default.")
                if not history or not isinstance(history, list):
                    LOG.error("History state YAML is invalid, resetting to default.")
                    history = []

        return history

    async def _save_history(self):
        async with aiofiles.open(self._history_state_yaml, mode="w") as yaml_file:
            LOG.debug("Saving history state to yaml file")
            content = await asyncio.get_running_loop().run_in_executor(
                None, yaml.dump, self._history
            )
            await yaml_file.write(content)


class HiloChallengeSensor(HiloEntity, SensorEntity):
    """Hilo challenge sensor.

    Its state will be either:
    - off: no ongoing or scheduled challenge
    - scheduled: A challenge is scheduled, details in the next_events
                 extra attribute
    - pre_cold: optional phase to cool further before appreciation
    - appreciation: optional phase to pre-heat more before challenge
    - pre_heat: Currently in the pre-heat phase
    - reduction or on: Challenge is currently active, heat is lowered
    - recovery: Challenge is completed, we're reheating.
    """

    def __init__(self, hilo, device, scan_interval):
        """Hilo Challenge sensor initialization."""
        self._attr_name = "Defi Hilo"
        self._attr_device_class = SensorDeviceClass.ENUM
        self._attr_options = [
            "off",
            "scheduled",
            "pre_cold",
            "appreciation",
            "pre_heat",
            "reduction",
            "recovery",
            "completed",
            "unknown",
        ]
        super().__init__(hilo, name=self._attr_name, device=device)
        old_unique_id = slugify(self._attr_name)
        self._attr_unique_id = f"{device.identifier.lower()}-{slugify(self._attr_name)}"
        hilo.async_migrate_unique_id(
            old_unique_id, self._attr_unique_id, Platform.SENSOR
        )
        hilo.async_migrate_unique_id(
            f"{slugify(device.identifier)}-{slugify(self._attr_name)}",
            self._attr_unique_id,
            Platform.SENSOR,
        )
        LOG.debug("Setting up ChallengeSensor entity: %s", self._attr_name)
        self.scan_interval = timedelta(seconds=EVENT_SCAN_INTERVAL_REDUCTION)
        self._state = "off"
        self._next_events = []
        self._events = {}  # Store active events
        self.async_update = Throttle(timedelta(seconds=MIN_SCAN_INTERVAL))(
            self._async_update
        )
        hilo.register_signalr_listener(self)

    async def handle_challenge_added(self, event_data):
        """Handle new challenge event."""
        LOG.debug("handle_challenge_added running")
        if event_data.get("progress") == "scheduled":
            event_id = event_data.get("id")
            if event_id:
                event = Event(**event_data)
                if self._hilo.appreciation > 0:
                    event.appreciation(self._hilo.appreciation)
                if self._hilo.pre_cold > 0:
                    event.pre_cold(self._hilo.pre_cold)
                self._events[event_id] = event
                self._update_next_events()

    async def handle_challenge_list_initial(self, challenges):
        """Handle initial challenge list."""
        LOG.debug("handle_challenge_list_initial challenges: %s", challenges)
        self._events.clear()
        LOG.debug("handle_challenge_list_initial events: %s", self._events)
        for challenge in challenges:
            event_id = challenge.get("id")
            progress = challenge.get("progress")
            LOG.debug("handle_challenge_list_initial progress is %s", progress)
            if challenge.get("progress") in ["scheduled", "inProgress"]:
                event_id = challenge.get("id")
                if event_id:
                    event = Event(**challenge)
                    if self._hilo.appreciation > 0:
                        event.appreciation(self._hilo.appreciation)
                    if self._hilo.pre_cold > 0:
                        event.pre_cold(self._hilo.pre_cold)
                    self._events[event_id] = event
        self._update_next_events()

    async def handle_challenge_list_update(self, challenges):
        """Handle challenge list updates."""
        LOG.debug("handle_challenge_list_update is running")
        for challenge in challenges:
            event_id = challenge.get("id")
            progress = challenge.get("progress")
            baselinewH = challenge.get("baselineWh")
            LOG.debug("handle_challenge_list_update progress is %s", progress)
            LOG.debug("handle_challenge_list_update baselineWh is %s", baselinewH)
            if event_id in self._events:
                if challenge.get("progress") == "completed":
                    # Find the oldest event based on recovery_end datetime
                    oldest_event_id = min(
                        self._events.keys(),
                        key=lambda key: (
                            self._events[key]
                            .as_dict()
                            .get("phases", {})
                            .get("recovery_end", "")
                        ),
                    )
                    await asyncio.sleep(300)
                    del self._events[oldest_event_id]
                    break
                else:
                    current_event = self._events[event_id]
                    LOG.debug(
                        "handle_challenge_list_update current event is: %s",
                        current_event,
                    )
                    updated_event = Event(**{**current_event.as_dict(), **challenge})
                    if self._hilo.appreciation > 0:
                        updated_event.appreciation(self._hilo.appreciation)
                    if self._hilo.pre_cold > 0:
                        updated_event.pre_cold(self._hilo.pre_cold)
                    self._events[event_id] = updated_event
        self._update_next_events()

    async def handle_challenge_details_update(self, challenge):
        """Handle challenge detail updates."""
        LOG.debug("handle_challenge_details_update challenge is %s", challenge)
        challenge = challenge[0] if isinstance(challenge, list) else challenge
        event_id = challenge.get("id")
        event_has_id = event_id is not None

        # In case we get a consumption update (there is no event id),
        # get the event id of the next event so that we can update it
        if event_id is None and len(self._next_events) > 0:
            event_id = self._next_events[0]["event_id"]

        progress = challenge.get("progress", "unknown")

        baseline_points = challenge.get("cumulativeBaselinePoints", [])

        if not baseline_points:
            consumption = challenge.get("consumption", {})
            baseline_points = consumption.get("cumulativeBaselinePoints", [])
        if baseline_points:
            baselinewH = baseline_points[-1]["wh"]
        else:
            baselinewH = 0
        allowed_kwh = baselinewH / 1000 if baselinewH > 0 else 0

        used_wH = challenge.get("currentWh", 0)
        if used_wH is not None and used_wH > 0:
            used_kWh = used_wH / 1000
        else:
            used_kWh = 0

        LOG.debug("handle_challenge_details_update progress is %s", progress)
        LOG.debug("handle_challenge_details_update baselineWh is %s", baselinewH)
        LOG.debug("handle_challenge_details_update used_kwh is %s", used_kWh)
        LOG.debug("handle_challenge_details_update allowed_kwh is %s", allowed_kwh)

        if event_id in self._events:
            if challenge.get("progress") == "completed":
                # ajout d'un asyncio sleep ici pour avoir l'état completed avant le retrait du challenge
                await asyncio.sleep(300)
                self._events.pop(event_id, None)

            # Consumption update
            elif used_wH is not None and used_wH > 0:
                current_event = self._events[event_id]
                current_event.update_wh(used_wH)
                if baselinewH > 0:
                    current_event.update_allowed_wh(baselinewH)
            # For non consumption updates, we need an event id
            elif event_has_id:
                current_event = self._events[event_id]
                updated_event = Event(**{**current_event.as_dict(), **challenge})
                if self._hilo.appreciation > 0:
                    updated_event.appreciation(self._hilo.appreciation)
                if self._hilo.pre_cold > 0:
                    updated_event.pre_cold(self._hilo.pre_cold)
                if baselinewH > 0:
                    updated_event.update_allowed_wh(baselinewH)
                elif current_event.allowed_kWh > 0:
                    updated_event.allowed_kWh = current_event.allowed_kWh
                self._events[event_id] = updated_event
            self._update_next_events()

    def _update_next_events(self):
        """Update the next_events list based on current events."""
        LOG.debug("_update_next_events sorting events")
        # Sort events by start time
        sorted_events = sorted(self._events.values(), key=lambda x: x.preheat_start)

        self._next_events = [event.as_dict() for event in sorted_events]

        # Force an update of the entity
        self.async_write_ha_state()

    @property
    def state(self):
        """Return the current state based on next events."""
        if len(self._next_events) > 0:
            event = Event(**{**{"id": 0}, **self._next_events[0]})
            return event.state
        return "off"

    @property
    def icon(self):
        """Set the icon based on the current state."""
        if not self._device.available:
            return "mdi:lan-disconnect"
        if self.state == "appreciation":
            return "mdi:glass-cocktail"
        if self.state == "off":
            return "mdi:lightning-bolt"
        if self.state == "scheduled":
            return "mdi:progress-clock"
        if self.state == "pre_heat":
            return "mdi:radiator"
        if self.state in ["reduction", "on"]:
            return "mdi:power-plug-off"
        if self.state == "recovery":
            return "mdi:calendar-check"
        if self.state == "pre_cold":
            return "mdi:radiator-off"
        return "mdi:battery-alert"

    @property
    def should_poll(self):
        """Don't poll with websockets. Poll to update allowed_wh in pre_heat phrase and consumption in reduction phase."""
        return self.state in ["recovery", "reduction", "pre_heat"]

    @property
    def extra_state_attributes(self):
        """Return the next events attribute."""
        return {"next_events": self._next_events}

    async def async_added_to_hass(self):
        """Handle entity about to be added to hass event."""
        await super().async_added_to_hass()

        await self._hilo.subscribe_to_challengelist()

    async def _async_update(self):
        """Update fallback, but not needed with websockets."""
        for event_id in self._events:
            event = self._events.get(event_id)
            if event.should_check_for_allowed_wh():
                LOG.debug("ASYNC UPDATE SUB: EVENT: %s", event_id)
                await self._hilo.subscribe_to_challenge(event_id)
                await self._hilo.request_challenge_consumption_update(event_id)
            elif self.state == "reduction":
                LOG.debug("ASYNC UPDATE: EVENT: %s", event_id)
                await self._hilo.request_challenge_consumption_update(event_id)


class DeviceSensor(HiloEntity, SensorEntity):
    """Simple device entity.

    Devices like the gateway or Smoke Detectors don't have many attributes,
    except for the "disconnected" attribute. These entities are monitoring
    this state.
    """

    def __init__(self, hilo, device):
        """Initialize."""
        self._attr_name = device.name
        super().__init__(hilo, name=self._attr_name, device=device)
        old_unique_id = slugify(device.name)
        self._attr_unique_id = f"{device.identifier.lower()}-{slugify(device.name)}"
        hilo.async_migrate_unique_id(
            old_unique_id, self._attr_unique_id, Platform.SENSOR
        )
        hilo.async_migrate_unique_id(
            f"{slugify(device.identifier)}-{slugify(device.name)}",
            self._attr_unique_id,
            Platform.SENSOR,
        )
        LOG.debug("Setting up DeviceSensor entity: %s", self._attr_name)

    @property
    def state(self):
        """Return the connection state."""
        return "on" if self._device.available else "off"

    @property
    def extra_state_attributes(self):
        """Return the extra state attributes."""
        return {k: self._device.get_value(k) for k in self._device.attributes}

    @property
    def icon(self):
        """Set the icon based on the connection state."""
        if not self._device.available:
            return "mdi:lan-disconnect"
        if self.state == "off":
            return "mdi:access-point-network-off"
        return "mdi:access-point-network"


class HiloCostSensor(HiloEntity, SensorEntity):
    """This sensor generates cost entities."""

    _attr_device_class = SensorDeviceClass.MONETARY
    _attr_native_unit_of_measurement = (
        f"{CURRENCY_DOLLAR}/{UnitOfEnergy.KILO_WATT_HOUR}"
    )
    _attr_state_class = SensorStateClass.TOTAL
    _attr_icon = "mdi:cash"

    def __init__(self, hilo, name, plan_name, amount=0):
        """Initialize."""
        for d in hilo.devices.all:
            if d.type == "Gateway":
                device = d
        if "low_threshold" in name:
            self._attr_device_class = SensorDeviceClass.ENERGY
            self._attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
        elif "access" in name.lower():
            # Access fee is a fixed daily cost, not per kWh
            self._attr_native_unit_of_measurement = f"{CURRENCY_DOLLAR}/day"
        self.data = None
        self._attr_name = name
        self.plan_name = plan_name
        self._last_update = dt_util.utcnow()
        self._cost = amount
        old_unique_id = slugify(self._attr_name)
        self._attr_unique_id = f"{device.identifier.lower()}-{slugify(self._attr_name)}"
        hilo.async_migrate_unique_id(
            old_unique_id, self._attr_unique_id, Platform.SENSOR
        )
        hilo.async_migrate_unique_id(
            f"{slugify(device.identifier)}-{slugify(self._attr_name)}",
            self._attr_unique_id,
            Platform.SENSOR,
        )
        self._last_update = dt_util.utcnow()
        super().__init__(hilo, name=self._attr_name, device=device)
        LOG.info(
            "Initializing energy cost sensor %s %s Amount: %s", name, plan_name, amount
        )

    def _handle_state_change(self, event):
        LOG.debug("_handle_state_change() %s | %s ", self, self._last_update)
        if (state := event.data.get("new_state")) is None:
            return

        now = dt_util.utcnow()
        try:
            if (
                state.attributes.get("hilo_update")
                and self._last_update + timedelta(seconds=30) < now
            ):
                LOG.debug(
                    "Setting new state %s state=%s state.attributes=%s",
                    state.state,
                    state,
                    state.attributes,
                )
                self._cost = state.state
                self._last_update = now
        except ValueError:
            LOG.error(
                "Invalidate state received for %s: %s", self._attr_unique_id, state
            )

    @property
    def state(self):
        """Return the cost."""
        return self._cost

    @property
    def should_poll(self) -> bool:
        """Disable polling."""
        return False

    @property
    def extra_state_attributes(self):
        """Return the cost sensor attributes."""
        return {
            "Cost": self._cost,
            "Plan": self.plan_name,
            "last_update": self._last_update,
        }

    async def async_added_to_hass(self):
        """Handle entity about to be added to hass event."""
        await super().async_added_to_hass()

    async def async_update(self):
        """Update the state."""
        self._last_update = dt_util.utcnow()


class HiloCostTotalSensor(HiloEntity, SensorEntity):
    """Sensor that totals all electricity costs including access fee.

    Calculates: (low_kwh × low_rate) + (medium_kwh × medium_rate)
                + (high_kwh × high_rate) + access_fee_prorated_today

    All values in dollars. Updates on poll via should_poll + scan_interval.
    """

    _attr_device_class = SensorDeviceClass.MONETARY
    _attr_state_class = SensorStateClass.TOTAL
    _attr_icon = "mdi:cash-register"

    def __init__(self, hilo, name, plan_name, tariff_config, energy_meter_period):
        """Initialize."""
        for d in hilo.devices.all:
            if d.type == "Gateway":
                device = d
        # Check if currency is configured, set a default if not
        currency = hilo._hass.config.currency
        if currency:
            self._attr_native_unit_of_measurement = currency
        else:
            self._attr_native_unit_of_measurement = "CAD"
        self._attr_name = name
        self.plan_name = plan_name
        self._tariff_config = tariff_config
        self._access_rate = tariff_config.get("access", 0)
        self._last_update = dt_util.utcnow()
        old_unique_id = slugify(self._attr_name)
        self._attr_unique_id = (
            f"{slugify(device.identifier)}-{slugify(self._attr_name)}"
        )
        hilo.async_migrate_unique_id(
            old_unique_id, self._attr_unique_id, Platform.SENSOR
        )
        super().__init__(hilo, name=self._attr_name, device=device)
        LOG.info(
            "Initializing total cost sensor %s %s access_rate: %s",
            name,
            plan_name,
            self._access_rate,
        )

    def _update_callback(self):
        """Gateway value updates don't need to trigger a cost refresh."""
        return

    @property
    def state(self):
        """Return the total cost in dollars."""
        total = 0.0
        for tarif in ["low", "medium", "high"]:
            rate = self._tariff_config.get(tarif, 0)
            if rate <= 0:
                continue
            energy_entity = f"sensor.{HILO_ENERGY_TOTAL}_{tarif}"
            energy_state = self._hilo._hass.states.get(energy_entity)
            if energy_state is None or energy_state.state in ("unknown", "unavailable"):
                continue
            try:
                total += float(energy_state.state) * rate
            except (ValueError, TypeError):
                LOG.debug(
                    "Could not parse energy state for %s: %s",
                    energy_entity,
                    energy_state.state,
                )
        # Access fee: prorated based on time elapsed today
        now = dt_util.now()
        midnight = now.replace(hour=0, minute=0, second=0, microsecond=0)
        seconds_today = (now - midnight).total_seconds()
        total += self._access_rate * (seconds_today / 86400)
        return round(total, 2)

    @property
    def suggested_display_precision(self) -> int:
        """Return the suggested display precision."""
        return 2

    @property
    def should_poll(self) -> bool:
        """Enable polling to update the cost periodically."""
        return True

    @property
    def extra_state_attributes(self):
        """Return the cost breakdown attributes."""
        attrs = {
            "Plan": self.plan_name,
            "Access Rate ($/day)": self._access_rate,
            "last_update": self._last_update,
        }
        for tarif in ["low", "medium", "high"]:
            rate = self._tariff_config.get(tarif, 0)
            if rate > 0:
                energy_entity = f"sensor.{HILO_ENERGY_TOTAL}_{tarif}"
                energy_state = self._hilo._hass.states.get(energy_entity)
                kwh = 0.0
                if energy_state is not None and energy_state.state not in (
                    "unknown",
                    "unavailable",
                ):
                    try:
                        kwh = float(energy_state.state)
                    except (ValueError, TypeError):
                        pass
                attrs[f"{tarif}_kwh"] = round(kwh, 3)
                attrs[f"{tarif}_rate"] = rate
                attrs[f"{tarif}_cost"] = round(kwh * rate, 2)
        now = dt_util.now()
        midnight = now.replace(hour=0, minute=0, second=0, microsecond=0)
        seconds_today = (now - midnight).total_seconds()
        attrs["access_cost_today"] = round(
            self._access_rate * (seconds_today / 86400), 4
        )
        return attrs

    async def async_added_to_hass(self):
        """Handle entity about to be added to hass event."""
        await super().async_added_to_hass()

    async def async_update(self):
        """Update the state."""
        self._last_update = dt_util.utcnow()


class HiloOutdoorTempSensor(HiloEntity, SensorEntity):
    """Hilo outdoor temperature sensor.

    Its state will be the current outdoor weather as reported by the Hilo App
    """

    _attr_device_class = SensorDeviceClass.TEMPERATURE
    _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
    _attr_state_class = SensorStateClass.MEASUREMENT

    def __init__(self, hilo, device, scan_interval):
        """Initialize."""
        self._attr_name = "Outdoor Weather Hilo"
        super().__init__(hilo, name=self._attr_name, device=device)
        old_unique_id = slugify(self._attr_name)
        self._attr_unique_id = f"{device.identifier.lower()}-{slugify(self._attr_name)}"
        hilo.async_migrate_unique_id(
            old_unique_id, self._attr_unique_id, Platform.SENSOR
        )
        hilo.async_migrate_unique_id(
            f"{slugify(device.identifier)}-{slugify(self._attr_name)}",
            self._attr_unique_id,
            Platform.SENSOR,
        )
        LOG.debug("Setting up OutdoorWeatherSensor entity: %s", self._attr_name)
        self.scan_interval = timedelta(seconds=WEATHER_SCAN_INTERVAL)
        self._state = STATE_UNKNOWN
        self._weather = {}
        self.async_update = Throttle(self.scan_interval)(self._async_update)

    @property
    def state(self):
        """Return the current outdoor temperature."""
        try:
            return int(self._state)
        except ValueError:
            return STATE_UNKNOWN

    @property
    def icon(self):
        """Set the icon based on weather condition."""
        condition = self._weather.get("condition", "").lower()
        LOG.debug("Current condition: %s", condition)
        if not condition:
            return "mdi:lan-disconnect"
        return WEATHER_CONDITIONS.get(self._weather.get("condition", "Unknown"))

    @property
    def should_poll(self):
        """Poll needed to update weather data."""
        return True

    @property
    def extra_state_attributes(self):
        """Add weather attributes."""
        LOG.debug("Adding weather %s", self._weather)
        return {
            key: self._weather[key]
            for key in self._weather
            if key not in ["temperature", "icon"]
        }

    def _update_callback(self):
        """Gateway value updates don't need to trigger a weather refresh."""
        return

    async def async_added_to_hass(self):
        """Handle entity about to be added to hass event."""
        await super().async_added_to_hass()

    async def _async_update(self):
        self._weather = {}
        self._weather = await self._hilo._api.get_weather(
            self._hilo.devices.location_id
        )
        self._state = self._weather.get("temperature")
