import logging from dataclasses import dataclass from datetime import datetime, timedelta from typing import Any from homeassistant.components.switch import SwitchEntityDescription, SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback from homeassistant.helpers.entity import EntityCategory from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import HomeAssistantType from pyhon.parameter.base import HonParameter from pyhon.parameter.range import HonParameterRange from .const import DOMAIN from .hon import HonEntity, unique_entities _LOGGER = logging.getLogger(__name__) @dataclass(frozen=True) class HonControlSwitchEntityDescription(SwitchEntityDescription): turn_on_key: str = "" turn_off_key: str = "" only_mandatory_parameters: bool = False on_value: bool | float = True off_value: bool | float = False to_sync: bool = False @dataclass(frozen=True) class HonSwitchEntityDescription(SwitchEntityDescription): pass @dataclass(frozen=True) class HonConfigSwitchEntityDescription(SwitchEntityDescription): entity_category: EntityCategory = EntityCategory.CONFIG SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = { "WM": ( HonControlSwitchEntityDescription( key="active", name="Washing Machine", icon="mdi:washing-machine", turn_on_key="startProgram", turn_off_key="stopProgram", translation_key="washing_machine", ), HonControlSwitchEntityDescription( key="pause", name="Pause Washing Machine", icon="mdi:pause", turn_on_key="pauseProgram", turn_off_key="resumeProgram", translation_key="pause", ), HonConfigSwitchEntityDescription( key="startProgram.delayStatus", name="Delay Status", icon="mdi:timer-check", translation_key="delay_time", ), HonConfigSwitchEntityDescription( key="startProgram.haier_SoakPrewashSelection", name="Soak Prewash Selection", icon="mdi:tshirt-crew", translation_key="prewash", ), HonConfigSwitchEntityDescription( key="startProgram.prewash", name="Prewash", icon="mdi:tshirt-crew", translation_key="prewash", ), HonConfigSwitchEntityDescription( key="startProgram.permanentPressStatus", name="Keep Fresh", icon="mdi:refresh-circle", translation_key="keep_fresh", ), HonConfigSwitchEntityDescription( key="startProgram.autoSoftenerStatus", name="Auto Dose Softener", icon="mdi:teddy-bear", translation_key="auto_dose_softener", ), HonConfigSwitchEntityDescription( key="startProgram.autoDetergentStatus", name="Auto Dose Detergent", icon="mdi:cup", translation_key="auto_dose_detergent", ), HonSwitchEntityDescription( key="autoSoftenerStatus", name="Auto Dose Softener", icon="mdi:teddy-bear", translation_key="auto_dose_softener", ), HonSwitchEntityDescription( key="autoDetergentStatus", name="Auto Dose Detergent", icon="mdi:cup", translation_key="auto_dose_detergent", ), HonConfigSwitchEntityDescription( key="startProgram.acquaplus", name="Acqua Plus", icon="mdi:water-plus", translation_key="acqua_plus", ), HonConfigSwitchEntityDescription( key="startProgram.extraRinse1", name="Extra Rinse 1", icon="mdi:numeric-1-box-multiple-outline", translation_key="extra_rinse_1", ), HonConfigSwitchEntityDescription( key="startProgram.extraRinse2", name="Extra Rinse 2", icon="mdi:numeric-2-box-multiple-outline", translation_key="extra_rinse_2", ), HonConfigSwitchEntityDescription( key="startProgram.extraRinse3", name="Extra Rinse 3", icon="mdi:numeric-3-box-multiple-outline", translation_key="extra_rinse_3", ), HonConfigSwitchEntityDescription( key="startProgram.goodNight", name="Good Night", icon="mdi:weather-night", translation_key="good_night", ), HonConfigSwitchEntityDescription( key="startProgram.hygiene", name="Hygiene", icon="mdi:lotion-plus", translation_key="hygiene", ), HonConfigSwitchEntityDescription( key="startProgram.anticrease", name="Anti-Crease", icon="mdi:iron", translation_key="anti_crease", ), ), "TD": ( HonControlSwitchEntityDescription( key="active", name="Tumble Dryer", icon="mdi:tumble-dryer", turn_on_key="startProgram", turn_off_key="stopProgram", translation_key="tumble_dryer", ), HonControlSwitchEntityDescription( key="pause", name="Pause Tumble Dryer", icon="mdi:pause", turn_on_key="pauseProgram", turn_off_key="resumeProgram", translation_key="pause", ), HonConfigSwitchEntityDescription( key="startProgram.sterilizationStatus", name="Sterilization", icon="mdi:lotion-plus", ), HonConfigSwitchEntityDescription( key="startProgram.tumblingStatus", name="Tumbling", icon="mdi:refresh-circle", translation_key="keep_fresh", ), HonConfigSwitchEntityDescription( key="startProgram.antiCreaseTime", name="Anti-Crease", icon="mdi:iron", translation_key="anti_crease", ), HonConfigSwitchEntityDescription( key="startProgram.anticrease", name="Anti-Crease", icon="mdi:iron", translation_key="anti_crease", ), ), "OV": ( HonControlSwitchEntityDescription( key="active", name="Oven", icon="mdi:toaster-oven", turn_on_key="startProgram", turn_off_key="stopProgram", translation_key="oven", ), HonConfigSwitchEntityDescription( key="startProgram.preheatStatus", name="Preheat", icon="mdi:thermometer-chevron-up", translation_key="preheat", ), ), "WD": ( HonControlSwitchEntityDescription( key="active", name="Washer Dryer", icon="mdi:washing-machine", turn_on_key="startProgram", turn_off_key="stopProgram", translation_key="washer_dryer", ), HonControlSwitchEntityDescription( key="pause", name="Pause Washer Dryer", icon="mdi:pause", turn_on_key="pauseProgram", turn_off_key="resumeProgram", translation_key="pause", ), ), "DW": ( HonControlSwitchEntityDescription( key="active", name="Dish Washer", icon="mdi:dishwasher", turn_on_key="startProgram", turn_off_key="stopProgram", translation_key="dish_washer", ), HonConfigSwitchEntityDescription( key="startProgram.extraDry", name="Extra Dry", icon="mdi:hair-dryer", translation_key="extra_dry", ), HonConfigSwitchEntityDescription( key="startProgram.halfLoad", name="Half Load", icon="mdi:fraction-one-half", translation_key="half_load", ), HonConfigSwitchEntityDescription( key="startProgram.openDoor", name="Open Door", icon="mdi:door-open", translation_key="open_door", ), HonConfigSwitchEntityDescription( key="startProgram.threeInOne", name="Three in One", icon="mdi:numeric-3-box-outline", translation_key="three_in_one", ), HonConfigSwitchEntityDescription( key="startProgram.ecoExpress", name="Eco Express", icon="mdi:sprout", translation_key="eco", ), HonConfigSwitchEntityDescription( key="startProgram.addDish", name="Add Dish", icon="mdi:silverware-fork-knife", translation_key="add_dish", ), HonSwitchEntityDescription( key="buzzerDisabled", name="Buzzer Disabled", icon="mdi:volume-off", translation_key="buzzer", ), ), "AC": ( HonSwitchEntityDescription( key="10degreeHeatingStatus", name="10° Heating", icon="mdi:heat-wave", translation_key="10_degree_heating", ), HonSwitchEntityDescription( key="echoStatus", name="Echo", icon="mdi:account-voice", ), HonSwitchEntityDescription( key="ecoMode", name="Eco Mode", icon="mdi:sprout", translation_key="eco_mode", ), HonSwitchEntityDescription( key="healthMode", name="Health Mode", icon="mdi:medication-outline", ), HonSwitchEntityDescription( key="muteStatus", name="Silent Mode", icon="mdi:volume-off", translation_key="silent_mode", ), HonSwitchEntityDescription( key="rapidMode", name="Rapid Mode", icon="mdi:run-fast", translation_key="rapid_mode", ), HonSwitchEntityDescription( key="screenDisplayStatus", name="Screen Display", icon="mdi:monitor-small", ), HonSwitchEntityDescription( key="selfCleaning56Status", name="Self Cleaning 56", icon="mdi:air-filter", translation_key="self_clean_56", ), HonSwitchEntityDescription( key="selfCleaningStatus", name="Self Cleaning", icon="mdi:air-filter", translation_key="self_clean", ), HonSwitchEntityDescription( key="silentSleepStatus", name="Night Mode", icon="mdi:bed", translation_key="night_mode", ), ), "REF": ( HonSwitchEntityDescription( key="intelligenceMode", name="Auto-Set Mode", icon="mdi:thermometer-auto", translation_key="auto_set", ), HonSwitchEntityDescription( key="quickModeZ2", name="Super Freeze", icon="mdi:snowflake-variant", translation_key="super_freeze", ), HonSwitchEntityDescription( key="quickModeZ1", name="Super Cool", icon="mdi:snowflake", translation_key="super_cool", ), ), "WC": ( HonSwitchEntityDescription( key="sabbathStatus", name="Sabbath Mode", icon="mdi:palm-tree", translation_key="holiday_mode", ), ), "HO": ( HonControlSwitchEntityDescription( key="onOffStatus", name="Hood", icon="mdi:hvac", turn_on_key="startProgram", turn_off_key="stopProgram", translation_key="hood", ), ), "AP": ( HonSwitchEntityDescription( key="touchToneStatus", name="Touch Tone", icon="mdi:account-voice", translation_key="touch_tone", ), ), "WH": ( HonControlSwitchEntityDescription( key="onOffStatus", name="Power", icon="mdi:power-standby", turn_on_key="startProgram", turn_off_key="stopProgram", translation_key="power", only_mandatory_parameters=True, on_value=1, off_value=0, to_sync=True, ), ), } SWITCHES["WD"] = unique_entities(SWITCHES["WD"], SWITCHES["WM"]) SWITCHES["WD"] = unique_entities(SWITCHES["WD"], SWITCHES["TD"]) async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: entities = [] entity: HonConfigSwitchEntity | HonControlSwitchEntity | HonSwitchEntity for device in hass.data[DOMAIN][entry.unique_id].appliances: for description in SWITCHES.get(device.appliance_type, []): if isinstance(description, HonConfigSwitchEntityDescription): if description.key not in device.available_settings: continue entity = HonConfigSwitchEntity(hass, entry, device, description) elif isinstance(description, HonControlSwitchEntityDescription): if not ( device.get(description.key) is not None or description.turn_on_key in list(device.commands) or description.turn_off_key in list(device.commands) ): continue entity = HonControlSwitchEntity(hass, entry, device, description) elif isinstance(description, HonSwitchEntityDescription): if f"settings.{description.key}" not in device.available_settings: continue entity = HonSwitchEntity(hass, entry, device, description) else: continue await entity.coordinator.async_config_entry_first_refresh() entities.append(entity) async_add_entities(entities) class HonSwitchEntity(HonEntity, SwitchEntity): entity_description: HonSwitchEntityDescription @property def is_on(self) -> bool | None: """Return True if entity is on.""" return self._device.get(self.entity_description.key, 0) == 1 async def async_turn_on(self, **kwargs: Any) -> None: setting = self._device.settings[f"settings.{self.entity_description.key}"] if type(setting) == HonParameter: return setting.value = setting.max if isinstance(setting, HonParameterRange) else 1 self.async_write_ha_state() await self._device.commands["settings"].send() await self.coordinator.async_refresh() async def async_turn_off(self, **kwargs: Any) -> None: setting = self._device.settings[f"settings.{self.entity_description.key}"] if type(setting) == HonParameter: return setting.value = setting.min if isinstance(setting, HonParameterRange) else 0 self.async_write_ha_state() await self._device.commands["settings"].send() await self.coordinator.async_refresh() @property def available(self) -> bool: """Return True if entity is available.""" if not super().available: return False if not self._device.get("remoteCtrValid", 1) == 1: return False if self._device.get("attributes.lastConnEvent.category") == "DISCONNECTED": return False setting = self._device.settings[f"settings.{self.entity_description.key}"] if isinstance(setting, HonParameterRange) and len(setting.values) < 2: return False return True @callback def _handle_coordinator_update(self, update: bool = True) -> None: self._attr_is_on = self.is_on if update: self.async_write_ha_state() class HonControlSwitchEntity(HonEntity, SwitchEntity): entity_description: HonControlSwitchEntityDescription @property def is_on(self) -> bool | None: """Return True if entity is on.""" on_value = self.entity_description.on_value off_value = self.entity_description.off_value return self._device.get(self.entity_description.key, off_value) == on_value async def async_turn_on(self, **kwargs: Any) -> None: desc = self.entity_description self._device.sync_command(desc.turn_on_key, "settings", desc.to_sync) await self.coordinator.async_refresh() command = self._device.commands[desc.turn_on_key] await command.send(desc.only_mandatory_parameters) self._device.attributes[desc.key] = desc.on_value self.async_write_ha_state() async def async_turn_off(self, **kwargs: Any) -> None: desc = self.entity_description self._device.sync_command(desc.turn_off_key, "settings", desc.to_sync) await self.coordinator.async_refresh() command = self._device.commands[desc.turn_off_key] await command.send(desc.only_mandatory_parameters) self._device.attributes[desc.key] = desc.off_value self.async_write_ha_state() @property def available(self) -> bool: """Return True if entity is available.""" return ( super().available and int(self._device.get("remoteCtrValid", 1)) == 1 and self._device.get("attributes.lastConnEvent.category") != "DISCONNECTED" ) @property def extra_state_attributes(self) -> dict[str, Any]: """Return the optional state attributes.""" result = {} if remaining_time := self._device.get("remainingTimeMM", 0): delay_time = self._device.get("delayTime", 0) result["start_time"] = datetime.now() + timedelta(minutes=delay_time) result["end_time"] = datetime.now() + timedelta( minutes=delay_time + remaining_time ) return result class HonConfigSwitchEntity(HonEntity, SwitchEntity): entity_description: HonConfigSwitchEntityDescription @property def is_on(self) -> bool | None: """Return True if entity is on.""" setting = self._device.settings[self.entity_description.key] return ( setting.value != setting.min if hasattr(setting, "min") else setting.value == "1" ) async def async_turn_on(self, **kwargs: Any) -> None: setting = self._device.settings[self.entity_description.key] if type(setting) == HonParameter: return setting.value = setting.max if isinstance(setting, HonParameterRange) else "1" self.async_write_ha_state() await self.coordinator.async_refresh() async def async_turn_off(self, **kwargs: Any) -> None: setting = self._device.settings[self.entity_description.key] if type(setting) == HonParameter: return setting.value = setting.min if isinstance(setting, HonParameterRange) else "0" self.async_write_ha_state() await self.coordinator.async_refresh() @callback def _handle_coordinator_update(self, update: bool = True) -> None: self._attr_is_on = self.is_on if update: self.async_write_ha_state()