From f5fef16809ef5e6d11c9a71189d7706a8caa8caf Mon Sep 17 00:00:00 2001 From: klasek Date: Sat, 7 Feb 2026 16:00:19 +0100 Subject: [PATCH] upgrade aioaquarea --- custom_components/panasonic_cc/__init__.py | 85 ++++-- custom_components/panasonic_cc/base.py | 14 +- custom_components/panasonic_cc/button.py | 48 ++- custom_components/panasonic_cc/climate.py | 274 ++++++++++++------ custom_components/panasonic_cc/config_flow.py | 106 ++++--- custom_components/panasonic_cc/const.py | 42 +-- custom_components/panasonic_cc/coordinator.py | 240 ++++++++++----- custom_components/panasonic_cc/manifest.json | 2 +- custom_components/panasonic_cc/number.py | 38 ++- custom_components/panasonic_cc/select.py | 63 ++-- custom_components/panasonic_cc/sensor.py | 169 +++++++---- custom_components/panasonic_cc/switch.py | 120 +++++--- .../panasonic_cc/water_heater.py | 60 ++-- requirements.txt | 2 +- 14 files changed, 861 insertions(+), 402 deletions(-) diff --git a/custom_components/panasonic_cc/__init__.py b/custom_components/panasonic_cc/__init__.py index 62ef278..b4943ad 100644 --- a/custom_components/panasonic_cc/__init__.py +++ b/custom_components/panasonic_cc/__init__.py @@ -1,4 +1,5 @@ """Platform for the Panasonic Comfort Cloud.""" + import logging from typing import Dict @@ -8,8 +9,7 @@ from homeassistant.core import HomeAssistant from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_USERNAME, CONF_PASSWORD) +from homeassistant.const import CONF_USERNAME, CONF_PASSWORD import homeassistant.helpers.config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.loader import async_get_integration @@ -30,10 +30,14 @@ STARTUP, DATA_COORDINATORS, ENERGY_COORDINATORS, - AQUAREA_COORDINATORS) - -from .coordinator import PanasonicDeviceCoordinator, PanasonicDeviceEnergyCoordinator, AquareaDeviceCoordinator + AQUAREA_COORDINATORS, +) +from .coordinator import ( + PanasonicDeviceCoordinator, + PanasonicDeviceEnergyCoordinator, + AquareaDeviceCoordinator, +) _LOGGER = logging.getLogger(__name__) @@ -45,7 +49,10 @@ { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_ENABLE_DAILY_ENERGY_SENSOR, default=DEFAULT_ENABLE_DAILY_ENERGY_SENSOR): cv.boolean, + vol.Optional( + CONF_ENABLE_DAILY_ENERGY_SENSOR, + default=DEFAULT_ENABLE_DAILY_ENERGY_SENSOR, + ): cv.boolean, # noqa: E501 } ) @@ -55,6 +62,7 @@ AQUAREA_DEMO = False + def setup(hass, config): pass @@ -70,7 +78,6 @@ async def async_setup(hass: HomeAssistant, config: Dict) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Establish connection with Comfort Cloud.""" - conf = entry.data if PANASONIC_DEVICES not in hass.data: @@ -78,26 +85,40 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): username = conf[CONF_USERNAME] password = conf[CONF_PASSWORD] - enable_daily_energy_sensor = entry.options.get(CONF_ENABLE_DAILY_ENERGY_SENSOR, DEFAULT_ENABLE_DAILY_ENERGY_SENSOR) - + enable_daily_energy_sensor = entry.options.get( + CONF_ENABLE_DAILY_ENERGY_SENSOR, DEFAULT_ENABLE_DAILY_ENERGY_SENSOR + ) + client = async_get_clientsession(hass) api = ApiClient(username, password, client) await api.start_session() devices = api.get_devices() - - if CONF_UPDATE_INTERVAL_VERSION not in conf or conf[CONF_UPDATE_INTERVAL_VERSION] < 2: + + if ( + CONF_UPDATE_INTERVAL_VERSION not in conf + or conf[CONF_UPDATE_INTERVAL_VERSION] < 2 + ): _LOGGER.info("Updating configuration") updated_config = dict(entry.data) updated_config[CONF_UPDATE_INTERVAL_VERSION] = 2 - if CONF_DEVICE_FETCH_INTERVAL not in conf or conf[CONF_DEVICE_FETCH_INTERVAL] <= 31: + if ( + CONF_DEVICE_FETCH_INTERVAL not in conf + or conf[CONF_DEVICE_FETCH_INTERVAL] <= 31 + ): updated_config[CONF_DEVICE_FETCH_INTERVAL] = DEFAULT_DEVICE_FETCH_INTERVAL - _LOGGER.info(f"Setting default fetch interval to {DEFAULT_DEVICE_FETCH_INTERVAL}") - if CONF_ENERGY_FETCH_INTERVAL not in conf or conf[CONF_ENERGY_FETCH_INTERVAL] <= 61: + _LOGGER.info( + f"Setting default fetch interval to {DEFAULT_DEVICE_FETCH_INTERVAL}" + ) + if ( + CONF_ENERGY_FETCH_INTERVAL not in conf + or conf[CONF_ENERGY_FETCH_INTERVAL] <= 61 + ): updated_config[CONF_ENERGY_FETCH_INTERVAL] = DEFAULT_ENERGY_FETCH_INTERVAL - _LOGGER.info(f"Setting default energy fetch interval to {DEFAULT_ENERGY_FETCH_INTERVAL}") + _LOGGER.info( + f"Setting default energy fetch interval to {DEFAULT_ENERGY_FETCH_INTERVAL}" + ) hass.config_entries.async_update_entry(entry, data=updated_config) - if len(devices) == 0 and not api.has_unknown_devices: _LOGGER.error("Could not find any Panasonic Comfort Cloud Heat Pumps") return False @@ -107,48 +128,52 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): energy_coordinators: list[PanasonicDeviceEnergyCoordinator] = [] aquarea_coordinators: list[AquareaDeviceCoordinator] = [] - for device in devices: try: device_coordinator = PanasonicDeviceCoordinator(hass, conf, api, device) await device_coordinator.async_config_entry_first_refresh() data_coordinators.append(device_coordinator) if enable_daily_energy_sensor: - energy_coordinators.append(PanasonicDeviceEnergyCoordinator(hass, conf, api, device)) + energy_coordinators.append( + PanasonicDeviceEnergyCoordinator(hass, conf, api, device) + ) except Exception as e: _LOGGER.warning(f"Failed to setup device: {device.name} ({e})", exc_info=e) if api.has_unknown_devices or AQUAREA_DEMO: try: - + if not AQUAREA_DEMO: aquarea_api_client = AquareaApiClient(client, username, password) await aquarea_api_client.login() else: - aquarea_api_client = AquareaApiClient(client, environment=AquareaEnvironment.DEMO) - aquarea_api_client._access_token = 'dummy' + aquarea_api_client = AquareaApiClient( + client, environment=AquareaEnvironment.DEMO + ) + aquarea_api_client._access_token = "dummy" aquarea_api_client._token_expiration = None - aquarea_devices = await aquarea_api_client.get_devices(include_long_id=True) + aquarea_devices = await aquarea_api_client.get_devices() for aquarea_device in aquarea_devices: try: - aquarea_device_coordinator = AquareaDeviceCoordinator(hass, conf, aquarea_api_client, aquarea_device) + aquarea_device_coordinator = AquareaDeviceCoordinator( + hass, conf, aquarea_api_client, aquarea_device + ) await aquarea_device_coordinator.async_config_entry_first_refresh() aquarea_coordinators.append(aquarea_device_coordinator) except Exception as e: - _LOGGER.warning(f"Failed to setup Aquarea device: {aquarea_device.name} ({e})", exc_info=e) + _LOGGER.warning( + f"Failed to setup Aquarea device: {aquarea_device.name} ({e})", + exc_info=e, + ) except Exception as e: _LOGGER.warning(f"Failed to setup Aquarea: {e}", exc_info=e) - hass.data[DOMAIN][DATA_COORDINATORS] = data_coordinators hass.data[DOMAIN][ENERGY_COORDINATORS] = energy_coordinators hass.data[DOMAIN][AQUAREA_COORDINATORS] = aquarea_coordinators await asyncio.gather( - *( - data.async_config_entry_first_refresh() - for data in energy_coordinators - ), - return_exceptions=True + *(data.async_config_entry_first_refresh() for data in energy_coordinators), + return_exceptions=True, ) await hass.config_entries.async_forward_entry_setups(entry, COMPONENT_TYPES) diff --git a/custom_components/panasonic_cc/base.py b/custom_components/panasonic_cc/base.py index 0d6a314..762388e 100644 --- a/custom_components/panasonic_cc/base.py +++ b/custom_components/panasonic_cc/base.py @@ -3,7 +3,12 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity -from .coordinator import PanasonicDeviceCoordinator, PanasonicDeviceEnergyCoordinator, AquareaDeviceCoordinator +from .coordinator import ( + PanasonicDeviceCoordinator, + PanasonicDeviceEnergyCoordinator, + AquareaDeviceCoordinator, +) + class PanasonicDataEntity(CoordinatorEntity[PanasonicDeviceCoordinator]): @@ -16,7 +21,6 @@ def __init__(self, coordinator: PanasonicDeviceCoordinator, key: str) -> None: self._attr_device_info = self.coordinator.device_info self._async_update_attrs() - def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" self._async_update_attrs() @@ -26,6 +30,7 @@ def _handle_coordinator_update(self) -> None: def _async_update_attrs(self) -> None: """Update the attributes of the entity.""" + class PanasonicEnergyEntity(CoordinatorEntity[PanasonicDeviceEnergyCoordinator]): _attr_has_entity_name = True @@ -37,7 +42,6 @@ def __init__(self, coordinator: PanasonicDeviceEnergyCoordinator, key: str) -> N self._attr_device_info = self.coordinator.device_info self._async_update_attrs() - def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" self._async_update_attrs() @@ -47,6 +51,7 @@ def _handle_coordinator_update(self) -> None: def _async_update_attrs(self) -> None: """Update the attributes of the entity.""" + class AquareaDataEntity(CoordinatorEntity[AquareaDeviceCoordinator]): _attr_has_entity_name = True @@ -58,7 +63,6 @@ def __init__(self, coordinator: AquareaDeviceCoordinator, key: str) -> None: self._attr_device_info = self.coordinator.device_info self._async_update_attrs() - def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" self._async_update_attrs() @@ -66,4 +70,4 @@ def _handle_coordinator_update(self) -> None: @abstractmethod def _async_update_attrs(self) -> None: - """Update the attributes of the entity.""" \ No newline at end of file + """Update the attributes of the entity.""" diff --git a/custom_components/panasonic_cc/button.py b/custom_components/panasonic_cc/button.py index d71e1b2..dee1837 100644 --- a/custom_components/panasonic_cc/button.py +++ b/custom_components/panasonic_cc/button.py @@ -12,9 +12,11 @@ _LOGGER = logging.getLogger(__name__) + @dataclass(frozen=True, kw_only=True) class PanasonicButtonEntityDescription(ButtonEntityDescription): """Describes a Panasonic Button entity.""" + func: Callable[[PanasonicDeviceCoordinator], Awaitable[Any]] | None = None @@ -23,44 +25,57 @@ class PanasonicButtonEntityDescription(ButtonEntityDescription): name="Fetch latest app version", icon="mdi:refresh", entity_category=EntityCategory.DIAGNOSTIC, - func = lambda coordinator: coordinator.api_client.update_app_version() + func=lambda coordinator: coordinator.api_client.update_app_version(), ) UPDATE_DATA_DESCRIPTION = ButtonEntityDescription( key="update_data", name="Fetch latest data", icon="mdi:update", - entity_category=EntityCategory.DIAGNOSTIC + entity_category=EntityCategory.DIAGNOSTIC, ) UPDATE_ENERGY_DESCRIPTION = ButtonEntityDescription( key="update_energy", name="Fetch latest energy data", icon="mdi:update", - entity_category=EntityCategory.DIAGNOSTIC + entity_category=EntityCategory.DIAGNOSTIC, ) + async def async_setup_entry(hass: HomeAssistant, config, async_add_entities): entities = [] - data_coordinators: list[PanasonicDeviceCoordinator] = hass.data[DOMAIN][DATA_COORDINATORS] - energy_coordinators: list[PanasonicDeviceEnergyCoordinator] = hass.data[DOMAIN][ENERGY_COORDINATORS] + data_coordinators: list[PanasonicDeviceCoordinator] = hass.data[DOMAIN][ + DATA_COORDINATORS + ] + energy_coordinators: list[PanasonicDeviceEnergyCoordinator] = hass.data[DOMAIN][ + ENERGY_COORDINATORS + ] for coordinator in data_coordinators: entities.append(PanasonicButtonEntity(coordinator, APP_VERSION_DESCRIPTION)) - entities.append(CoordinatorUpdateButtonEntity(coordinator, UPDATE_DATA_DESCRIPTION)) + entities.append( + CoordinatorUpdateButtonEntity(coordinator, UPDATE_DATA_DESCRIPTION) + ) for coordinator in energy_coordinators: - entities.append(CoordinatorUpdateButtonEntity(coordinator, UPDATE_ENERGY_DESCRIPTION)) - + entities.append( + CoordinatorUpdateButtonEntity(coordinator, UPDATE_ENERGY_DESCRIPTION) + ) + async_add_entities(entities) - + + class PanasonicButtonEntity(PanasonicDataEntity, ButtonEntity): """Representation of a Panasonic Button.""" - + entity_description: PanasonicButtonEntityDescription - def __init__(self, coordinator: PanasonicDeviceCoordinator, description: PanasonicButtonEntityDescription) -> None: + def __init__( + self, + coordinator: PanasonicDeviceCoordinator, + description: PanasonicButtonEntityDescription, + ) -> None: self.entity_description = description super().__init__(coordinator, description.key) - def _async_update_attrs(self) -> None: """Update the attributes of the entity.""" @@ -70,13 +85,15 @@ async def async_press(self) -> None: if self.entity_description.func: await self.entity_description.func(self.coordinator) + class CoordinatorUpdateButtonEntity(PanasonicDataEntity, ButtonEntity): """Representation of a Coordinator Update Button.""" - - def __init__(self, coordinator: DataUpdateCoordinator, description: ButtonEntityDescription) -> None: + + def __init__( + self, coordinator: DataUpdateCoordinator, description: ButtonEntityDescription + ) -> None: self.entity_description = description super().__init__(coordinator, description.key) - def _async_update_attrs(self) -> None: """Update the attributes of the entity.""" @@ -84,4 +101,3 @@ def _async_update_attrs(self) -> None: async def async_press(self) -> None: """Press the button.""" await self.coordinator.async_request_refresh() - diff --git a/custom_components/panasonic_cc/climate.py b/custom_components/panasonic_cc/climate.py index 97a4e28..ef340a1 100644 --- a/custom_components/panasonic_cc/climate.py +++ b/custom_components/panasonic_cc/climate.py @@ -1,4 +1,5 @@ """Support for the Panasonic HVAC.""" + from typing import Callable, Any from dataclasses import dataclass import logging @@ -6,7 +7,13 @@ import voluptuous as vol from homeassistant.core import HomeAssistant -from homeassistant.components.climate import ClimateEntity, ClimateEntityDescription, HVACAction, HVACMode, ATTR_HVAC_MODE +from homeassistant.components.climate import ( + ClimateEntity, + ClimateEntityDescription, + HVACAction, + HVACMode, + ATTR_HVAC_MODE, +) from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.config_entries import ConfigEntry from homeassistant.const import UnitOfTemperature, ATTR_TEMPERATURE @@ -15,45 +22,57 @@ from .base import PanasonicDataEntity, AquareaDataEntity from .coordinator import PanasonicDeviceCoordinator, AquareaDeviceCoordinator -from aio_panasonic_comfort_cloud import PanasonicDeviceParameters, ChangeRequestBuilder, constants +from aio_panasonic_comfort_cloud import ( + PanasonicDeviceParameters, + ChangeRequestBuilder, + constants, +) from aioaquarea import ( ExtendedOperationMode as AquareaExtendedOperationMode, OperationStatus as AquareaZoneOperationStatus, DeviceAction as AquareaDeviceAction, - UpdateOperationMode as AquareaUpdateOperationMode - ) + UpdateOperationMode as AquareaUpdateOperationMode, +) from .const import ( SUPPORT_FLAGS, SERVICE_SET_SWING_LR_MODE, - PRESET_8_15, - PRESET_NONE, - PRESET_ECO, - PRESET_BOOST, - PRESET_QUIET, + PRESET_8_15, + PRESET_NONE, + PRESET_ECO, + PRESET_BOOST, + PRESET_QUIET, PRESET_POWERFUL, DOMAIN, DATA_COORDINATORS, AQUAREA_COORDINATORS, - CONF_USE_PANASONIC_PRESET_NAMES) + CONF_USE_PANASONIC_PRESET_NAMES, +) _LOGGER = logging.getLogger(__name__) + @dataclass(frozen=True, kw_only=True) class PanasonicClimateEntityDescription(ClimateEntityDescription): """Describes a Panasonic climate entity.""" + @dataclass(frozen=True, kw_only=True) class AquareaClimateEntityDescription(ClimateEntityDescription): """Describes a Aquarea climate entity.""" - zone_id:int + + zone_id: int + PANASONIC_CLIMATE_DESCRIPTION = PanasonicClimateEntityDescription( key="climate", translation_key="climate", ) -def convert_operation_mode_to_hvac_mode(operation_mode: constants.OperationMode, iauto: bool) -> HVACMode | None: + +def convert_operation_mode_to_hvac_mode( + operation_mode: constants.OperationMode, iauto: bool +) -> HVACMode | None: """Convert OperationMode to HVAC mode.""" match operation_mode: case constants.OperationMode.Auto: @@ -66,8 +85,11 @@ def convert_operation_mode_to_hvac_mode(operation_mode: constants.OperationMode, return HVACMode.FAN_ONLY case constants.OperationMode.Heat: return HVACMode.HEAT - -def convert_hvac_mode_to_operation_mode(hvac_mode: HVACMode) -> constants.OperationMode | None: + + +def convert_hvac_mode_to_operation_mode( + hvac_mode: HVACMode, +) -> constants.OperationMode | None: """Convert HVAC mode to OperationMode.""" match hvac_mode: case HVACMode.HEAT_COOL: @@ -80,12 +102,13 @@ def convert_hvac_mode_to_operation_mode(hvac_mode: HVACMode) -> constants.Operat return constants.OperationMode.Fan case HVACMode.HEAT: return constants.OperationMode.Heat - + + def convert_state_to_hvac_action(state: PanasonicDeviceParameters) -> HVACAction | None: """Convert state to HVAC action.""" if state.power == constants.Power.Off: return HVACAction.OFF - + match state.mode: case constants.OperationMode.Auto: auto_diff = state.target_temperature - state.inside_temperature @@ -95,14 +118,23 @@ def convert_state_to_hvac_action(state: PanasonicDeviceParameters) -> HVACAction return HVACAction.COOLING return HVACAction.IDLE case constants.OperationMode.Cool: - return HVACAction.COOLING if state.target_temperature < state.inside_temperature else HVACAction.IDLE + return ( + HVACAction.COOLING + if state.target_temperature < state.inside_temperature + else HVACAction.IDLE + ) case constants.OperationMode.Dry: return HVACAction.DRYING case constants.OperationMode.Fan: return HVACAction.FAN case constants.OperationMode.Heat: - return HVACAction.HEATING if state.target_temperature > state.inside_temperature else HVACAction.IDLE - + return ( + HVACAction.HEATING + if state.target_temperature > state.inside_temperature + else HVACAction.IDLE + ) + + def convert_mode_and_status_to_hvac_mode( mode: AquareaExtendedOperationMode, zone_status: AquareaZoneOperationStatus ) -> HVACMode: @@ -120,6 +152,7 @@ def convert_mode_and_status_to_hvac_mode( return HVACMode.OFF + def convert_aquarea_action_to_hvac_action(action: AquareaDeviceAction) -> HVACAction: """Convert device action to HVAC action.""" match action: @@ -129,7 +162,10 @@ def convert_aquarea_action_to_hvac_action(action: AquareaDeviceAction) -> HVACAc return HVACAction.HEATING return HVACAction.IDLE -def convert_hvac_mode_to_aquarea_operation_mode(mode: HVACMode) -> AquareaUpdateOperationMode: + +def convert_hvac_mode_to_aquarea_operation_mode( + mode: HVACMode, +) -> AquareaUpdateOperationMode: """Convert HVAC mode to update operation mode.""" match mode: case HVACMode.HEAT: @@ -140,23 +176,39 @@ def convert_hvac_mode_to_aquarea_operation_mode(mode: HVACMode) -> AquareaUpdate return AquareaUpdateOperationMode.AUTO return AquareaUpdateOperationMode.OFF -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities): + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +): entities = [] - data_coordinators: list[PanasonicDeviceCoordinator] = hass.data[DOMAIN][DATA_COORDINATORS] - aquarea_coordinators: list[AquareaDeviceCoordinator] = hass.data[DOMAIN][AQUAREA_COORDINATORS] - use_panasonic_preset_names = entry.options.get(CONF_USE_PANASONIC_PRESET_NAMES, False) + data_coordinators: list[PanasonicDeviceCoordinator] = hass.data[DOMAIN][ + DATA_COORDINATORS + ] + aquarea_coordinators: list[AquareaDeviceCoordinator] = hass.data[DOMAIN][ + AQUAREA_COORDINATORS + ] + use_panasonic_preset_names = entry.options.get( + CONF_USE_PANASONIC_PRESET_NAMES, False + ) for coordinator in data_coordinators: - entities.append(PanasonicClimateEntity(coordinator, PANASONIC_CLIMATE_DESCRIPTION, use_panasonic_preset_names)) + entities.append( + PanasonicClimateEntity( + coordinator, PANASONIC_CLIMATE_DESCRIPTION, use_panasonic_preset_names + ) + ) for aquarea_coordinator in aquarea_coordinators: - for zone_id in aquarea_coordinator.device.zones: - entities.append(AquareaClimateEntity( - aquarea_coordinator, - AquareaClimateEntityDescription( - zone_id=zone_id, - name=aquarea_coordinator.device.zones.get(zone_id).name, - key=f"zone-{zone_id}-climate", - translation_key=f"zone-{zone_id}-climate" - ))) + for zone_id in aquarea_coordinator.device.zones: + entities.append( + AquareaClimateEntity( + aquarea_coordinator, + AquareaClimateEntityDescription( + zone_id=zone_id, + name=aquarea_coordinator.device.zones.get(zone_id).name, + key=f"zone-{zone_id}-climate", + translation_key=f"zone-{zone_id}-climate", + ), + ) + ) async_add_entities(entities) platform = entity_platform.current_platform.get() @@ -164,7 +216,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_e platform.async_register_entity_service( SERVICE_SET_SWING_LR_MODE, { - vol.Required('swing_mode'): cv.string, + vol.Required("swing_mode"): cv.string, }, "async_set_horizontal_swing_mode", ) @@ -179,9 +231,14 @@ class PanasonicClimateEntity(PanasonicDataEntity, ClimateEntity): _attr_fan_modes = [f.name for f in constants.FanSpeed] _attr_name = None - def __init__(self, coordinator: PanasonicDeviceCoordinator, description: PanasonicClimateEntityDescription, use_panasonic_preset_names: bool): + def __init__( + self, + coordinator: PanasonicDeviceCoordinator, + description: PanasonicClimateEntityDescription, + use_panasonic_preset_names: bool, + ): """Initialize the climate entity.""" - self.entity_description = description + self.entity_description = description device = coordinator.device hvac_modes = [HVACMode.OFF] if device.features.auto_mode: @@ -196,7 +253,9 @@ def __init__(self, coordinator: PanasonicDeviceCoordinator, description: Panason self._attr_hvac_modes = hvac_modes self._quiet_preset = PRESET_QUIET if use_panasonic_preset_names else PRESET_ECO - self._powerful_preset = PRESET_POWERFUL if use_panasonic_preset_names else PRESET_BOOST + self._powerful_preset = ( + PRESET_POWERFUL if use_panasonic_preset_names else PRESET_BOOST + ) preset_modes = [PRESET_NONE] if device.features.quiet_mode: @@ -206,28 +265,34 @@ def __init__(self, coordinator: PanasonicDeviceCoordinator, description: Panason if device.features.summer_house > 0: preset_modes += [PRESET_8_15] self._attr_preset_modes = preset_modes - - self._attr_swing_modes = [opt.name for opt in constants.AirSwingUD if opt != constants.AirSwingUD.Swing or device.features.auto_swing_ud] + + self._attr_swing_modes = [ + opt.name + for opt in constants.AirSwingUD + if opt != constants.AirSwingUD.Swing or device.features.auto_swing_ud + ] if device.has_horizontal_swing: self._attr_supported_features |= ClimateEntityFeature.SWING_HORIZONTAL_MODE - self._attr_swing_horizontal_modes = [opt.name for opt in constants.AirSwingLR if opt != constants.AirSwingLR.Unavailable] + self._attr_swing_horizontal_modes = [ + opt.name + for opt in constants.AirSwingLR + if opt != constants.AirSwingLR.Unavailable + ] super().__init__(coordinator, description.key) _LOGGER.info(f"Registing Climate entity: '{self._attr_unique_id}'") - - - def _async_update_attrs(self) -> None: """Update attributes.""" state = self.coordinator.device.parameters - self._attr_hvac_mode = (HVACMode.OFF - if state.power == constants.Power.Off - else convert_operation_mode_to_hvac_mode( - state.mode, - state.iautox_mode == constants.IAutoXMode.On)) - + self._attr_hvac_mode = ( + HVACMode.OFF + if state.power == constants.Power.Off + else convert_operation_mode_to_hvac_mode( + state.mode, state.iautox_mode == constants.IAutoXMode.On + ) + ) self._set_temp_range() self._attr_current_temperature = state.inside_temperature @@ -247,7 +312,6 @@ def _async_update_attrs(self) -> None: self._attr_preset_mode = PRESET_NONE if self.coordinator.device.has_inside_temperature: self._attr_hvac_action = convert_state_to_hvac_action(state) - def _set_temp_range(self) -> None: """Set new target temperature range.""" @@ -265,11 +329,17 @@ def _update_attributes(self, builder: ChangeRequestBuilder) -> None: default_preset = PRESET_NONE if builder.target_temperature: self._attr_target_temperature = builder.target_temperature - if builder.target_temperature > 15 and self._attr_preset_mode == PRESET_8_15: + if ( + builder.target_temperature > 15 + and self._attr_preset_mode == PRESET_8_15 + ): self._attr_preset_mode = default_preset - elif builder.target_temperature < 15 and self._attr_preset_mode != PRESET_8_15: + elif ( + builder.target_temperature < 15 + and self._attr_preset_mode != PRESET_8_15 + ): self._attr_preset_mode = default_preset = PRESET_8_15 - + if builder.eco_mode: if builder.eco_mode.name in (PRESET_QUIET, PRESET_ECO): self._attr_preset_mode = self._quiet_preset @@ -285,19 +355,20 @@ def _update_attributes(self, builder: ChangeRequestBuilder) -> None: if builder.horizontal_swing: self._attr_swing_horizontal_mode = builder.horizontal_swing.name if builder.hvac_mode: - self._attr_hvac_mode = convert_operation_mode_to_hvac_mode(builder.hvac_mode, False) + self._attr_hvac_mode = convert_operation_mode_to_hvac_mode( + builder.hvac_mode, False + ) self.async_write_ha_state() - async def _async_enter_summer_house_mode(self, builder: ChangeRequestBuilder): """Enter summer house mode.""" device = self.coordinator.device stored_data = await self.coordinator.async_get_stored_data() - stored_data['mode'] = device.parameters.mode.value - stored_data['ecoMode'] = device.parameters.eco_mode.value - stored_data['targetTemperature'] = device.parameters.target_temperature - stored_data['fanSpeed'] = device.parameters.fan_speed.value + stored_data["mode"] = device.parameters.mode.value + stored_data["ecoMode"] = device.parameters.eco_mode.value + stored_data["targetTemperature"] = device.parameters.target_temperature + stored_data["fanSpeed"] = device.parameters.fan_speed.value await self.coordinator.async_store_data(stored_data) builder.set_hvac_mode(constants.OperationMode.Heat) @@ -308,7 +379,9 @@ async def _async_enter_summer_house_mode(self, builder: ChangeRequestBuilder): self._attr_min_temp = 8 self._attr_max_temp = 15 if device.features.summer_house == 2 else 10 - async def _async_exit_summer_house_mode(self, builder: ChangeRequestBuilder) -> Callable[[ClimateEntity], None]: + async def _async_exit_summer_house_mode( + self, builder: ChangeRequestBuilder + ) -> Callable[[ClimateEntity], None]: """Exit summer house mode.""" self._attr_min_temp = 16 self._attr_max_temp = 30 @@ -316,16 +389,32 @@ async def _async_exit_summer_house_mode(self, builder: ChangeRequestBuilder) -> return stored_data = await self.coordinator.async_get_stored_data() try: - hvac_mode = constants.OperationMode(stored_data['mode']) if 'mode' in stored_data else constants.OperationMode.Heat + hvac_mode = ( + constants.OperationMode(stored_data["mode"]) + if "mode" in stored_data + else constants.OperationMode.Heat + ) except: hvac_mode = constants.OperationMode.Heat try: - eco_mode = constants.EcoMode(stored_data['ecoMode']) if 'ecoMode' in stored_data else constants.EcoMode.Auto + eco_mode = ( + constants.EcoMode(stored_data["ecoMode"]) + if "ecoMode" in stored_data + else constants.EcoMode.Auto + ) except: - eco_mode = constants.EcoMode.Auto - target_temperature = stored_data['targetTemperature'] if 'targetTemperature' in stored_data else 20 + eco_mode = constants.EcoMode.Auto + target_temperature = ( + stored_data["targetTemperature"] + if "targetTemperature" in stored_data + else 20 + ) try: - fan_speed = constants.FanSpeed(stored_data['fanSpeed']) if 'fanSpeed' in stored_data else constants.FanSpeed.Auto + fan_speed = ( + constants.FanSpeed(stored_data["fanSpeed"]) + if "fanSpeed" in stored_data + else constants.FanSpeed.Auto + ) except: fan_speed = constants.FanSpeed.Auto @@ -360,7 +449,7 @@ async def async_set_temperature(self, **kwargs: Any) -> None: builder.set_hvac_mode(op_mode) else: mode = None - await self.coordinator.async_apply_changes(builder) + await self.coordinator.async_apply_changes(builder) self._update_attributes(builder) async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: @@ -370,18 +459,18 @@ async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: return if not (op_mode := convert_hvac_mode_to_operation_mode(hvac_mode)): raise ValueError(f"Invalid hvac mode {hvac_mode}") - + builder = self.coordinator.get_change_request_builder() await self._async_exit_summer_house_mode(builder) builder.set_hvac_mode(op_mode) await self.coordinator.async_apply_changes(builder) self._update_attributes(builder) - + async def async_set_preset_mode(self, preset_mode: str) -> None: """Set new target preset mode.""" if preset_mode not in self.preset_modes: raise ValueError(f"Unsupported preset_mode '{preset_mode}'") - + builder = self.coordinator.get_change_request_builder() await self._async_exit_summer_house_mode(builder) builder.set_eco_mode(constants.EcoMode.Auto) @@ -394,12 +483,12 @@ async def async_set_preset_mode(self, preset_mode: str) -> None: await self.coordinator.async_apply_changes(builder) self._update_attributes(builder) await self.coordinator.async_request_refresh() - + async def async_set_fan_mode(self, fan_mode: str) -> None: """Set new target fan mode.""" if fan_mode not in self.fan_modes: raise ValueError(f"Unsupported fan_mode '{fan_mode}'") - + builder = self.coordinator.get_change_request_builder() builder.set_fan_speed(fan_mode) await self.coordinator.async_apply_changes(builder) @@ -409,7 +498,7 @@ async def async_set_swing_mode(self, swing_mode: str): """Set new target swing mode.""" if swing_mode not in self.swing_modes: raise ValueError(f"Unsupported swing mode '{swing_mode}'") - + builder = self.coordinator.get_change_request_builder() builder.set_vertical_swing(swing_mode) await self.coordinator.async_apply_changes(builder) @@ -419,27 +508,35 @@ async def async_set_swing_horizontal_mode(self, swing_mode: str): """Set new target swing mode.""" if swing_mode not in self.swing_horizontal_modes: raise ValueError(f"Unsupported swing mode '{swing_mode}'") - + builder = self.coordinator.get_change_request_builder() builder.set_horizontal_swing(swing_mode) await self.coordinator.async_apply_changes(builder) self._update_attributes(builder) + class AquareaClimateEntity(AquareaDataEntity, ClimateEntity): """Representation of a Aquarea Climate Device.""" _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_target_temperature_step = 1 - _attr_supported_features = ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.TURN_ON | ClimateEntityFeature.TURN_OFF + _attr_supported_features = ( + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.TURN_ON + | ClimateEntityFeature.TURN_OFF + ) entity_description: AquareaClimateEntityDescription - def __init__(self, coordinator: AquareaDeviceCoordinator, description: AquareaClimateEntityDescription): + def __init__( + self, + coordinator: AquareaDeviceCoordinator, + description: AquareaClimateEntityDescription, + ): """Initialize the climate entity.""" self.entity_description = description device = coordinator.device self._attr_hvac_modes = [HVACMode.HEAT, HVACMode.OFF] - if device.support_cooling(description.zone_id): self._attr_hvac_modes.extend([HVACMode.COOL, HVACMode.HEAT_COOL]) @@ -451,24 +548,37 @@ def _async_update_attrs(self) -> None: """Update attributes.""" device = self.coordinator.device zone = device.zones.get(self.entity_description.zone_id) - self._attr_hvac_mode = convert_mode_and_status_to_hvac_mode(device.mode, zone.operation_status) - self._attr_hvac_action = convert_aquarea_action_to_hvac_action(device.current_action) + self._attr_hvac_mode = convert_mode_and_status_to_hvac_mode( + device.mode, zone.operation_status + ) + self._attr_hvac_action = convert_aquarea_action_to_hvac_action( + device.current_action + ) self._attr_current_temperature = zone.temperature self._attr_max_temp = zone.temperature self._attr_min_temp = zone.temperature - if zone.supports_set_temperature and device.mode != AquareaExtendedOperationMode.OFF: + if ( + zone.supports_set_temperature + and device.mode != AquareaExtendedOperationMode.OFF + ): self._attr_max_temp = ( zone.cool_max if device.mode - in (AquareaExtendedOperationMode.COOL, AquareaExtendedOperationMode.AUTO_COOL) + in ( + AquareaExtendedOperationMode.COOL, + AquareaExtendedOperationMode.AUTO_COOL, + ) else zone.heat_max ) self._attr_min_temp = ( zone.cool_min if device.mode - in (AquareaExtendedOperationMode.COOL, AquareaExtendedOperationMode.AUTO_COOL) + in ( + AquareaExtendedOperationMode.COOL, + AquareaExtendedOperationMode.AUTO_COOL, + ) else zone.heat_min ) self._attr_target_temperature = ( diff --git a/custom_components/panasonic_cc/config_flow.py b/custom_components/panasonic_cc/config_flow.py index 8402645..a52cfc4 100644 --- a/custom_components/panasonic_cc/config_flow.py +++ b/custom_components/panasonic_cc/config_flow.py @@ -1,4 +1,5 @@ """Config flow for the Panasonic Comfort Cloud platform.""" + import asyncio import logging from typing import Any, Dict, Optional, Mapping @@ -24,7 +25,8 @@ CONF_ENERGY_FETCH_INTERVAL, DEFAULT_ENERGY_FETCH_INTERVAL, CONF_FORCE_ENABLE_NANOE, - DEFAULT_FORCE_ENABLE_NANOE) + DEFAULT_FORCE_ENABLE_NANOE, +) _LOGGER = logging.getLogger(__name__) @@ -49,16 +51,19 @@ async def _create_entry(self, username, password): if entry.data[KEY_DOMAIN] == PANASONIC_DOMAIN: return self.async_abort(reason="already_configured") - return self.async_create_entry(title="", data={ - CONF_USERNAME: username, - CONF_PASSWORD: password, - CONF_FORCE_OUTSIDE_SENSOR: False, - CONF_FORCE_ENABLE_NANOE: DEFAULT_FORCE_ENABLE_NANOE, - CONF_ENABLE_DAILY_ENERGY_SENSOR: DEFAULT_ENABLE_DAILY_ENERGY_SENSOR, - CONF_USE_PANASONIC_PRESET_NAMES: DEFAULT_USE_PANASONIC_PRESET_NAMES, - CONF_DEVICE_FETCH_INTERVAL: DEFAULT_DEVICE_FETCH_INTERVAL, - CONF_ENERGY_FETCH_INTERVAL: DEFAULT_ENERGY_FETCH_INTERVAL, - }) + return self.async_create_entry( + title="", + data={ + CONF_USERNAME: username, + CONF_PASSWORD: password, + CONF_FORCE_OUTSIDE_SENSOR: False, + CONF_FORCE_ENABLE_NANOE: DEFAULT_FORCE_ENABLE_NANOE, + CONF_ENABLE_DAILY_ENERGY_SENSOR: DEFAULT_ENABLE_DAILY_ENERGY_SENSOR, + CONF_USE_PANASONIC_PRESET_NAMES: DEFAULT_USE_PANASONIC_PRESET_NAMES, + CONF_DEVICE_FETCH_INTERVAL: DEFAULT_DEVICE_FETCH_INTERVAL, + CONF_ENERGY_FETCH_INTERVAL: DEFAULT_ENERGY_FETCH_INTERVAL, + }, + ) async def _create_device(self, username, password): """Create device.""" @@ -86,35 +91,40 @@ async def _create_device(self, username, password): async def async_step_user(self, user_input=None): """User initiated config flow.""" - + if user_input is None: return self.async_show_form( - step_id="user", data_schema=vol.Schema({ - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, - vol.Optional( - CONF_ENABLE_DAILY_ENERGY_SENSOR, - default=DEFAULT_ENABLE_DAILY_ENERGY_SENSOR, - ): bool, - vol.Optional( - CONF_FORCE_ENABLE_NANOE, - default=False, - ): bool, - vol.Optional( - CONF_USE_PANASONIC_PRESET_NAMES, - default=DEFAULT_USE_PANASONIC_PRESET_NAMES, - ): bool, - vol.Optional( - CONF_DEVICE_FETCH_INTERVAL, - default=DEFAULT_DEVICE_FETCH_INTERVAL, - ): int, - vol.Optional( - CONF_ENERGY_FETCH_INTERVAL, - default=DEFAULT_ENERGY_FETCH_INTERVAL, - ): int, - }) + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional( + CONF_ENABLE_DAILY_ENERGY_SENSOR, + default=DEFAULT_ENABLE_DAILY_ENERGY_SENSOR, + ): bool, + vol.Optional( + CONF_FORCE_ENABLE_NANOE, + default=False, + ): bool, + vol.Optional( + CONF_USE_PANASONIC_PRESET_NAMES, + default=DEFAULT_USE_PANASONIC_PRESET_NAMES, + ): bool, + vol.Optional( + CONF_DEVICE_FETCH_INTERVAL, + default=DEFAULT_DEVICE_FETCH_INTERVAL, + ): int, + vol.Optional( + CONF_ENERGY_FETCH_INTERVAL, + default=DEFAULT_ENERGY_FETCH_INTERVAL, + ): int, + } + ), ) - return await self._create_device(user_input[CONF_USERNAME], user_input[CONF_PASSWORD]) + return await self._create_device( + user_input[CONF_USERNAME], user_input[CONF_PASSWORD] + ) async def async_step_import(self, user_input): """Import a config entry.""" @@ -122,7 +132,7 @@ async def async_step_import(self, user_input): if not username: return await self.async_step_user() return await self._create_device(username, user_input[CONF_PASSWORD]) - + async def async_step_reconfigure( self, entry_data: Mapping[str, Any] ) -> config_entries.ConfigFlowResult: @@ -155,7 +165,6 @@ async def async_auth(self, user_input: Mapping[str, Any]) -> dict[str, str]: _LOGGER.exception("Unexpected error creating device", e) return {"base": "device_fail"} - return {} async def async_step_reconfigure_confirm( @@ -174,15 +183,16 @@ async def async_step_reconfigure_confirm( return self.async_show_form( step_id="reconfigure_confirm", - data_schema=vol.Schema({ - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, - }), + data_schema=vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } + ), errors=errors, ) - class PanasonicOptionsFlowHandler(config_entries.OptionsFlow): """Handle Panasonic options.""" @@ -191,7 +201,7 @@ def __init__(self, config_entry): self.config_entry = config_entry async def async_step_init( - self, user_input: Optional[Dict[str, Any]] = None + self, user_input: Optional[Dict[str, Any]] = None ) -> config_entries.ConfigFlowResult: """Manage Panasonic options.""" if user_input is not None: @@ -204,7 +214,8 @@ async def async_step_init( vol.Optional( CONF_ENABLE_DAILY_ENERGY_SENSOR, default=self.config_entry.options.get( - CONF_ENABLE_DAILY_ENERGY_SENSOR, DEFAULT_ENABLE_DAILY_ENERGY_SENSOR + CONF_ENABLE_DAILY_ENERGY_SENSOR, + DEFAULT_ENABLE_DAILY_ENERGY_SENSOR, ), ): bool, vol.Optional( @@ -216,7 +227,8 @@ async def async_step_init( vol.Optional( CONF_USE_PANASONIC_PRESET_NAMES, default=self.config_entry.options.get( - CONF_USE_PANASONIC_PRESET_NAMES, DEFAULT_USE_PANASONIC_PRESET_NAMES + CONF_USE_PANASONIC_PRESET_NAMES, + DEFAULT_USE_PANASONIC_PRESET_NAMES, ), ): bool, vol.Optional( diff --git a/custom_components/panasonic_cc/const.py b/custom_components/panasonic_cc/const.py index 77e95c8..c62ea36 100644 --- a/custom_components/panasonic_cc/const.py +++ b/custom_components/panasonic_cc/const.py @@ -1,8 +1,13 @@ """Constants for Panasonic Cloud.""" + from homeassistant.const import CONF_ICON, CONF_NAME, CONF_TYPE, Platform from homeassistant.components.climate.const import ( - HVACMode, ClimateEntityFeature, - PRESET_ECO, PRESET_NONE, PRESET_BOOST) + HVACMode, + ClimateEntityFeature, + PRESET_ECO, + PRESET_NONE, + PRESET_BOOST, +) ATTR_TARGET_TEMPERATURE = "target_temperature" ATTR_INSIDE_TEMPERATURE = "inside_temperature" @@ -64,23 +69,22 @@ } SUPPORT_FLAGS = ( - ClimateEntityFeature.TARGET_TEMPERATURE | - ClimateEntityFeature.FAN_MODE | - ClimateEntityFeature.PRESET_MODE | - ClimateEntityFeature.SWING_MODE | - ClimateEntityFeature.TURN_OFF | - ClimateEntityFeature.TURN_ON - ) - + ClimateEntityFeature.TARGET_TEMPERATURE + | ClimateEntityFeature.FAN_MODE + | ClimateEntityFeature.PRESET_MODE + | ClimateEntityFeature.SWING_MODE + | ClimateEntityFeature.TURN_OFF + | ClimateEntityFeature.TURN_ON +) OPERATION_LIST = { - HVACMode.OFF: 'Off', - HVACMode.HEAT: 'Heat', - HVACMode.COOL: 'Cool', - HVACMode.HEAT_COOL: 'Auto', - HVACMode.DRY: 'Dry', - HVACMode.FAN_ONLY: 'Fan' + HVACMode.OFF: "Off", + HVACMode.HEAT: "Heat", + HVACMode.COOL: "Cool", + HVACMode.HEAT_COOL: "Auto", + HVACMode.DRY: "Dry", + HVACMode.FAN_ONLY: "Fan", } DOMAIN = "panasonic_cc" @@ -97,8 +101,8 @@ Platform.BUTTON, Platform.SELECT, Platform.NUMBER, - Platform.WATER_HEATER - ] + Platform.WATER_HEATER, +] STARTUP = """ ------------------------------------------------------------------- @@ -120,4 +124,4 @@ DEFAULT_DEVICE_FETCH_INTERVAL = 120 DEFAULT_ENERGY_FETCH_INTERVAL = 300 CONF_FORCE_ENABLE_NANOE = "force_enable_nanoe" -DEFAULT_FORCE_ENABLE_NANOE = False \ No newline at end of file +DEFAULT_FORCE_ENABLE_NANOE = False diff --git a/custom_components/panasonic_cc/coordinator.py b/custom_components/panasonic_cc/coordinator.py index 98d1e9b..fefb0ea 100644 --- a/custom_components/panasonic_cc/coordinator.py +++ b/custom_components/panasonic_cc/coordinator.py @@ -7,61 +7,87 @@ from homeassistant.helpers.storage import Store from homeassistant.util import dt as dt_util -from aio_panasonic_comfort_cloud import ApiClient, PanasonicDevice, PanasonicDeviceInfo, PanasonicDeviceEnergy, ChangeRequestBuilder -from aioaquarea import Client as AquareaApiClient, Device as AquareaDevice, AquareaEnvironment +from aio_panasonic_comfort_cloud import ( + ApiClient, + PanasonicDevice, + PanasonicDeviceInfo, + PanasonicDeviceEnergy, + ChangeRequestBuilder, +) +from aioaquarea import ( + Client as AquareaApiClient, + Device as AquareaDevice, + AquareaEnvironment, +) from aioaquarea.data import DeviceInfo as AquareaDeviceInfo - -from .const import DOMAIN,MANUFACTURER, DEFAULT_DEVICE_FETCH_INTERVAL, CONF_DEVICE_FETCH_INTERVAL, CONF_ENERGY_FETCH_INTERVAL, DEFAULT_ENERGY_FETCH_INTERVAL +from aioaquarea.errors import AuthenticationError, RequestFailedError +from .const import ( + DOMAIN, + MANUFACTURER, + DEFAULT_DEVICE_FETCH_INTERVAL, + CONF_DEVICE_FETCH_INTERVAL, + CONF_ENERGY_FETCH_INTERVAL, + DEFAULT_ENERGY_FETCH_INTERVAL, +) _LOGGER = logging.getLogger(__name__) + class PanasonicDeviceCoordinator(DataUpdateCoordinator[int]): - def __init__(self, hass: HomeAssistant, config: dict, api_client: ApiClient, device_info: PanasonicDeviceInfo): + def __init__( + self, + hass: HomeAssistant, + config: dict, + api_client: ApiClient, + device_info: PanasonicDeviceInfo, + ): super().__init__( hass, _LOGGER, name="Panasonic Device Coordinator", - update_interval=timedelta(seconds=config.get(CONF_DEVICE_FETCH_INTERVAL, DEFAULT_DEVICE_FETCH_INTERVAL)), + update_interval=timedelta( + seconds=config.get( + CONF_DEVICE_FETCH_INTERVAL, DEFAULT_DEVICE_FETCH_INTERVAL + ) + ), update_method=self._fetch_device_data, ) self._hass = hass self._config = config self._api_client = api_client self._panasonic_device_info = device_info - self._device:PanasonicDevice | None = None + self._device: PanasonicDevice | None = None self._store = Store(hass, version=1, key=f"panasonic_cc_{device_info.id}") self._update_id = 0 - - + @property def device(self) -> PanasonicDevice: if self._device is None: raise ValueError("device has not been initialized") return self._device - + @property def api_client(self) -> ApiClient: return self._api_client - + @property def device_id(self) -> str: return self._panasonic_device_info.id - @property - def device_info(self)->DeviceInfo: + def device_info(self) -> DeviceInfo: return DeviceInfo( - identifiers={(DOMAIN, self._panasonic_device_info.id )}, + identifiers={(DOMAIN, self._panasonic_device_info.id)}, manufacturer=MANUFACTURER, model=self._panasonic_device_info.model, name=self._panasonic_device_info.name, - sw_version=self._api_client.app_version + sw_version=self._api_client.app_version, ) - + def get_change_request_builder(self): return ChangeRequestBuilder(self.device) - + async def async_apply_changes(self, request_builder: ChangeRequestBuilder): await self._api_client.set_device_raw(self.device, request_builder.build()) @@ -70,39 +96,52 @@ async def async_get_stored_data(self): if data is None: data = {} return data - + async def async_store_data(self, data): await self._store.async_save(data) - - async def _fetch_device_data(self)->int: + async def _fetch_device_data(self) -> int: try: if self._device is None: - self._device = await self._api_client.get_device(self._panasonic_device_info) + self._device = await self._api_client.get_device( + self._panasonic_device_info + ) _LOGGER.debug( - "%s Device features\nNanoe: %s\nEco Navi: %s\nAI Eco: %s", + "%s Device features\nNanoe: %s\nEco Navi: %s\nAI Eco: %s", self._panasonic_device_info.name, - self._device.has_nanoe, - self._device.has_eco_navi, - self._device.has_eco_function) + self._device.has_nanoe, + self._device.has_eco_navi, + self._device.has_eco_function, + ) self._update_id = 1 return self._update_id if await self._api_client.try_update_device(self._device): - self._update_id = self._update_id + 1 - return self._update_id + self._update_id = self._update_id + 1 + return self._update_id except BaseException as e: _LOGGER.error("Error fetching device data from API: %s", e, exc_info=e) raise UpdateFailed(f"Invalid response from API: {e}") from e return self._update_id + class PanasonicDeviceEnergyCoordinator(DataUpdateCoordinator[int]): - def __init__(self, hass: HomeAssistant, config: dict, api_client: ApiClient, device_info: PanasonicDeviceInfo): + def __init__( + self, + hass: HomeAssistant, + config: dict, + api_client: ApiClient, + device_info: PanasonicDeviceInfo, + ): super().__init__( hass, _LOGGER, name="Panasonic Device Energy Coordinator", - update_interval=timedelta(seconds=config.get(CONF_ENERGY_FETCH_INTERVAL, DEFAULT_ENERGY_FETCH_INTERVAL)), + update_interval=timedelta( + seconds=config.get( + CONF_ENERGY_FETCH_INTERVAL, DEFAULT_ENERGY_FETCH_INTERVAL + ) + ), update_method=self._fetch_device_data, ) self._hass = hass @@ -115,55 +154,67 @@ def __init__(self, hass: HomeAssistant, config: dict, api_client: ApiClient, dev @property def api_client(self) -> ApiClient: return self._api_client - + @property def device_id(self) -> str: return self._panasonic_device_info.id - + @property def energy(self) -> PanasonicDeviceEnergy | None: return self._energy - + @property - def device_info(self)->DeviceInfo: + def device_info(self) -> DeviceInfo: return DeviceInfo( - identifiers={(DOMAIN, self._panasonic_device_info.id )}, + identifiers={(DOMAIN, self._panasonic_device_info.id)}, manufacturer=MANUFACTURER, model=self._panasonic_device_info.model, name=self._panasonic_device_info.name, - sw_version=self._api_client.app_version + sw_version=self._api_client.app_version, ) - async def _fetch_device_data(self)->int: + async def _fetch_device_data(self) -> int: try: if self._energy is None: - self._energy = await self._api_client.async_get_energy(self._panasonic_device_info) + self._energy = await self._api_client.async_get_energy( + self._panasonic_device_info + ) self._update_id = 1 return self._update_id if await self._api_client.async_try_update_energy(self._energy): - self._update_id = self._update_id + 1 - return self._update_id + self._update_id = self._update_id + 1 + return self._update_id except BaseException as e: _LOGGER.error("Error fetching energy data from API: %s", e, exc_info=e) raise UpdateFailed(f"Invalid response from API: {e}") from e return self._update_id - + class AquareaDeviceCoordinator(DataUpdateCoordinator): - def __init__(self, hass: HomeAssistant, config: dict, api_client: AquareaApiClient, device_info: AquareaDeviceInfo): + def __init__( + self, + hass: HomeAssistant, + config: dict, + api_client: AquareaApiClient, + device_info: AquareaDeviceInfo, + ): super().__init__( hass, _LOGGER, name="Aquarea Device Coordinator", - update_interval=timedelta(seconds=config.get(CONF_DEVICE_FETCH_INTERVAL, DEFAULT_DEVICE_FETCH_INTERVAL)), + update_interval=timedelta( + seconds=config.get( + CONF_DEVICE_FETCH_INTERVAL, DEFAULT_DEVICE_FETCH_INTERVAL + ) + ), update_method=self._fetch_device_data, ) self._hass = hass self._config = config self._api_client = api_client self._aquarea_device_info = device_info - self._device:AquareaDevice | None = None + self._device: AquareaDevice | None = None self._update_id = 0 self._is_demo = api_client._environment == AquareaEnvironment.DEMO @@ -172,40 +223,93 @@ def device(self) -> AquareaDevice: if self._device is None: raise ValueError("device has not been initialized") return self._device - + @property def api_client(self) -> AquareaApiClient: return self._api_client - + @property def device_id(self) -> str: - return self.device.device_id if not self._is_demo else "demo-house" + if self._is_demo: + return "demo-house" + if self._device is not None: + return ( + getattr(self._device, "device_id", None) or self._aquarea_device_info.id + ) + return self._aquarea_device_info.id - @property - def device_info(self)->DeviceInfo: + def device_info(self) -> DeviceInfo: + name = ( + getattr(self._device, "device_name", None) + or getattr(self._device, "name", None) + or self._aquarea_device_info.name + ) + manufacturer = getattr(self._device, "manufacturer", None) or "Panasonic" + model = getattr(self._device, "model", None) or "" + sw_version = getattr(self._device, "version", None) + return DeviceInfo( identifiers={(DOMAIN, self.device_id)}, - manufacturer=self.device.manufacturer, - model="", - name=self.device.name, - sw_version=self.device.version, + manufacturer=manufacturer, + model=model, + name=name, + sw_version=sw_version, ) - async def _fetch_device_data(self)->int: - try: - if self._device is None: - self._device = await self._api_client.get_device( - device_info=self._aquarea_device_info, - consumption_refresh_interval=timedelta(seconds=self._config.get(CONF_ENERGY_FETCH_INTERVAL, DEFAULT_ENERGY_FETCH_INTERVAL)), - timezone=dt_util.DEFAULT_TIME_ZONE) - - self._update_id = 1 + async def _fetch_device_data(self) -> int: + for attempt in (1, 2): + try: + if self._device is None: + self._device = await self._api_client.get_device( + device_info=self._aquarea_device_info, + consumption_refresh_interval=timedelta( + seconds=self._config.get( + CONF_ENERGY_FETCH_INTERVAL, + DEFAULT_ENERGY_FETCH_INTERVAL, + ) + ), + timezone=dt_util.DEFAULT_TIME_ZONE, + ) + self._update_id = 1 + return self._update_id + + await self._device.refresh_data() + self._update_id = self._update_id + 1 return self._update_id - await self._device.refresh_data() - self._update_id = self._update_id + 1 - return self._update_id - except BaseException as e: - _LOGGER.error("Error fetching device data from API: %s", e, exc_info=e) - raise UpdateFailed(f"Invalid response from API: {e}") from e - return self._update_id \ No newline at end of file + + except AuthenticationError as auth_err: + if attempt == 2: + _LOGGER.error( + "Authentication failed after retry: %s", + auth_err, + exc_info=auth_err, + ) + raise UpdateFailed( + f"Invalid response from API: {auth_err}" + ) from auth_err + _LOGGER.warning( + "Aquarea token expired, re-authenticating and retrying once" + ) + await self._api_client.login() + self._device = None + + except RequestFailedError as req_err: + cause = req_err.__cause__ + if attempt == 1 and isinstance(cause, AuthenticationError): + _LOGGER.warning( + "Aquarea request failed due to expired token, re-authenticating and retrying" + ) + await self._api_client.login() + self._device = None + continue + _LOGGER.error( + "Error fetching device data from API: %s", req_err, exc_info=req_err + ) + raise UpdateFailed(f"Invalid response from API: {req_err}") from req_err + + except BaseException as e: + _LOGGER.error("Error fetching device data from API: %s", e, exc_info=e) + raise UpdateFailed(f"Invalid response from API: {e}") from e + + return self._update_id diff --git a/custom_components/panasonic_cc/manifest.json b/custom_components/panasonic_cc/manifest.json index 8e0388e..322dfcc 100644 --- a/custom_components/panasonic_cc/manifest.json +++ b/custom_components/panasonic_cc/manifest.json @@ -10,6 +10,6 @@ "integration_type": "hub", "iot_class": "cloud_polling", "issue_tracker": "https://github.com/sockless-coding/panasonic_cc/issues", - "requirements": ["aiohttp","aio-panasonic-comfort-cloud==2025.5.1","aioaquarea==0.7.2"], + "requirements": ["aiohttp","aio-panasonic-comfort-cloud==2025.5.1","aioaquarea==1.0.3"], "quality_scale": "silver" } diff --git a/custom_components/panasonic_cc/number.py b/custom_components/panasonic_cc/number.py index 81accfa..49d8ee8 100644 --- a/custom_components/panasonic_cc/number.py +++ b/custom_components/panasonic_cc/number.py @@ -11,24 +11,31 @@ NumberMode, ) -from aio_panasonic_comfort_cloud import PanasonicDevice, PanasonicDeviceZone, ChangeRequestBuilder +from aio_panasonic_comfort_cloud import ( + PanasonicDevice, + PanasonicDeviceZone, + ChangeRequestBuilder, +) from . import DOMAIN from .const import DATA_COORDINATORS from .coordinator import PanasonicDeviceCoordinator from .base import PanasonicDataEntity + @dataclass(frozen=True, kw_only=True) class PanasonicNumberEntityDescription(NumberEntityDescription): """Describes Panasonic Number entity.""" + get_value: Callable[[PanasonicDevice], int] set_value: Callable[[ChangeRequestBuilder, int], ChangeRequestBuilder] + def create_zone_damper_description(zone: PanasonicDeviceZone): return PanasonicNumberEntityDescription( - key = f"zone-{zone.id}-damper", + key=f"zone-{zone.id}-damper", translation_key=f"zone-{zone.id}-damper", - name = f"{zone.name} Damper Position", + name=f"{zone.name} Damper Position", icon="mdi:valve", native_unit_of_measurement=PERCENTAGE, native_max_value=100, @@ -39,26 +46,35 @@ def create_zone_damper_description(zone: PanasonicDeviceZone): set_value=lambda builder, value: builder.set_zone_damper(zone.id, value), ) + async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities): devices = [] - data_coordinators: list[PanasonicDeviceCoordinator] = hass.data[DOMAIN][DATA_COORDINATORS] + data_coordinators: list[PanasonicDeviceCoordinator] = hass.data[DOMAIN][ + DATA_COORDINATORS + ] for data_coordinator in data_coordinators: if data_coordinator.device.has_zones: for zone in data_coordinator.device.parameters.zones: - devices.append(PanasonicNumberEntity( - data_coordinator, - create_zone_damper_description(zone))) + devices.append( + PanasonicNumberEntity( + data_coordinator, create_zone_damper_description(zone) + ) + ) async_add_entities(devices) + class PanasonicNumberEntity(PanasonicDataEntity, NumberEntity): entity_description: PanasonicNumberEntityDescription - def __init__(self, coordinator: PanasonicDeviceCoordinator, description: PanasonicNumberEntityDescription): + def __init__( + self, + coordinator: PanasonicDeviceCoordinator, + description: PanasonicNumberEntityDescription, + ): self.entity_description = description super().__init__(coordinator, description.key) - async def async_set_native_value(self, value: float) -> None: """Set new value.""" @@ -70,4 +86,6 @@ async def async_set_native_value(self, value: float) -> None: self.async_write_ha_state() def _async_update_attrs(self) -> None: - self._attr_native_value = self.entity_description.get_value(self.coordinator.device) \ No newline at end of file + self._attr_native_value = self.entity_description.get_value( + self.coordinator.device + ) diff --git a/custom_components/panasonic_cc/select.py b/custom_components/panasonic_cc/select.py index 3fda240..4c3d120 100644 --- a/custom_components/panasonic_cc/select.py +++ b/custom_components/panasonic_cc/select.py @@ -4,15 +4,22 @@ from homeassistant.core import HomeAssistant from homeassistant.components.select import SelectEntity, SelectEntityDescription -from .const import DOMAIN, DATA_COORDINATORS, SELECT_HORIZONTAL_SWING, SELECT_VERTICAL_SWING +from .const import ( + DOMAIN, + DATA_COORDINATORS, + SELECT_HORIZONTAL_SWING, + SELECT_VERTICAL_SWING, +) from aio_panasonic_comfort_cloud import PanasonicDevice, ChangeRequestBuilder, constants from .coordinator import PanasonicDeviceCoordinator from .base import PanasonicDataEntity + @dataclass(frozen=True, kw_only=True) class PanasonicSelectEntityDescription(SelectEntityDescription): """Description of a select entity.""" + set_option: Callable[[ChangeRequestBuilder, str], ChangeRequestBuilder] get_current_option: Callable[[PanasonicDevice], str] is_available: Callable[[PanasonicDevice], bool] @@ -20,48 +27,67 @@ class PanasonicSelectEntityDescription(SelectEntityDescription): HORIZONTAL_SWING_DESCRIPTION = PanasonicSelectEntityDescription( - key=SELECT_HORIZONTAL_SWING, + key=SELECT_HORIZONTAL_SWING, translation_key=SELECT_HORIZONTAL_SWING, icon="mdi:swap-horizontal", name="Horizontal Swing Mode", - options= [opt.name for opt in constants.AirSwingLR if opt != constants.AirSwingLR.Unavailable], - set_option = lambda builder, new_value : builder.set_horizontal_swing(new_value), - get_current_option = lambda device : device.parameters.horizontal_swing_mode.name, - is_available = lambda device : device.has_horizontal_swing + options=[ + opt.name + for opt in constants.AirSwingLR + if opt != constants.AirSwingLR.Unavailable + ], + set_option=lambda builder, new_value: builder.set_horizontal_swing(new_value), + get_current_option=lambda device: device.parameters.horizontal_swing_mode.name, + is_available=lambda device: device.has_horizontal_swing, ) VERTICAL_SWING_DESCRIPTION = PanasonicSelectEntityDescription( - key=SELECT_VERTICAL_SWING, + key=SELECT_VERTICAL_SWING, translation_key=SELECT_VERTICAL_SWING, icon="mdi:swap-vertical", name="Vertical Swing Mode", - get_options= lambda device: [opt.name for opt in constants.AirSwingUD if opt != constants.AirSwingUD.Swing or device.features.auto_swing_ud], - set_option = lambda builder, new_value : builder.set_vertical_swing(new_value), - get_current_option = lambda device : device.parameters.vertical_swing_mode.name, - is_available = lambda device : True + get_options=lambda device: [ + opt.name + for opt in constants.AirSwingUD + if opt != constants.AirSwingUD.Swing or device.features.auto_swing_ud + ], + set_option=lambda builder, new_value: builder.set_vertical_swing(new_value), + get_current_option=lambda device: device.parameters.vertical_swing_mode.name, + is_available=lambda device: True, ) async def async_setup_entry(hass: HomeAssistant, entry, async_add_entities): entities = [] - data_coordinators: list[PanasonicDeviceCoordinator] = hass.data[DOMAIN][DATA_COORDINATORS] + data_coordinators: list[PanasonicDeviceCoordinator] = hass.data[DOMAIN][ + DATA_COORDINATORS + ] for coordinator in data_coordinators: - entities.append(PanasonicSelectEntity(coordinator, HORIZONTAL_SWING_DESCRIPTION)) + entities.append( + PanasonicSelectEntity(coordinator, HORIZONTAL_SWING_DESCRIPTION) + ) entities.append(PanasonicSelectEntity(coordinator, VERTICAL_SWING_DESCRIPTION)) - + async_add_entities(entities) + class PanasonicSelectEntityBase(SelectEntity): """Base class for all select entities.""" + entity_description: PanasonicSelectEntityDescription + class PanasonicSelectEntity(PanasonicDataEntity, PanasonicSelectEntityBase): - def __init__(self, coordinator: PanasonicDeviceCoordinator, description: PanasonicSelectEntityDescription): + def __init__( + self, + coordinator: PanasonicDeviceCoordinator, + description: PanasonicSelectEntityDescription, + ): self.entity_description = description if description.get_options is not None: self._attr_options = description.get_options(coordinator.device) super().__init__(coordinator, description.key) - + @property def available(self) -> bool: """Return if entity is available.""" @@ -75,5 +101,6 @@ async def async_select_option(self, option: str) -> None: self.async_write_ha_state() def _async_update_attrs(self) -> None: - self.current_option = self.entity_description.get_current_option(self.coordinator.device) - + self.current_option = self.entity_description.get_current_option( + self.coordinator.device + ) diff --git a/custom_components/panasonic_cc/sensor.py b/custom_components/panasonic_cc/sensor.py index 8786eee..8b2c161 100644 --- a/custom_components/panasonic_cc/sensor.py +++ b/custom_components/panasonic_cc/sensor.py @@ -7,39 +7,50 @@ SensorEntity, SensorStateClass, SensorDeviceClass, - SensorEntityDescription + SensorEntityDescription, ) -from aio_panasonic_comfort_cloud import PanasonicDevice, PanasonicDeviceEnergy, PanasonicDeviceZone, constants +from aio_panasonic_comfort_cloud import ( + PanasonicDevice, + PanasonicDeviceEnergy, + PanasonicDeviceZone, + constants, +) from aioaquarea import Device as AquareaDevice -from .const import ( - DOMAIN, - DATA_COORDINATORS, - ENERGY_COORDINATORS, - AQUAREA_COORDINATORS - ) +from .const import DOMAIN, DATA_COORDINATORS, ENERGY_COORDINATORS, AQUAREA_COORDINATORS from .base import PanasonicDataEntity, PanasonicEnergyEntity, AquareaDataEntity -from .coordinator import PanasonicDeviceCoordinator, PanasonicDeviceEnergyCoordinator, AquareaDeviceCoordinator +from .coordinator import ( + PanasonicDeviceCoordinator, + PanasonicDeviceEnergyCoordinator, + AquareaDeviceCoordinator, +) _LOGGER = logging.getLogger(__name__) + @dataclass(frozen=True, kw_only=True) class PanasonicSensorEntityDescription(SensorEntityDescription): """Describes Panasonic sensor entity.""" + get_state: Callable[[PanasonicDevice], Any] | None = None is_available: Callable[[PanasonicDevice], bool] | None = None + @dataclass(frozen=True, kw_only=True) class PanasonicEnergySensorEntityDescription(SensorEntityDescription): """Describes Panasonic sensor entity.""" - get_state: Callable[[PanasonicDeviceEnergy], Any]| None = None + + get_state: Callable[[PanasonicDeviceEnergy], Any] | None = None + @dataclass(frozen=True, kw_only=True) class AquareaSensorEntityDescription(SensorEntityDescription): """Describes Aquarea sensor entity.""" + get_state: Callable[[AquareaDevice], Any] | None = None - is_available: Callable[[AquareaDevice], bool]| None = None + is_available: Callable[[AquareaDevice], bool] | None = None + INSIDE_TEMPERATURE_DESCRIPTION = PanasonicSensorEntityDescription( key="inside_temperature", @@ -86,7 +97,8 @@ class AquareaSensorEntityDescription(SensorEntityDescription): state_class=None, native_unit_of_measurement=None, get_state=lambda device: device.timestamp, - is_available=lambda device: device.info.status_data_mode == constants.StatusDataMode.CACHED, + is_available=lambda device: device.info.status_data_mode + == constants.StatusDataMode.CACHED, entity_registry_enabled_default=False, ) DATA_MODE_DESCRIPTION = PanasonicSensorEntityDescription( @@ -110,7 +122,7 @@ class AquareaSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement="kWh", - get_state=lambda energy: energy.consumption + get_state=lambda energy: energy.consumption, ) DAILY_HEATING_ENERGY_DESCRIPTION = PanasonicEnergySensorEntityDescription( key="daily_heating_energy", @@ -120,7 +132,7 @@ class AquareaSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement="kWh", - get_state=lambda energy: energy.heating_consumption + get_state=lambda energy: energy.heating_consumption, ) DAILY_COOLING_ENERGY_DESCRIPTION = PanasonicEnergySensorEntityDescription( key="daily_cooling_energy", @@ -130,7 +142,7 @@ class AquareaSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.ENERGY, state_class=SensorStateClass.TOTAL_INCREASING, native_unit_of_measurement="kWh", - get_state=lambda energy: energy.cooling_consumption + get_state=lambda energy: energy.cooling_consumption, ) POWER_DESCRIPTION = PanasonicEnergySensorEntityDescription( key="current_power", @@ -140,7 +152,7 @@ class AquareaSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement="W", - get_state=lambda energy: energy.current_power + get_state=lambda energy: energy.current_power, ) COOLING_POWER_DESCRIPTION = PanasonicEnergySensorEntityDescription( key="cooling_power", @@ -150,7 +162,7 @@ class AquareaSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement="W", - get_state=lambda energy: energy.cooling_power + get_state=lambda energy: energy.cooling_power, ) HEATING_POWER_DESCRIPTION = PanasonicEnergySensorEntityDescription( key="heating_power", @@ -160,7 +172,7 @@ class AquareaSensorEntityDescription(SensorEntityDescription): device_class=SensorDeviceClass.POWER, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement="W", - get_state=lambda energy: energy.heating_power + get_state=lambda energy: energy.heating_power, ) AQUAREA_OUTSIDE_TEMPERATURE_DESCRIPTION = AquareaSensorEntityDescription( @@ -175,59 +187,92 @@ class AquareaSensorEntityDescription(SensorEntityDescription): is_available=lambda device: device.temperature_outdoor is not None, ) + def create_zone_temperature_description(zone: PanasonicDeviceZone): return PanasonicSensorEntityDescription( - key = f"zone-{zone.id}-temperature", + key=f"zone-{zone.id}-temperature", translation_key=f"zone-{zone.id}-temperature", - name = f"{zone.name} Temperature", + name=f"{zone.name} Temperature", icon="mdi:thermometer", device_class=SensorDeviceClass.TEMPERATURE, state_class=SensorStateClass.MEASUREMENT, native_unit_of_measurement=UnitOfTemperature.CELSIUS, get_state=lambda device: zone.temperature, - is_available=lambda device: zone.has_temperature + is_available=lambda device: zone.has_temperature, ) async def async_setup_entry(hass, entry, async_add_entities): entities = [] - data_coordinators: list[PanasonicDeviceCoordinator] = hass.data[DOMAIN][DATA_COORDINATORS] - energy_coordinators: list[PanasonicDeviceEnergyCoordinator] = hass.data[DOMAIN][ENERGY_COORDINATORS] - aquarea_coordinators: list[AquareaDeviceCoordinator] = hass.data[DOMAIN][AQUAREA_COORDINATORS] + data_coordinators: list[PanasonicDeviceCoordinator] = hass.data[DOMAIN][ + DATA_COORDINATORS + ] + energy_coordinators: list[PanasonicDeviceEnergyCoordinator] = hass.data[DOMAIN][ + ENERGY_COORDINATORS + ] + aquarea_coordinators: list[AquareaDeviceCoordinator] = hass.data[DOMAIN][ + AQUAREA_COORDINATORS + ] for coordinator in data_coordinators: - entities.append(PanasonicSensorEntity(coordinator, INSIDE_TEMPERATURE_DESCRIPTION)) - entities.append(PanasonicSensorEntity(coordinator, OUTSIDE_TEMPERATURE_DESCRIPTION)) - entities.append(PanasonicSensorEntity(coordinator, LAST_UPDATE_TIME_DESCRIPTION)) + entities.append( + PanasonicSensorEntity(coordinator, INSIDE_TEMPERATURE_DESCRIPTION) + ) + entities.append( + PanasonicSensorEntity(coordinator, OUTSIDE_TEMPERATURE_DESCRIPTION) + ) + entities.append( + PanasonicSensorEntity(coordinator, LAST_UPDATE_TIME_DESCRIPTION) + ) entities.append(PanasonicSensorEntity(coordinator, DATA_AGE_DESCRIPTION)) entities.append(PanasonicSensorEntity(coordinator, DATA_MODE_DESCRIPTION)) if coordinator.device.has_zones: for zone in coordinator.device.parameters.zones: - entities.append(PanasonicSensorEntity( - coordinator, - create_zone_temperature_description(zone))) + entities.append( + PanasonicSensorEntity( + coordinator, create_zone_temperature_description(zone) + ) + ) for coordinator in energy_coordinators: - entities.append(PanasonicEnergySensorEntity(coordinator, DAILY_ENERGY_DESCRIPTION)) - entities.append(PanasonicEnergySensorEntity(coordinator, DAILY_COOLING_ENERGY_DESCRIPTION)) - entities.append(PanasonicEnergySensorEntity(coordinator, DAILY_HEATING_ENERGY_DESCRIPTION)) + entities.append( + PanasonicEnergySensorEntity(coordinator, DAILY_ENERGY_DESCRIPTION) + ) + entities.append( + PanasonicEnergySensorEntity(coordinator, DAILY_COOLING_ENERGY_DESCRIPTION) + ) + entities.append( + PanasonicEnergySensorEntity(coordinator, DAILY_HEATING_ENERGY_DESCRIPTION) + ) entities.append(PanasonicEnergySensorEntity(coordinator, POWER_DESCRIPTION)) - entities.append(PanasonicEnergySensorEntity(coordinator, COOLING_POWER_DESCRIPTION)) - entities.append(PanasonicEnergySensorEntity(coordinator, HEATING_POWER_DESCRIPTION)) + entities.append( + PanasonicEnergySensorEntity(coordinator, COOLING_POWER_DESCRIPTION) + ) + entities.append( + PanasonicEnergySensorEntity(coordinator, HEATING_POWER_DESCRIPTION) + ) for coordinator in aquarea_coordinators: - entities.append(AquareaSensorEntity(coordinator, AQUAREA_OUTSIDE_TEMPERATURE_DESCRIPTION)) + entities.append( + AquareaSensorEntity(coordinator, AQUAREA_OUTSIDE_TEMPERATURE_DESCRIPTION) + ) async_add_entities(entities) class PanasonicSensorEntityBase(SensorEntity): """Base class for all sensor entities.""" - entity_description: PanasonicSensorEntityDescription # type: ignore[override] + + entity_description: PanasonicSensorEntityDescription # type: ignore[override] + class PanasonicSensorEntity(PanasonicDataEntity, PanasonicSensorEntityBase): - - def __init__(self, coordinator: PanasonicDeviceCoordinator, description: PanasonicSensorEntityDescription): + + def __init__( + self, + coordinator: PanasonicDeviceCoordinator, + description: PanasonicSensorEntityDescription, + ): self.entity_description = description super().__init__(coordinator, description.key) @@ -235,21 +280,30 @@ def __init__(self, coordinator: PanasonicDeviceCoordinator, description: Panason def available(self) -> bool: """Return if entity is available.""" if self.entity_description.is_available is None: - return False + return False return self.entity_description.is_available(self.coordinator.device) def _async_update_attrs(self) -> None: """Update the attributes of the sensor.""" if self.entity_description.is_available: - self._attr_available = self.entity_description.is_available(self.coordinator.device) + self._attr_available = self.entity_description.is_available( + self.coordinator.device + ) if self.entity_description.get_state: - self._attr_native_value = self.entity_description.get_state(self.coordinator.device) + self._attr_native_value = self.entity_description.get_state( + self.coordinator.device + ) + class PanasonicEnergySensorEntity(PanasonicEnergyEntity, SensorEntity): - - entity_description: PanasonicEnergySensorEntityDescription # type: ignore[override] - def __init__(self, coordinator: PanasonicDeviceEnergyCoordinator, description: PanasonicEnergySensorEntityDescription): + entity_description: PanasonicEnergySensorEntityDescription # type: ignore[override] + + def __init__( + self, + coordinator: PanasonicDeviceEnergyCoordinator, + description: PanasonicEnergySensorEntityDescription, + ): self.entity_description = description super().__init__(coordinator, description.key) @@ -257,30 +311,43 @@ def __init__(self, coordinator: PanasonicDeviceEnergyCoordinator, description: P def available(self) -> bool: """Return if entity is available.""" return self._attr_available - + def _async_update_attrs(self) -> None: """Update the attributes of the sensor.""" value = self.entity_description.get_state(self.coordinator.energy) self._attr_available = value is not None self._attr_native_value = value + class AquareaSensorEntity(AquareaDataEntity, SensorEntity): - + entity_description: AquareaSensorEntityDescription - def __init__(self, coordinator: AquareaDeviceCoordinator, description: AquareaSensorEntityDescription): + def __init__( + self, + coordinator: AquareaDeviceCoordinator, + description: AquareaSensorEntityDescription, + ): self.entity_description = description super().__init__(coordinator, description.key) @property def available(self) -> bool: """Return if entity is available.""" - value = self.entity_description.is_available(self.coordinator.device) if self.entity_description.is_available else None + value = ( + self.entity_description.is_available(self.coordinator.device) + if self.entity_description.is_available + else None + ) return value if value is not None else False def _async_update_attrs(self) -> None: """Update the attributes of the sensor.""" if self.entity_description.is_available: - self._attr_available = self.entity_description.is_available(self.coordinator.device) + self._attr_available = self.entity_description.is_available( + self.coordinator.device + ) if self.entity_description.get_state: - self._attr_native_value = self.entity_description.get_state(self.coordinator.device) \ No newline at end of file + self._attr_native_value = self.entity_description.get_state( + self.coordinator.device + ) diff --git a/custom_components/panasonic_cc/switch.py b/custom_components/panasonic_cc/switch.py index 56536cc..a9a461d 100644 --- a/custom_components/panasonic_cc/switch.py +++ b/custom_components/panasonic_cc/switch.py @@ -1,21 +1,36 @@ """Support for Panasonic Nanoe.""" + import logging from typing import Callable from dataclasses import dataclass from homeassistant.core import HomeAssistant from homeassistant.config_entries import ConfigEntry -from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity, SwitchEntityDescription -from aio_panasonic_comfort_cloud import constants, PanasonicDevice, PanasonicDeviceZone, ChangeRequestBuilder +from homeassistant.components.switch import ( + SwitchDeviceClass, + SwitchEntity, + SwitchEntityDescription, +) +from aio_panasonic_comfort_cloud import ( + constants, + PanasonicDevice, + PanasonicDeviceZone, + ChangeRequestBuilder, +) from . import DOMAIN -from .const import DATA_COORDINATORS, CONF_FORCE_ENABLE_NANOE,DEFAULT_FORCE_ENABLE_NANOE +from .const import ( + DATA_COORDINATORS, + CONF_FORCE_ENABLE_NANOE, + DEFAULT_FORCE_ENABLE_NANOE, +) from .coordinator import PanasonicDeviceCoordinator from .base import PanasonicDataEntity _LOGGER = logging.getLogger(__name__) + @dataclass(frozen=True, kw_only=True) class PanasonicSwitchEntityDescription(SwitchEntityDescription): """Describes Panasonic Switch entity.""" @@ -31,81 +46,111 @@ class PanasonicSwitchEntityDescription(SwitchEntityDescription): translation_key="nanoe", name="Nanoe", icon="mdi:virus-off", - on_func = lambda builder: builder.set_nanoe_mode(constants.NanoeMode.On), - off_func= lambda builder: builder.set_nanoe_mode(constants.NanoeMode.Off), - get_state = lambda device: device.parameters.nanoe_mode in [constants.NanoeMode.On, constants.NanoeMode.ModeG, constants.NanoeMode.All], - is_available = lambda device: device.has_nanoe + on_func=lambda builder: builder.set_nanoe_mode(constants.NanoeMode.On), + off_func=lambda builder: builder.set_nanoe_mode(constants.NanoeMode.Off), + get_state=lambda device: device.parameters.nanoe_mode + in [constants.NanoeMode.On, constants.NanoeMode.ModeG, constants.NanoeMode.All], + is_available=lambda device: device.has_nanoe, ) ECONAVI_DESCRIPTION = PanasonicSwitchEntityDescription( key="eco-navi", translation_key="eco-navi", name="ECONAVI", icon="mdi:leaf", - on_func = lambda builder: builder.set_eco_navi_mode(constants.EcoNaviMode.On), - off_func= lambda builder: builder.set_eco_navi_mode(constants.EcoNaviMode.Off), - get_state = lambda device: device.parameters.eco_navi_mode == constants.EcoNaviMode.On, - is_available = lambda device: device.has_eco_navi + on_func=lambda builder: builder.set_eco_navi_mode(constants.EcoNaviMode.On), + off_func=lambda builder: builder.set_eco_navi_mode(constants.EcoNaviMode.Off), + get_state=lambda device: device.parameters.eco_navi_mode + == constants.EcoNaviMode.On, + is_available=lambda device: device.has_eco_navi, ) ECO_FUNCTION_DESCRIPTION = PanasonicSwitchEntityDescription( key="eco-function", translation_key="eco-function", name="AI ECO", icon="mdi:leaf", - on_func = lambda builder: builder.set_eco_function_mode(constants.EcoFunctionMode.On), - off_func= lambda builder: builder.set_eco_function_mode(constants.EcoFunctionMode.Off), - get_state = lambda device: device.parameters.eco_function_mode == constants.EcoFunctionMode.On, - is_available = lambda device: device.has_eco_function + on_func=lambda builder: builder.set_eco_function_mode(constants.EcoFunctionMode.On), + off_func=lambda builder: builder.set_eco_function_mode( + constants.EcoFunctionMode.Off + ), + get_state=lambda device: device.parameters.eco_function_mode + == constants.EcoFunctionMode.On, + is_available=lambda device: device.has_eco_function, ) IAUTOX_DESCRIPTION = PanasonicSwitchEntityDescription( key="iauto-x", translation_key="iauto-x", name="iAUTO-X", icon="mdi:snowflake", - on_func = lambda builder: builder.set_iautox_mode(constants.IAutoXMode.On), - off_func= lambda builder: builder.set_iautox_mode(constants.IAutoXMode.Off), - get_state = lambda device: device.parameters.iautox_mode == constants.IAutoXMode.On and device.parameters.mode == constants.OperationMode.Auto, - is_available = lambda device: device.has_iauto_x + on_func=lambda builder: builder.set_iautox_mode(constants.IAutoXMode.On), + off_func=lambda builder: builder.set_iautox_mode(constants.IAutoXMode.Off), + get_state=lambda device: device.parameters.iautox_mode == constants.IAutoXMode.On + and device.parameters.mode == constants.OperationMode.Auto, + is_available=lambda device: device.has_iauto_x, ) + def create_zone_mode_description(zone: PanasonicDeviceZone): return PanasonicSwitchEntityDescription( - key = f"zone-{zone.id}", + key=f"zone-{zone.id}", translation_key=f"zone-{zone.id}", - name = zone.name, + name=zone.name, icon="mdi:thermostat", off_func=lambda builder: builder.set_zone_mode(zone.id, constants.ZoneMode.Off), on_func=lambda builder: builder.set_zone_mode(zone.id, constants.ZoneMode.On), - get_state=lambda device: device.parameters.get_zone(zone.id).mode == constants.ZoneMode.On, - is_available=lambda device: True + get_state=lambda device: device.parameters.get_zone(zone.id).mode + == constants.ZoneMode.On, + is_available=lambda device: True, ) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities): + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +): devices = [] - data_coordinators: list[PanasonicDeviceCoordinator] = hass.data[DOMAIN][DATA_COORDINATORS] - force_enable_nanoe = entry.options.get(CONF_FORCE_ENABLE_NANOE, DEFAULT_FORCE_ENABLE_NANOE) + data_coordinators: list[PanasonicDeviceCoordinator] = hass.data[DOMAIN][ + DATA_COORDINATORS + ] + force_enable_nanoe = entry.options.get( + CONF_FORCE_ENABLE_NANOE, DEFAULT_FORCE_ENABLE_NANOE + ) for data_coordinator in data_coordinators: - devices.append(PanasonicSwitchEntity(data_coordinator, NANOE_DESCRIPTION, always_available=force_enable_nanoe)) + devices.append( + PanasonicSwitchEntity( + data_coordinator, NANOE_DESCRIPTION, always_available=force_enable_nanoe + ) + ) devices.append(PanasonicSwitchEntity(data_coordinator, ECONAVI_DESCRIPTION)) - devices.append(PanasonicSwitchEntity(data_coordinator, ECO_FUNCTION_DESCRIPTION)) + devices.append( + PanasonicSwitchEntity(data_coordinator, ECO_FUNCTION_DESCRIPTION) + ) devices.append(PanasonicSwitchEntity(data_coordinator, IAUTOX_DESCRIPTION)) if data_coordinator.device.has_zones: for zone in data_coordinator.device.parameters.zones: - devices.append(PanasonicSwitchEntity( - data_coordinator, - create_zone_mode_description(zone))) + devices.append( + PanasonicSwitchEntity( + data_coordinator, create_zone_mode_description(zone) + ) + ) async_add_entities(devices) + class PanasonicSwitchEntityBase(SwitchEntity): """Base class for all Panasonic switch entities.""" _attr_device_class = SwitchDeviceClass.SWITCH - entity_description: PanasonicSwitchEntityDescription # type: ignore[override] + entity_description: PanasonicSwitchEntityDescription # type: ignore[override] + class PanasonicSwitchEntity(PanasonicDataEntity, PanasonicSwitchEntityBase): """Representation of a Panasonic switch.""" - def __init__(self, coordinator: PanasonicDeviceCoordinator, description: PanasonicSwitchEntityDescription, always_available: bool = False): + def __init__( + self, + coordinator: PanasonicDeviceCoordinator, + description: PanasonicSwitchEntityDescription, + always_available: bool = False, + ): """Initialize the Switch.""" self.entity_description = description self._always_available = always_available @@ -114,13 +159,16 @@ def __init__(self, coordinator: PanasonicDeviceCoordinator, description: Panason @property def available(self) -> bool: """Return if entity is available.""" - return self._always_available or self.entity_description.is_available(self.coordinator.device) + return self._always_available or self.entity_description.is_available( + self.coordinator.device + ) def _async_update_attrs(self) -> None: """Update the attributes of the sensor.""" - self._attr_available = self.entity_description.is_available(self.coordinator.device) + self._attr_available = self.entity_description.is_available( + self.coordinator.device + ) self._attr_is_on = self.entity_description.get_state(self.coordinator.device) - async def async_turn_on(self, **kwargs): """Turn on the Switch.""" diff --git a/custom_components/panasonic_cc/water_heater.py b/custom_components/panasonic_cc/water_heater.py index 3a92a08..3c54121 100644 --- a/custom_components/panasonic_cc/water_heater.py +++ b/custom_components/panasonic_cc/water_heater.py @@ -1,60 +1,84 @@ """Support for the Aquarea Tank.""" + import logging from dataclasses import dataclass from homeassistant.core import HomeAssistant from homeassistant.config_entries import ConfigEntry -from homeassistant.const import UnitOfTemperature, STATE_OFF, STATE_IDLE, PRECISION_WHOLE, ATTR_TEMPERATURE, MAJOR_VERSION +from homeassistant.const import ( + UnitOfTemperature, + STATE_OFF, + STATE_IDLE, + PRECISION_WHOLE, + ATTR_TEMPERATURE, + MAJOR_VERSION, +) from homeassistant.components.water_heater import ( STATE_HEAT_PUMP, WaterHeaterEntity, - WaterHeaterEntityFeature + WaterHeaterEntityFeature, ) + if MAJOR_VERSION >= 2025: from homeassistant.components.water_heater import WaterHeaterEntityDescription else: - from homeassistant.components.water_heater import WaterHeaterEntityEntityDescription as WaterHeaterEntityDescription + from homeassistant.components.water_heater import ( + WaterHeaterEntityEntityDescription as WaterHeaterEntityDescription, + ) from .base import AquareaDataEntity from .coordinator import AquareaDeviceCoordinator from .const import STATE_HEATING from aioaquarea.data import DeviceAction, OperationStatus -from .const import ( - DOMAIN, - AQUAREA_COORDINATORS) +from .const import DOMAIN, AQUAREA_COORDINATORS _LOGGER = logging.getLogger(__name__) + @dataclass(frozen=True, kw_only=True) class AquareaWaterHeaterEntityDescription(WaterHeaterEntityDescription): """Describes a Aquarea Water Heater entity.""" - + + AQUAREA_WATER_TANK_DESCRIPTION = AquareaWaterHeaterEntityDescription( - key="tank", - translation_key="tank", - name="Tank" + key="tank", translation_key="tank", name="Tank" ) -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry, async_add_entities): + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +): entities = [] - aquarea_coordinators: list[AquareaDeviceCoordinator] = hass.data[DOMAIN][AQUAREA_COORDINATORS] + aquarea_coordinators: list[AquareaDeviceCoordinator] = hass.data[DOMAIN][ + AQUAREA_COORDINATORS + ] for aquarea_coordinator in aquarea_coordinators: if aquarea_coordinator.device.tank is None: continue - entities.append(AquareaWaterHeater(aquarea_coordinator, AQUAREA_WATER_TANK_DESCRIPTION)) + entities.append( + AquareaWaterHeater(aquarea_coordinator, AQUAREA_WATER_TANK_DESCRIPTION) + ) async_add_entities(entities) + class AquareaWaterHeater(AquareaDataEntity, WaterHeaterEntity): """Representation of a Aquarea Water Tank.""" _attr_temperature_unit = UnitOfTemperature.CELSIUS - _attr_supported_features = WaterHeaterEntityFeature.TARGET_TEMPERATURE | WaterHeaterEntityFeature.OPERATION_MODE + _attr_supported_features = ( + WaterHeaterEntityFeature.TARGET_TEMPERATURE + | WaterHeaterEntityFeature.OPERATION_MODE + ) _attr_operation_list = [STATE_HEATING, STATE_OFF] _attr_precision = PRECISION_WHOLE _attr_target_temperature_step = 1 - def __init__(self, coordinator: AquareaDeviceCoordinator, description: AquareaWaterHeaterEntityDescription): + def __init__( + self, + coordinator: AquareaDeviceCoordinator, + description: AquareaWaterHeaterEntityDescription, + ): """Initialize the climate entity.""" self.entity_description = description @@ -76,10 +100,10 @@ def _async_update_attrs(self) -> None: if device.tank.operation_status == OperationStatus.OFF: self._attr_state = STATE_OFF - self._attr_current_operation = STATE_OFF + self._attr_current_operation = STATE_OFF else: self._attr_state = STATE_HEAT_PUMP - + self._attr_current_operation = ( STATE_HEATING if device.current_action == DeviceAction.HEATING_WATER @@ -97,4 +121,4 @@ async def async_set_operation_mode(self, operation_mode): if operation_mode == STATE_HEATING: await self.coordinator.device.tank.turn_on() elif operation_mode == STATE_OFF: - await self.coordinator.device.tank.turn_off() \ No newline at end of file + await self.coordinator.device.tank.turn_off() diff --git a/requirements.txt b/requirements.txt index 5c0c3a5..3d20c04 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ aiohttp aio-panasonic-comfort-cloud==2025.5.1 -aioaquarea==0.7.2 \ No newline at end of file +aioaquarea==1.0.3