"""Climate entities proxied through the standalone GREE Controller.""" from __future__ import annotations import logging from typing import Any from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( SWING_OFF, SWING_ON, ClimateEntityFeature, HVACMode, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryError from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from . import GreeControllerRuntimeData from .const import DOMAIN from .coordinator import GreeControllerCoordinator _LOGGER = logging.getLogger(__name__) MODE_TO_HA = { "auto": HVACMode.AUTO, "cool": HVACMode.COOL, "dry": HVACMode.DRY, "fan": HVACMode.FAN_ONLY, "heat": HVACMode.HEAT, } HA_TO_MODE = {value: key for key, value in MODE_TO_HA.items()} FAN_TO_NAME = {0: "auto", 1: "low", 2: "medium_low", 3: "medium", 4: "medium_high", 5: "high"} NAME_TO_FAN = {value: key for key, value in FAN_TO_NAME.items()} async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Create climate entities for all devices exposed by the controller.""" runtime: GreeControllerRuntimeData = entry.runtime_data registry = er.async_get(hass) entities: list[GreeControllerClimate] = [] for device_id, device in runtime.coordinator.data.items(): desired_entity_id = runtime.entity_map.get(device_id) unique_id = f"{device_id}-climate" if desired_entity_id: if not desired_entity_id.startswith("climate."): raise ConfigEntryError(f"Mapped entity ID must use the climate domain: {desired_entity_id}") existing = registry.async_get(desired_entity_id) if existing and not (existing.platform == DOMAIN and existing.unique_id == unique_id): raise ConfigEntryError( f"Entity ID {desired_entity_id} is still reserved by integration {existing.platform}. " "Disable/remove the previous GREE integration and remove its entity registry entry before takeover." ) if hass.states.get(desired_entity_id) is not None and existing is None: raise ConfigEntryError( f"Entity ID {desired_entity_id} is still active in Home Assistant. " "Unload the previous integration before takeover." ) entities.append( GreeControllerClimate(runtime.coordinator, device_id, desired_entity_id) ) async_add_entities(entities) class GreeControllerClimate(CoordinatorEntity[GreeControllerCoordinator], ClimateEntity): """Home Assistant climate entity controlled through the Rust service.""" _attr_has_entity_name = True _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_min_temp = 8.0 _attr_max_temp = 32.0 _attr_target_temperature_step = 0.5 _attr_hvac_modes = [HVACMode.OFF, HVACMode.AUTO, HVACMode.COOL, HVACMode.HEAT, HVACMode.DRY, HVACMode.FAN_ONLY] _attr_fan_modes = list(NAME_TO_FAN) _attr_swing_modes = [SWING_OFF, SWING_ON] _attr_swing_horizontal_modes = [SWING_OFF, SWING_ON] _attr_supported_features = ( ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.FAN_MODE | ClimateEntityFeature.SWING_MODE | ClimateEntityFeature.SWING_HORIZONTAL_MODE | ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF ) def __init__( self, coordinator: GreeControllerCoordinator, device_id: str, requested_entity_id: str | None, ) -> None: super().__init__(coordinator) self._device_id = device_id self._attr_unique_id = f"{device_id}-climate" self._attr_name = None if requested_entity_id: # This is intentionally limited to same-domain takeover migrations. # The setup guard above prevents accidental collisions. self.entity_id = requested_entity_id @property def _device(self) -> dict[str, Any]: return self.coordinator.data.get(self._device_id, {}) @property def available(self) -> bool: return super().available and bool(self._device.get("online", False)) @property def device_info(self) -> DeviceInfo: device = self._device return DeviceInfo( identifiers={(DOMAIN, self._device_id)}, name=str(device.get("name") or self._device_id), manufacturer="GREE", model=str(device.get("model") or "GREE HVAC"), sw_version=str(device.get("firmware") or "") or None, ) @property def current_temperature(self) -> float | None: value = self._device.get("current_temperature") return float(value) if value is not None else None @property def target_temperature(self) -> float | None: value = self._device.get("target_temperature") return float(value) if value is not None else None @property def hvac_mode(self) -> HVACMode: device = self._device if not device.get("power", False): return HVACMode.OFF return MODE_TO_HA.get(str(device.get("mode", "auto")), HVACMode.AUTO) @property def fan_mode(self) -> str: return FAN_TO_NAME.get(int(self._device.get("fan_speed", 0)), "auto") @property def swing_mode(self) -> str: return SWING_ON if self._device.get("swing_vertical", False) else SWING_OFF @property def swing_horizontal_mode(self) -> str: return SWING_ON if self._device.get("swing_horizontal", False) else SWING_OFF @property def extra_state_attributes(self) -> dict[str, Any]: device = self._device return { "controller_device_id": self._device_id, "controller_online": bool(device.get("online", False)), "quiet": bool(device.get("quiet", False)), "turbo": bool(device.get("turbo", False)), "light": bool(device.get("light", False)), "last_seen": device.get("last_seen"), } async def _command(self, payload: dict[str, Any]) -> None: await self.coordinator.client.command(self._device_id, payload) await self.coordinator.async_request_refresh() async def async_turn_on(self) -> None: await self._command({"power": True}) async def async_turn_off(self) -> None: await self._command({"power": False}) async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: if hvac_mode == HVACMode.OFF: await self._command({"power": False}) return mode = HA_TO_MODE.get(hvac_mode) if mode is None: return await self._command({"power": True, "mode": mode}) async def async_set_temperature(self, **kwargs: Any) -> None: temperature = kwargs.get(ATTR_TEMPERATURE) if temperature is not None: await self._command({"target_temperature": float(temperature)}) async def async_set_fan_mode(self, fan_mode: str) -> None: if fan_mode in NAME_TO_FAN: await self._command({"fan_speed": NAME_TO_FAN[fan_mode]}) async def async_set_swing_mode(self, swing_mode: str) -> None: await self._command({"swing_vertical": swing_mode == SWING_ON}) async def async_set_swing_horizontal_mode(self, swing_horizontal_mode: str) -> None: await self._command({"swing_horizontal": swing_horizontal_mode == SWING_ON})