"""Support for sensor entities.""" from collections.abc import Callable from dataclasses import dataclass from datetime import date, datetime, time, timedelta import logging import random from typing import override from thinqconnect import USAGE_DAILY, USAGE_MONTHLY, DeviceType, ThinQAPIException from thinqconnect.devices.const import Property as ThinQProperty from thinqconnect.integration import ActiveMode, ThinQPropertyEx, TimerProperty from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, SensorEntityDescription, SensorStateClass, ) from homeassistant.const import ( UnitOfDensity, UnitOfEnergy, UnitOfRatio, UnitOfTemperature, UnitOfTime, ) from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.event import async_track_point_in_time from homeassistant.util import dt as dt_util from . import ThinqConfigEntry from .coordinator import DeviceDataUpdateCoordinator from .entity import ThinQEntity AIR_QUALITY_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { ThinQProperty.PM1: SensorEntityDescription( key=ThinQProperty.PM1, device_class=SensorDeviceClass.PM1, native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, ), ThinQProperty.PM2: SensorEntityDescription( key=ThinQProperty.PM2, device_class=SensorDeviceClass.PM25, native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, ), ThinQProperty.PM10: SensorEntityDescription( key=ThinQProperty.PM10, device_class=SensorDeviceClass.PM10, native_unit_of_measurement=UnitOfDensity.MICROGRAMS_PER_CUBIC_METER, state_class=SensorStateClass.MEASUREMENT, ), ThinQProperty.HUMIDITY: SensorEntityDescription( key=ThinQProperty.HUMIDITY, device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=UnitOfRatio.PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ), ThinQProperty.MONITORING_ENABLED: SensorEntityDescription( key=ThinQProperty.MONITORING_ENABLED, device_class=SensorDeviceClass.ENUM, translation_key=ThinQProperty.MONITORING_ENABLED, ), ThinQProperty.TEMPERATURE: SensorEntityDescription( key=ThinQProperty.TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, translation_key=ThinQProperty.TEMPERATURE, ), ThinQProperty.ODOR_LEVEL: SensorEntityDescription( key=ThinQProperty.ODOR_LEVEL, device_class=SensorDeviceClass.ENUM, translation_key=ThinQProperty.ODOR_LEVEL, ), ThinQProperty.TOTAL_POLLUTION_LEVEL: SensorEntityDescription( key=ThinQProperty.TOTAL_POLLUTION_LEVEL, device_class=SensorDeviceClass.ENUM, translation_key=ThinQProperty.TOTAL_POLLUTION_LEVEL, ), ThinQProperty.CO2: SensorEntityDescription( key=ThinQProperty.CO2, device_class=SensorDeviceClass.ENUM, translation_key="carbon_dioxide", ), } BATTERY_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { ThinQProperty.BATTERY_PERCENT: SensorEntityDescription( key=ThinQProperty.BATTERY_PERCENT, translation_key=ThinQProperty.BATTERY_LEVEL, ), } DISH_WASHING_COURSE_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { ThinQProperty.CURRENT_DISH_WASHING_COURSE: SensorEntityDescription( key=ThinQProperty.CURRENT_DISH_WASHING_COURSE, device_class=SensorDeviceClass.ENUM, translation_key=ThinQProperty.CURRENT_DISH_WASHING_COURSE, ) } FILTER_INFO_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { ThinQProperty.FILTER_LIFETIME: SensorEntityDescription( key=ThinQProperty.FILTER_LIFETIME, native_unit_of_measurement=UnitOfTime.HOURS, translation_key=ThinQProperty.FILTER_LIFETIME, ), ThinQProperty.FILTER_REMAIN_PERCENT: SensorEntityDescription( key=ThinQProperty.FILTER_REMAIN_PERCENT, native_unit_of_measurement=UnitOfRatio.PERCENTAGE, translation_key=ThinQProperty.FILTER_LIFETIME, ), ThinQProperty.TOP_FILTER_REMAIN_PERCENT: SensorEntityDescription( key=ThinQProperty.TOP_FILTER_REMAIN_PERCENT, native_unit_of_measurement=UnitOfRatio.PERCENTAGE, translation_key=ThinQProperty.TOP_FILTER_REMAIN_PERCENT, ), } HUMIDITY_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { ThinQProperty.CURRENT_HUMIDITY: SensorEntityDescription( key=ThinQProperty.CURRENT_HUMIDITY, device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=UnitOfRatio.PERCENTAGE, state_class=SensorStateClass.MEASUREMENT, ) } JOB_MODE_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { ThinQProperty.CURRENT_JOB_MODE: SensorEntityDescription( key=ThinQProperty.CURRENT_JOB_MODE, device_class=SensorDeviceClass.ENUM, translation_key=ThinQProperty.CURRENT_JOB_MODE, ), ThinQPropertyEx.CURRENT_JOB_MODE_STICK_CLEANER: SensorEntityDescription( key=ThinQProperty.CURRENT_JOB_MODE, device_class=SensorDeviceClass.ENUM, translation_key=ThinQPropertyEx.CURRENT_JOB_MODE_STICK_CLEANER, ), ThinQProperty.PERSONALIZATION_MODE: SensorEntityDescription( key=ThinQProperty.PERSONALIZATION_MODE, device_class=SensorDeviceClass.ENUM, translation_key=ThinQProperty.PERSONALIZATION_MODE, ), } LIGHT_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { ThinQProperty.BRIGHTNESS: SensorEntityDescription( key=ThinQProperty.BRIGHTNESS, translation_key=ThinQProperty.BRIGHTNESS, ), ThinQProperty.DURATION: SensorEntityDescription( key=ThinQProperty.DURATION, native_unit_of_measurement=UnitOfTime.HOURS, translation_key=ThinQProperty.DURATION, ), } POWER_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { ThinQProperty.POWER_LEVEL: SensorEntityDescription( key=ThinQProperty.POWER_LEVEL, translation_key=ThinQProperty.POWER_LEVEL, ) } PREFERENCE_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { ThinQProperty.RINSE_LEVEL: SensorEntityDescription( key=ThinQProperty.RINSE_LEVEL, device_class=SensorDeviceClass.ENUM, translation_key=ThinQProperty.RINSE_LEVEL, ), ThinQProperty.SOFTENING_LEVEL: SensorEntityDescription( key=ThinQProperty.SOFTENING_LEVEL, device_class=SensorDeviceClass.ENUM, translation_key=ThinQProperty.SOFTENING_LEVEL, ), } RECIPE_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { ThinQProperty.RECIPE_NAME: SensorEntityDescription( key=ThinQProperty.RECIPE_NAME, device_class=SensorDeviceClass.ENUM, translation_key=ThinQProperty.RECIPE_NAME, ), ThinQProperty.WORT_INFO: SensorEntityDescription( key=ThinQProperty.WORT_INFO, device_class=SensorDeviceClass.ENUM, translation_key=ThinQProperty.WORT_INFO, ), ThinQProperty.YEAST_INFO: SensorEntityDescription( key=ThinQProperty.YEAST_INFO, device_class=SensorDeviceClass.ENUM, translation_key=ThinQProperty.YEAST_INFO, ), ThinQProperty.HOP_OIL_INFO: SensorEntityDescription( key=ThinQProperty.HOP_OIL_INFO, translation_key=ThinQProperty.HOP_OIL_INFO, ), ThinQProperty.HOP_OIL_CAPSULE_1: SensorEntityDescription( key=ThinQProperty.HOP_OIL_CAPSULE_1, device_class=SensorDeviceClass.ENUM, translation_key=ThinQProperty.HOP_OIL_CAPSULE_1, ), ThinQProperty.HOP_OIL_CAPSULE_2: SensorEntityDescription( key=ThinQProperty.HOP_OIL_CAPSULE_2, device_class=SensorDeviceClass.ENUM, translation_key=ThinQProperty.HOP_OIL_CAPSULE_2, ), ThinQProperty.FLAVOR_INFO: SensorEntityDescription( key=ThinQProperty.FLAVOR_INFO, translation_key=ThinQProperty.FLAVOR_INFO, ), ThinQProperty.FLAVOR_CAPSULE_1: SensorEntityDescription( key=ThinQProperty.FLAVOR_CAPSULE_1, device_class=SensorDeviceClass.ENUM, translation_key=ThinQProperty.FLAVOR_CAPSULE_1, ), ThinQProperty.FLAVOR_CAPSULE_2: SensorEntityDescription( key=ThinQProperty.FLAVOR_CAPSULE_2, device_class=SensorDeviceClass.ENUM, translation_key=ThinQProperty.FLAVOR_CAPSULE_2, ), ThinQProperty.BEER_REMAIN: SensorEntityDescription( key=ThinQProperty.BEER_REMAIN, native_unit_of_measurement=UnitOfRatio.PERCENTAGE, translation_key=ThinQProperty.BEER_REMAIN, ), } REFRIGERATION_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { ThinQProperty.FRESH_AIR_FILTER: SensorEntityDescription( key=ThinQProperty.FRESH_AIR_FILTER, device_class=SensorDeviceClass.ENUM, translation_key=ThinQProperty.FRESH_AIR_FILTER, ), ThinQProperty.FRESH_AIR_FILTER_REMAIN_PERCENT: SensorEntityDescription( key=ThinQProperty.FRESH_AIR_FILTER_REMAIN_PERCENT, native_unit_of_measurement=UnitOfRatio.PERCENTAGE, translation_key=ThinQProperty.FRESH_AIR_FILTER, ), } RUN_STATE_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { ThinQProperty.CURRENT_STATE: SensorEntityDescription( key=ThinQProperty.CURRENT_STATE, device_class=SensorDeviceClass.ENUM, translation_key=ThinQProperty.CURRENT_STATE, ), ThinQProperty.COCK_STATE: SensorEntityDescription( key=ThinQProperty.COCK_STATE, device_class=SensorDeviceClass.ENUM, translation_key=ThinQProperty.COCK_STATE, ), ThinQProperty.STERILIZING_STATE: SensorEntityDescription( key=ThinQProperty.STERILIZING_STATE, device_class=SensorDeviceClass.ENUM, translation_key=ThinQProperty.STERILIZING_STATE, ), ThinQProperty.GROWTH_MODE: SensorEntityDescription( key=ThinQProperty.GROWTH_MODE, device_class=SensorDeviceClass.ENUM, translation_key=ThinQProperty.GROWTH_MODE, ), ThinQProperty.WIND_VOLUME: SensorEntityDescription( key=ThinQProperty.WIND_VOLUME, device_class=SensorDeviceClass.WIND_SPEED, translation_key=ThinQProperty.WIND_VOLUME, ), } TEMPERATURE_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { ThinQProperty.TARGET_TEMPERATURE: SensorEntityDescription( key=ThinQProperty.TARGET_TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, translation_key=ThinQProperty.TARGET_TEMPERATURE, ), ThinQProperty.DAY_TARGET_TEMPERATURE: SensorEntityDescription( key=ThinQProperty.DAY_TARGET_TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, translation_key=ThinQProperty.DAY_TARGET_TEMPERATURE, ), ThinQProperty.NIGHT_TARGET_TEMPERATURE: SensorEntityDescription( key=ThinQProperty.NIGHT_TARGET_TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, translation_key=ThinQProperty.NIGHT_TARGET_TEMPERATURE, ), ThinQProperty.TEMPERATURE_STATE: SensorEntityDescription( key=ThinQProperty.TEMPERATURE_STATE, device_class=SensorDeviceClass.ENUM, translation_key=ThinQProperty.TEMPERATURE_STATE, ), ThinQProperty.CURRENT_TEMPERATURE: SensorEntityDescription( key=ThinQProperty.CURRENT_TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, state_class=SensorStateClass.MEASUREMENT, translation_key=ThinQProperty.CURRENT_TEMPERATURE, ), ThinQPropertyEx.ROOM_AIR_CURRENT_TEMPERATURE: SensorEntityDescription( key=ThinQPropertyEx.ROOM_AIR_CURRENT_TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, translation_key=ThinQPropertyEx.ROOM_AIR_CURRENT_TEMPERATURE, ), ThinQPropertyEx.ROOM_IN_WATER_CURRENT_TEMPERATURE: SensorEntityDescription( key=ThinQPropertyEx.ROOM_IN_WATER_CURRENT_TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, translation_key=ThinQPropertyEx.ROOM_IN_WATER_CURRENT_TEMPERATURE, ), ThinQPropertyEx.ROOM_OUT_WATER_CURRENT_TEMPERATURE: SensorEntityDescription( key=ThinQPropertyEx.ROOM_OUT_WATER_CURRENT_TEMPERATURE, device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, translation_key=ThinQPropertyEx.ROOM_OUT_WATER_CURRENT_TEMPERATURE, ), } WATER_FILTER_INFO_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { ThinQProperty.USED_TIME: SensorEntityDescription( key=ThinQProperty.USED_TIME, native_unit_of_measurement=UnitOfTime.MONTHS, translation_key=ThinQProperty.USED_TIME, ), ThinQProperty.WATER_FILTER_STATE: SensorEntityDescription( key=ThinQProperty.WATER_FILTER_STATE, translation_key=ThinQProperty.WATER_FILTER_STATE, ), ThinQProperty.WATER_FILTER_1_REMAIN_PERCENT: SensorEntityDescription( key=ThinQProperty.WATER_FILTER_1_REMAIN_PERCENT, native_unit_of_measurement=UnitOfRatio.PERCENTAGE, translation_key=ThinQProperty.WATER_FILTER_1_REMAIN_PERCENT, ), ThinQProperty.WATER_FILTER_2_REMAIN_PERCENT: SensorEntityDescription( key=ThinQProperty.WATER_FILTER_2_REMAIN_PERCENT, native_unit_of_measurement=UnitOfRatio.PERCENTAGE, translation_key=ThinQProperty.WATER_FILTER_2_REMAIN_PERCENT, ), ThinQProperty.WATER_FILTER_3_REMAIN_PERCENT: SensorEntityDescription( key=ThinQProperty.WATER_FILTER_3_REMAIN_PERCENT, native_unit_of_measurement=UnitOfRatio.PERCENTAGE, translation_key=ThinQProperty.WATER_FILTER_3_REMAIN_PERCENT, ), } WATER_INFO_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { ThinQProperty.WATER_TYPE: SensorEntityDescription( key=ThinQProperty.WATER_TYPE, translation_key=ThinQProperty.WATER_TYPE, ), } ELAPSED_DAY_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { ThinQProperty.ELAPSED_DAY_STATE: SensorEntityDescription( key=ThinQProperty.ELAPSED_DAY_STATE, device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.DAYS, translation_key=ThinQProperty.ELAPSED_DAY_STATE, ), ThinQProperty.ELAPSED_DAY_TOTAL: SensorEntityDescription( key=ThinQProperty.ELAPSED_DAY_TOTAL, device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.DAYS, translation_key=ThinQProperty.ELAPSED_DAY_TOTAL, ), } TIME_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { TimerProperty.LIGHT_START: SensorEntityDescription( key=TimerProperty.LIGHT_START, device_class=SensorDeviceClass.TIMESTAMP, translation_key=TimerProperty.LIGHT_START, ), TimerProperty.ABSOLUTE_TO_START: SensorEntityDescription( key=TimerProperty.ABSOLUTE_TO_START, device_class=SensorDeviceClass.TIMESTAMP, translation_key=TimerProperty.ABSOLUTE_TO_START, ), TimerProperty.ABSOLUTE_TO_STOP: SensorEntityDescription( key=TimerProperty.ABSOLUTE_TO_STOP, device_class=SensorDeviceClass.TIMESTAMP, translation_key=TimerProperty.ABSOLUTE_TO_STOP, ), } TIMER_SENSOR_DESC: dict[ThinQProperty, SensorEntityDescription] = { TimerProperty.TOTAL: SensorEntityDescription( key=TimerProperty.TOTAL, device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.MINUTES, translation_key=TimerProperty.TOTAL, ), TimerProperty.RELATIVE_TO_START: SensorEntityDescription( key=TimerProperty.RELATIVE_TO_START, device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.MINUTES, translation_key=TimerProperty.RELATIVE_TO_START, ), TimerProperty.RELATIVE_TO_STOP: SensorEntityDescription( key=TimerProperty.RELATIVE_TO_STOP, device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.MINUTES, translation_key=TimerProperty.RELATIVE_TO_STOP, ), TimerProperty.SLEEP_TIMER_RELATIVE_TO_STOP: SensorEntityDescription( key=TimerProperty.SLEEP_TIMER_RELATIVE_TO_STOP, device_class=SensorDeviceClass.DURATION, native_unit_of_measurement=UnitOfTime.MINUTES, translation_key=TimerProperty.SLEEP_TIMER_RELATIVE_TO_STOP, ), TimerProperty.RELATIVE_TO_START_WM: SensorEntityDescription( key=TimerProperty.RELATIVE_TO_START, device_class=SensorDeviceClass.TIMESTAMP, translation_key=TimerProperty.RELATIVE_TO_START_WM, ), TimerProperty.RELATIVE_TO_STOP_WM: SensorEntityDescription( key=TimerProperty.RELATIVE_TO_STOP, device_class=SensorDeviceClass.TIMESTAMP, translation_key=TimerProperty.RELATIVE_TO_STOP_WM, ), TimerProperty.REMAIN: SensorEntityDescription( key=TimerProperty.REMAIN, device_class=SensorDeviceClass.TIMESTAMP, translation_key=TimerProperty.REMAIN, ), TimerProperty.RUNNING: SensorEntityDescription( key=TimerProperty.RUNNING, device_class=SensorDeviceClass.TIMESTAMP, translation_key=TimerProperty.RUNNING, ), } WASHER_SENSORS: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( key=ThinQProperty.CYCLE_COUNT, translation_key=ThinQProperty.CYCLE_COUNT, ), RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE], TIMER_SENSOR_DESC[TimerProperty.TOTAL], TIMER_SENSOR_DESC[TimerProperty.RELATIVE_TO_START_WM], TIMER_SENSOR_DESC[TimerProperty.RELATIVE_TO_STOP_WM], TIMER_SENSOR_DESC[TimerProperty.REMAIN], ) DEVICE_TYPE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] = { DeviceType.AIR_CONDITIONER: ( AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM1], AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM2], AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM10], AIR_QUALITY_SENSOR_DESC[ThinQProperty.HUMIDITY], AIR_QUALITY_SENSOR_DESC[ThinQProperty.ODOR_LEVEL], AIR_QUALITY_SENSOR_DESC[ThinQProperty.TOTAL_POLLUTION_LEVEL], FILTER_INFO_SENSOR_DESC[ThinQProperty.FILTER_LIFETIME], FILTER_INFO_SENSOR_DESC[ThinQProperty.FILTER_REMAIN_PERCENT], TIMER_SENSOR_DESC[TimerProperty.RELATIVE_TO_START], TIMER_SENSOR_DESC[TimerProperty.RELATIVE_TO_STOP], TIMER_SENSOR_DESC[TimerProperty.SLEEP_TIMER_RELATIVE_TO_STOP], TIME_SENSOR_DESC[TimerProperty.ABSOLUTE_TO_START], TIME_SENSOR_DESC[TimerProperty.ABSOLUTE_TO_STOP], ), DeviceType.AIR_PURIFIER_FAN: ( AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM1], AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM2], AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM10], AIR_QUALITY_SENSOR_DESC[ThinQProperty.HUMIDITY], AIR_QUALITY_SENSOR_DESC[ThinQProperty.TEMPERATURE], AIR_QUALITY_SENSOR_DESC[ThinQProperty.MONITORING_ENABLED], AIR_QUALITY_SENSOR_DESC[ThinQProperty.ODOR_LEVEL], AIR_QUALITY_SENSOR_DESC[ThinQProperty.TOTAL_POLLUTION_LEVEL], TIMER_SENSOR_DESC[TimerProperty.SLEEP_TIMER_RELATIVE_TO_STOP], TIME_SENSOR_DESC[TimerProperty.ABSOLUTE_TO_START], TIME_SENSOR_DESC[TimerProperty.ABSOLUTE_TO_STOP], ), DeviceType.AIR_PURIFIER: ( AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM1], AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM2], AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM10], AIR_QUALITY_SENSOR_DESC[ThinQProperty.HUMIDITY], AIR_QUALITY_SENSOR_DESC[ThinQProperty.MONITORING_ENABLED], AIR_QUALITY_SENSOR_DESC[ThinQProperty.ODOR_LEVEL], AIR_QUALITY_SENSOR_DESC[ThinQProperty.TOTAL_POLLUTION_LEVEL], FILTER_INFO_SENSOR_DESC[ThinQProperty.FILTER_REMAIN_PERCENT], FILTER_INFO_SENSOR_DESC[ThinQProperty.TOP_FILTER_REMAIN_PERCENT], JOB_MODE_SENSOR_DESC[ThinQProperty.CURRENT_JOB_MODE], JOB_MODE_SENSOR_DESC[ThinQProperty.PERSONALIZATION_MODE], TIME_SENSOR_DESC[TimerProperty.ABSOLUTE_TO_START], TIME_SENSOR_DESC[TimerProperty.ABSOLUTE_TO_STOP], ), DeviceType.COOKTOP: ( RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE], POWER_SENSOR_DESC[ThinQProperty.POWER_LEVEL], TIMER_SENSOR_DESC[TimerProperty.REMAIN], ), DeviceType.DEHUMIDIFIER: ( JOB_MODE_SENSOR_DESC[ThinQProperty.CURRENT_JOB_MODE], HUMIDITY_SENSOR_DESC[ThinQProperty.CURRENT_HUMIDITY], ), DeviceType.DISH_WASHER: ( DISH_WASHING_COURSE_SENSOR_DESC[ThinQProperty.CURRENT_DISH_WASHING_COURSE], PREFERENCE_SENSOR_DESC[ThinQProperty.RINSE_LEVEL], PREFERENCE_SENSOR_DESC[ThinQProperty.SOFTENING_LEVEL], RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE], TIMER_SENSOR_DESC[TimerProperty.TOTAL], TIMER_SENSOR_DESC[TimerProperty.RELATIVE_TO_START_WM], TIMER_SENSOR_DESC[TimerProperty.REMAIN], ), DeviceType.DRYER: WASHER_SENSORS, DeviceType.HOME_BREW: ( RECIPE_SENSOR_DESC[ThinQProperty.RECIPE_NAME], RECIPE_SENSOR_DESC[ThinQProperty.WORT_INFO], RECIPE_SENSOR_DESC[ThinQProperty.YEAST_INFO], RECIPE_SENSOR_DESC[ThinQProperty.HOP_OIL_INFO], RECIPE_SENSOR_DESC[ThinQProperty.HOP_OIL_CAPSULE_1], RECIPE_SENSOR_DESC[ThinQProperty.HOP_OIL_CAPSULE_2], RECIPE_SENSOR_DESC[ThinQProperty.FLAVOR_INFO], RECIPE_SENSOR_DESC[ThinQProperty.FLAVOR_CAPSULE_1], RECIPE_SENSOR_DESC[ThinQProperty.FLAVOR_CAPSULE_2], RECIPE_SENSOR_DESC[ThinQProperty.BEER_REMAIN], RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE], ELAPSED_DAY_SENSOR_DESC[ThinQProperty.ELAPSED_DAY_STATE], ELAPSED_DAY_SENSOR_DESC[ThinQProperty.ELAPSED_DAY_TOTAL], ), DeviceType.HUMIDIFIER: ( AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM1], AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM2], AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM10], AIR_QUALITY_SENSOR_DESC[ThinQProperty.HUMIDITY], AIR_QUALITY_SENSOR_DESC[ThinQProperty.TEMPERATURE], AIR_QUALITY_SENSOR_DESC[ThinQProperty.MONITORING_ENABLED], AIR_QUALITY_SENSOR_DESC[ThinQProperty.TOTAL_POLLUTION_LEVEL], TIMER_SENSOR_DESC[TimerProperty.SLEEP_TIMER_RELATIVE_TO_STOP], TIME_SENSOR_DESC[TimerProperty.ABSOLUTE_TO_START], TIME_SENSOR_DESC[TimerProperty.ABSOLUTE_TO_STOP], ), DeviceType.KIMCHI_REFRIGERATOR: ( REFRIGERATION_SENSOR_DESC[ThinQProperty.FRESH_AIR_FILTER], ), DeviceType.MICROWAVE_OVEN: (RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE],), DeviceType.OVEN: ( RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE], TEMPERATURE_SENSOR_DESC[ThinQProperty.TARGET_TEMPERATURE], ), DeviceType.PLANT_CULTIVATOR: ( LIGHT_SENSOR_DESC[ThinQProperty.BRIGHTNESS], LIGHT_SENSOR_DESC[ThinQProperty.DURATION], RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE], RUN_STATE_SENSOR_DESC[ThinQProperty.GROWTH_MODE], RUN_STATE_SENSOR_DESC[ThinQProperty.WIND_VOLUME], TEMPERATURE_SENSOR_DESC[ThinQProperty.DAY_TARGET_TEMPERATURE], TEMPERATURE_SENSOR_DESC[ThinQProperty.NIGHT_TARGET_TEMPERATURE], TEMPERATURE_SENSOR_DESC[ThinQProperty.TEMPERATURE_STATE], TIME_SENSOR_DESC[TimerProperty.LIGHT_START], ), DeviceType.REFRIGERATOR: ( REFRIGERATION_SENSOR_DESC[ThinQProperty.FRESH_AIR_FILTER], REFRIGERATION_SENSOR_DESC[ThinQProperty.FRESH_AIR_FILTER_REMAIN_PERCENT], WATER_FILTER_INFO_SENSOR_DESC[ThinQProperty.USED_TIME], WATER_FILTER_INFO_SENSOR_DESC[ThinQProperty.WATER_FILTER_STATE], WATER_FILTER_INFO_SENSOR_DESC[ThinQProperty.WATER_FILTER_1_REMAIN_PERCENT], WATER_FILTER_INFO_SENSOR_DESC[ThinQProperty.WATER_FILTER_2_REMAIN_PERCENT], WATER_FILTER_INFO_SENSOR_DESC[ThinQProperty.WATER_FILTER_3_REMAIN_PERCENT], ), DeviceType.ROBOT_CLEANER: ( RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE], JOB_MODE_SENSOR_DESC[ThinQProperty.CURRENT_JOB_MODE], TIMER_SENSOR_DESC[TimerProperty.RUNNING], TIME_SENSOR_DESC[TimerProperty.ABSOLUTE_TO_START], ), DeviceType.STICK_CLEANER: ( BATTERY_SENSOR_DESC[ThinQProperty.BATTERY_PERCENT], JOB_MODE_SENSOR_DESC[ThinQPropertyEx.CURRENT_JOB_MODE_STICK_CLEANER], RUN_STATE_SENSOR_DESC[ThinQProperty.CURRENT_STATE], ), DeviceType.STYLER: WASHER_SENSORS, DeviceType.SYSTEM_BOILER: ( TEMPERATURE_SENSOR_DESC[ThinQPropertyEx.ROOM_AIR_CURRENT_TEMPERATURE], TEMPERATURE_SENSOR_DESC[ThinQPropertyEx.ROOM_IN_WATER_CURRENT_TEMPERATURE], TEMPERATURE_SENSOR_DESC[ThinQPropertyEx.ROOM_OUT_WATER_CURRENT_TEMPERATURE], ), DeviceType.VENTILATOR: ( AIR_QUALITY_SENSOR_DESC[ThinQProperty.CO2], AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM1], AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM2], AIR_QUALITY_SENSOR_DESC[ThinQProperty.PM10], TEMPERATURE_SENSOR_DESC[ThinQProperty.CURRENT_TEMPERATURE], TIME_SENSOR_DESC[TimerProperty.ABSOLUTE_TO_START], TIME_SENSOR_DESC[TimerProperty.ABSOLUTE_TO_STOP], TIMER_SENSOR_DESC[TimerProperty.SLEEP_TIMER_RELATIVE_TO_STOP], ), DeviceType.WASHCOMBO_MAIN: WASHER_SENSORS, DeviceType.WASHCOMBO_MINI: WASHER_SENSORS, DeviceType.WASHER: WASHER_SENSORS, DeviceType.WASHTOWER_DRYER: WASHER_SENSORS, DeviceType.WASHTOWER: WASHER_SENSORS, DeviceType.WASHTOWER_WASHER: WASHER_SENSORS, DeviceType.WATER_HEATER: ( TEMPERATURE_SENSOR_DESC[ThinQProperty.CURRENT_TEMPERATURE], ), DeviceType.WATER_PURIFIER: ( RUN_STATE_SENSOR_DESC[ThinQProperty.COCK_STATE], RUN_STATE_SENSOR_DESC[ThinQProperty.STERILIZING_STATE], WATER_INFO_SENSOR_DESC[ThinQProperty.WATER_TYPE], ), } ENUM_TEMPERATURE_SENSOR_MAP: dict[DeviceType, tuple[SensorEntityDescription, ...]] = { DeviceType.KIMCHI_REFRIGERATOR: ( SensorEntityDescription( key=ThinQProperty.TARGET_TEMPERATURE, native_unit_of_measurement=UnitOfTemperature.CELSIUS, translation_key=ThinQProperty.TARGET_TEMPERATURE, ), ), } @dataclass(frozen=True, kw_only=True) class ThinQEnergySensorEntityDescription(SensorEntityDescription): """Describes ThinQ energy sensor entity.""" device_class = SensorDeviceClass.ENERGY state_class = SensorStateClass.TOTAL native_unit_of_measurement = UnitOfEnergy.WATT_HOUR suggested_display_precision = 0 usage_period: str start_date_fn: Callable[[datetime], datetime] end_date_fn: Callable[[datetime], datetime] update_interval: timedelta = timedelta(days=1) reset_at_midnight: bool = False ENERGY_USAGE_SENSORS: tuple[ThinQEnergySensorEntityDescription, ...] = ( ThinQEnergySensorEntityDescription( key="today", translation_key="energy_usage_today", usage_period=USAGE_DAILY, start_date_fn=lambda today: today, end_date_fn=lambda today: today, update_interval=timedelta(hours=1), reset_at_midnight=True, ), ThinQEnergySensorEntityDescription( key="yesterday", translation_key="energy_usage_yesterday", usage_period=USAGE_DAILY, start_date_fn=lambda today: today - timedelta(days=1), end_date_fn=lambda today: today - timedelta(days=1), ), ThinQEnergySensorEntityDescription( key="this_month", translation_key="energy_usage_this_month", usage_period=USAGE_MONTHLY, start_date_fn=lambda today: today, end_date_fn=lambda today: today, state_class=SensorStateClass.TOTAL_INCREASING, ), ThinQEnergySensorEntityDescription( key="last_month", translation_key="energy_usage_last_month", usage_period=USAGE_MONTHLY, start_date_fn=lambda today: today.replace(day=1) - timedelta(days=1), end_date_fn=lambda today: today.replace(day=1) - timedelta(days=1), ), ) _LOGGER = logging.getLogger(__name__) async def async_setup_entry( hass: HomeAssistant, entry: ThinqConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up an entry for sensor platform.""" entities: list[ ThinQSensorEntity | ThinQEnergySensorEntity | ThinQEnumTempSensorEntity ] = [] for coordinator in entry.runtime_data.coordinators.values(): if ( descriptions := DEVICE_TYPE_SENSOR_MAP.get( coordinator.api.device.device_type ) ) is not None: for description in descriptions: entities.extend( ThinQSensorEntity(coordinator, description, property_id) for property_id in coordinator.api.get_active_idx( description.key, ( ActiveMode.READABLE if ( coordinator.api.device.device_type == DeviceType.COOKTOP or isinstance(description.key, TimerProperty) ) else ActiveMode.READ_ONLY ), ) ) if ( descriptions := ENUM_TEMPERATURE_SENSOR_MAP.get( coordinator.api.device.device_type ) ) is not None: for description in descriptions: entities.extend( ThinQEnumTempSensorEntity(coordinator, description, property_id) for property_id in coordinator.api.get_active_idx( description.key, ActiveMode.READ_ONLY, ) ) for energy_description in ENERGY_USAGE_SENSORS: entities.extend( ThinQEnergySensorEntity( coordinator=coordinator, entity_description=energy_description, property_id=energy_property_id, postfix_id=energy_description.key, ) for energy_property_id in coordinator.api.get_active_idx( ( ThinQPropertyEx.ENERGY_USAGE if coordinator.sub_id is None else f"{ThinQPropertyEx.ENERGY_USAGE}_{coordinator.sub_id}" ), ActiveMode.READ_ONLY, ) ) if entities: async_add_entities(entities) class ThinQSensorEntity(ThinQEntity, SensorEntity): """Represent a thinq sensor platform.""" def __init__( self, coordinator: DeviceDataUpdateCoordinator, entity_description: SensorEntityDescription, property_id: str, ) -> None: """Initialize a sensor entity.""" super().__init__(coordinator, entity_description, property_id) if entity_description.device_class == SensorDeviceClass.ENUM: self._attr_options = self.data.options self._device_state: str | None = None self._device_state_id = ( ThinQProperty.CURRENT_STATE if self.location is None else f"{self.location}_{ThinQProperty.CURRENT_STATE}" ) @override def _update_status(self) -> None: """Update status itself.""" super()._update_status() value = self.data.value if isinstance(value, time): local_now = dt_util.now() self._device_state = ( self.coordinator.data[self._device_state_id].value if self._device_state_id in self.coordinator.data else None ) if value in [0, None, time.min] or ( self._device_state == "power_off" and self.entity_description.key in [TimerProperty.REMAIN, TimerProperty.TOTAL] ): # Reset to None when power_off value = None elif self.entity_description.device_class == SensorDeviceClass.TIMESTAMP: if self.entity_description.key in TIME_SENSOR_DESC: # Set timestamp for absolute time value = local_now.replace(hour=value.hour, minute=value.minute) else: # Set timestamp for delta event_data = timedelta( hours=value.hour, minutes=value.minute, seconds=value.second ) new_time = ( (local_now - event_data) if self.entity_description.key == TimerProperty.RUNNING else (local_now + event_data) ) # The remain_time may change during the # wash/dry operation depending on # various reasons. # If there is a diff of more than 60sec, the new timestamp is used if ( parse_native_value := dt_util.parse_datetime( str(self.native_value) ) ) is None or abs(new_time - parse_native_value) > timedelta( seconds=60 ): value = new_time else: value = self.native_value elif self.entity_description.device_class == SensorDeviceClass.DURATION: # Set duration value = self._get_duration( value, self.entity_description.native_unit_of_measurement ) self._attr_native_value = value if (data_unit := self._get_unit_of_measurement(self.data.unit)) is not None: # For different from description's unit self._attr_native_unit_of_measurement = data_unit _LOGGER.debug( "[%s:%s] update status: %s -> %s, options:%s, unit:%s", self.coordinator.device_name, self.property_id, self.data.value, self.native_value, self.options, self.native_unit_of_measurement, ) def _get_duration(self, data: time, unit: str | None) -> float | None: if unit == UnitOfTime.MINUTES: return (data.hour * 60) + data.minute if unit == UnitOfTime.SECONDS: return (data.hour * 3600) + (data.minute * 60) + data.second return 0 class ThinQEnergySensorEntity(ThinQEntity, SensorEntity): """Represent a ThinQ energy sensor platform.""" entity_description: ThinQEnergySensorEntityDescription _stop_update: Callable[[], None] | None = None _last_fetch_date: date | None = None @override async def async_added_to_hass(self) -> None: """Handle added to Hass.""" await super().async_added_to_hass() if self.coordinator.update_energy_at_time_of_day is None: # random time 01:00:00 ~ 02:59:00 self.coordinator.update_energy_at_time_of_day = time( hour=random.randint(1, 2), minute=random.randint(0, 59) ) _LOGGER.debug( "[%s] Set energy update time: %s", self.coordinator.device_name, self.coordinator.update_energy_at_time_of_day, ) await self._async_update_and_schedule() @override async def async_will_remove_from_hass(self) -> None: """Run when entity will be removed from hass.""" if self._stop_update is not None: self._stop_update() return await super().async_will_remove_from_hass() @property @override def available(self) -> bool: """Return True if entity is available.""" return super().available or self.native_value is not None @override async def async_update(self, now: datetime | None = None) -> None: """Update the state of the sensor.""" await self._async_update_and_schedule() self.async_write_ha_state() async def _async_update_and_schedule(self) -> None: """Update the state of the sensor.""" local_now = dt_util.now() next_update = local_now + self.entity_description.update_interval if ( self.coordinator.update_energy_at_time_of_day is not None and self.entity_description.update_interval >= timedelta(days=1) ): # For daily sensors: align to a consistent time of day to avoid # clock drift and reduce the chance of multiple sensors fetching # simultaneously across restarts. next_update = datetime.combine( next_update.date(), self.coordinator.update_energy_at_time_of_day, next_update.tzinfo, ) start_date = (self.entity_description.start_date_fn(local_now)).date() end_date = (self.entity_description.end_date_fn(local_now)).date() try: self._attr_native_value = await self.coordinator.api.async_get_energy_usage( energy_property=self.property_id, period=self.entity_description.usage_period, start_date=start_date, end_date=end_date, detail=False, ) if ( self.entity_description.reset_at_midnight and start_date != self._last_fetch_date ): self._attr_last_reset = datetime.combine( start_date, time.min, local_now.tzinfo ) self._last_fetch_date = start_date except ThinQAPIException as exc: _LOGGER.warning( "[%s:%s] Failed to fetch energy usage data. reason=%s", self.coordinator.device_name, self.entity_description.key, exc, ) finally: _LOGGER.debug( "[%s:%s] async_update_and_schedule next_update: %s, native_value: %s", self.coordinator.device_name, self.entity_description.key, next_update, self._attr_native_value, ) self._stop_update = async_track_point_in_time( self.coordinator.hass, self.async_update, next_update, ) class ThinQEnumTempSensorEntity(ThinQEntity, SensorEntity): """Represent a thinq sensor platform.""" def __init__( self, coordinator: DeviceDataUpdateCoordinator, entity_description: SensorEntityDescription, property_id: str, ) -> None: """Initialize a sensor entity.""" super().__init__(coordinator, entity_description, property_id) if self.data.options: # some kimchi refrigerator's target temperature # have data in the form of string with enum # options. # Set options to display the correct value in the UI. self._attr_options = self.data.options self._attr_device_class = SensorDeviceClass.ENUM self._attr_native_unit_of_measurement = None @override def _update_status(self) -> None: """Update status itself.""" super()._update_status() self._attr_native_value = self.data.value _LOGGER.debug( "[%s:%s] update status: %s -> %s, options:%s, unit:%s", self.coordinator.device_name, self.property_id, self.data.value, self.native_value, self.options, self.native_unit_of_measurement, )