diff --git a/homeassistant/components/weheat/__init__.py b/homeassistant/components/weheat/__init__.py index b67c3540dc58bf307c943e8907c8458a0ff8f57c..15935f3e4183fb03d086288a32c2ae1e9135455f 100644 --- a/homeassistant/components/weheat/__init__.py +++ b/homeassistant/components/weheat/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio from http import HTTPStatus import aiohttp @@ -18,7 +19,13 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( ) from .const import API_URL, LOGGER -from .coordinator import WeheatConfigEntry, WeheatDataUpdateCoordinator +from .coordinator import ( + HeatPumpInfo, + WeheatConfigEntry, + WeheatData, + WeheatDataUpdateCoordinator, + WeheatEnergyUpdateCoordinator, +) PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR] @@ -52,14 +59,38 @@ async def async_setup_entry(hass: HomeAssistant, entry: WeheatConfigEntry) -> bo except UnauthorizedException as error: raise ConfigEntryAuthFailed from error + nr_of_pumps = len(discovered_heat_pumps) + for pump_info in discovered_heat_pumps: LOGGER.debug("Adding %s", pump_info) - # for each pump, add a coordinator - new_coordinator = WeheatDataUpdateCoordinator(hass, entry, session, pump_info) + # for each pump, add the coordinators + + new_heat_pump = HeatPumpInfo(pump_info) + new_data_coordinator = WeheatDataUpdateCoordinator( + hass, entry, session, pump_info, nr_of_pumps + ) + new_energy_coordinator = WeheatEnergyUpdateCoordinator( + hass, entry, session, pump_info + ) - await new_coordinator.async_config_entry_first_refresh() + entry.runtime_data.append( + WeheatData( + heat_pump_info=new_heat_pump, + data_coordinator=new_data_coordinator, + energy_coordinator=new_energy_coordinator, + ) + ) - entry.runtime_data.append(new_coordinator) + await asyncio.gather( + *[ + data.data_coordinator.async_config_entry_first_refresh() + for data in entry.runtime_data + ], + *[ + data.energy_coordinator.async_config_entry_first_refresh() + for data in entry.runtime_data + ], + ) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/weheat/binary_sensor.py b/homeassistant/components/weheat/binary_sensor.py index 6a4a03a1e484fdb2ad2b7bdc474e1b86d690d7c1..5e4c91fde60abcf02f94ff9e1eb09dd03f5d18ff 100644 --- a/homeassistant/components/weheat/binary_sensor.py +++ b/homeassistant/components/weheat/binary_sensor.py @@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback from homeassistant.helpers.typing import StateType -from .coordinator import WeheatConfigEntry, WeheatDataUpdateCoordinator +from .coordinator import HeatPumpInfo, WeheatConfigEntry, WeheatDataUpdateCoordinator from .entity import WeheatEntity # Coordinator is used to centralize the data updates @@ -68,10 +68,14 @@ async def async_setup_entry( ) -> None: """Set up the sensors for weheat heat pump.""" entities = [ - WeheatHeatPumpBinarySensor(coordinator, entity_description) + WeheatHeatPumpBinarySensor( + weheatdata.heat_pump_info, + weheatdata.data_coordinator, + entity_description, + ) + for weheatdata in entry.runtime_data for entity_description in BINARY_SENSORS - for coordinator in entry.runtime_data - if entity_description.value_fn(coordinator.data) is not None + if entity_description.value_fn(weheatdata.data_coordinator.data) is not None ] async_add_entities(entities) @@ -80,20 +84,21 @@ async def async_setup_entry( class WeheatHeatPumpBinarySensor(WeheatEntity, BinarySensorEntity): """Defines a Weheat heat pump binary sensor.""" + heat_pump_info: HeatPumpInfo coordinator: WeheatDataUpdateCoordinator entity_description: WeHeatBinarySensorEntityDescription def __init__( self, + heat_pump_info: HeatPumpInfo, coordinator: WeheatDataUpdateCoordinator, entity_description: WeHeatBinarySensorEntityDescription, ) -> None: """Pass coordinator to CoordinatorEntity.""" - super().__init__(coordinator) - + super().__init__(heat_pump_info, coordinator) self.entity_description = entity_description - self._attr_unique_id = f"{coordinator.heatpump_id}_{entity_description.key}" + self._attr_unique_id = f"{heat_pump_info.heatpump_id}_{entity_description.key}" @property def is_on(self) -> bool | None: diff --git a/homeassistant/components/weheat/const.py b/homeassistant/components/weheat/const.py index e33fd98357272da62a361218e847b47ffaf0c68d..ee9b77281e6a9e764285d273aec9080994336bcc 100644 --- a/homeassistant/components/weheat/const.py +++ b/homeassistant/components/weheat/const.py @@ -17,7 +17,8 @@ API_URL = "https://api.weheat.nl" OAUTH2_SCOPES = ["openid", "offline_access"] -UPDATE_INTERVAL = 30 +LOG_UPDATE_INTERVAL = 120 +ENERGY_UPDATE_INTERVAL = 1800 LOGGER: Logger = getLogger(__package__) diff --git a/homeassistant/components/weheat/coordinator.py b/homeassistant/components/weheat/coordinator.py index d7e53258e9b4ad26bd29c3fdbf1de6e9dd4557e2..30ca61d0387a4c27d08af59127bc1b9e9ed8de68 100644 --- a/homeassistant/components/weheat/coordinator.py +++ b/homeassistant/components/weheat/coordinator.py @@ -1,5 +1,6 @@ """Define a custom coordinator for the Weheat heatpump integration.""" +from dataclasses import dataclass from datetime import timedelta from weheat.abstractions.discovery import HeatPumpDiscovery @@ -10,6 +11,7 @@ from weheat.exceptions import ( ForbiddenException, NotFoundException, ServiceException, + TooManyRequestsException, UnauthorizedException, ) @@ -21,7 +23,9 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.config_entry_oauth2_flow import OAuth2Session from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import API_URL, DOMAIN, LOGGER, UPDATE_INTERVAL +from .const import API_URL, DOMAIN, ENERGY_UPDATE_INTERVAL, LOG_UPDATE_INTERVAL, LOGGER + +type WeheatConfigEntry = ConfigEntry[list[WeheatData]] EXCEPTIONS = ( ServiceException, @@ -29,9 +33,43 @@ EXCEPTIONS = ( ForbiddenException, BadRequestException, ApiException, + TooManyRequestsException, ) -type WeheatConfigEntry = ConfigEntry[list[WeheatDataUpdateCoordinator]] + +class HeatPumpInfo(HeatPumpDiscovery.HeatPumpInfo): + """Heat pump info with additional properties.""" + + def __init__(self, pump_info: HeatPumpDiscovery.HeatPumpInfo) -> None: + """Initialize the HeatPump object with the provided pump information. + + Args: + pump_info (HeatPumpDiscovery.HeatPumpInfo): An object containing the heat pump's discovery information, including: + - uuid (str): Unique identifier for the heat pump. + - uuid (str): Unique identifier for the heat pump. + - device_name (str): Name of the heat pump device. + - model (str): Model of the heat pump. + - sn (str): Serial number of the heat pump. + - has_dhw (bool): Indicates if the heat pump has domestic hot water functionality. + + """ + super().__init__( + pump_info.uuid, + pump_info.device_name, + pump_info.model, + pump_info.sn, + pump_info.has_dhw, + ) + + @property + def readable_name(self) -> str | None: + """Return the readable name of the heat pump.""" + return self.device_name if self.device_name else self.model + + @property + def heatpump_id(self) -> str: + """Return the heat pump id.""" + return self.uuid class WeheatDataUpdateCoordinator(DataUpdateCoordinator[HeatPump]): @@ -45,45 +83,70 @@ class WeheatDataUpdateCoordinator(DataUpdateCoordinator[HeatPump]): config_entry: WeheatConfigEntry, session: OAuth2Session, heat_pump: HeatPumpDiscovery.HeatPumpInfo, + nr_of_heat_pumps: int, ) -> None: """Initialize the data coordinator.""" super().__init__( hass, - logger=LOGGER, config_entry=config_entry, + logger=LOGGER, name=DOMAIN, - update_interval=timedelta(seconds=UPDATE_INTERVAL), + update_interval=timedelta(seconds=LOG_UPDATE_INTERVAL * nr_of_heat_pumps), ) - self.heat_pump_info = heat_pump self._heat_pump_data = HeatPump( API_URL, heat_pump.uuid, async_get_clientsession(hass) ) self.session = session - @property - def heatpump_id(self) -> str: - """Return the heat pump id.""" - return self.heat_pump_info.uuid + async def _async_update_data(self) -> HeatPump: + """Fetch data from the API.""" + await self.session.async_ensure_token_valid() - @property - def readable_name(self) -> str | None: - """Return the readable name of the heat pump.""" - if self.heat_pump_info.name: - return self.heat_pump_info.name - return self.heat_pump_info.model + try: + await self._heat_pump_data.async_get_logs( + self.session.token[CONF_ACCESS_TOKEN] + ) + except UnauthorizedException as error: + raise ConfigEntryAuthFailed from error + except EXCEPTIONS as error: + raise UpdateFailed(error) from error - @property - def model(self) -> str: - """Return the model of the heat pump.""" - return self.heat_pump_info.model + return self._heat_pump_data + + +class WeheatEnergyUpdateCoordinator(DataUpdateCoordinator[HeatPump]): + """A custom Energy coordinator for the Weheat heatpump integration.""" + + config_entry: WeheatConfigEntry + + def __init__( + self, + hass: HomeAssistant, + config_entry: WeheatConfigEntry, + session: OAuth2Session, + heat_pump: HeatPumpDiscovery.HeatPumpInfo, + ) -> None: + """Initialize the data coordinator.""" + super().__init__( + hass, + config_entry=config_entry, + logger=LOGGER, + name=DOMAIN, + update_interval=timedelta(seconds=ENERGY_UPDATE_INTERVAL), + ) + self._heat_pump_data = HeatPump( + API_URL, heat_pump.uuid, async_get_clientsession(hass) + ) + + self.session = session async def _async_update_data(self) -> HeatPump: """Fetch data from the API.""" await self.session.async_ensure_token_valid() try: - await self._heat_pump_data.async_get_status( + await self._heat_pump_data.async_get_energy( self.session.token[CONF_ACCESS_TOKEN] ) except UnauthorizedException as error: @@ -92,3 +155,12 @@ class WeheatDataUpdateCoordinator(DataUpdateCoordinator[HeatPump]): raise UpdateFailed(error) from error return self._heat_pump_data + + +@dataclass +class WeheatData: + """Data for the Weheat integration.""" + + heat_pump_info: HeatPumpInfo + data_coordinator: WeheatDataUpdateCoordinator + energy_coordinator: WeheatEnergyUpdateCoordinator diff --git a/homeassistant/components/weheat/entity.py b/homeassistant/components/weheat/entity.py index 079db596e192f9eae19db5459539b2e65fbb2ae8..7a12b2edcfac292ff435cda1aaa2e736094b5e48 100644 --- a/homeassistant/components/weheat/entity.py +++ b/homeassistant/components/weheat/entity.py @@ -3,25 +3,30 @@ from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.update_coordinator import CoordinatorEntity +from . import HeatPumpInfo from .const import DOMAIN, MANUFACTURER -from .coordinator import WeheatDataUpdateCoordinator +from .coordinator import WeheatDataUpdateCoordinator, WeheatEnergyUpdateCoordinator -class WeheatEntity(CoordinatorEntity[WeheatDataUpdateCoordinator]): +class WeheatEntity[ + _WeheatEntityT: WeheatDataUpdateCoordinator | WeheatEnergyUpdateCoordinator +](CoordinatorEntity[_WeheatEntityT]): """Defines a base Weheat entity.""" _attr_has_entity_name = True def __init__( self, - coordinator: WeheatDataUpdateCoordinator, + heat_pump_info: HeatPumpInfo, + coordinator: _WeheatEntityT, ) -> None: """Initialize the Weheat entity.""" super().__init__(coordinator) + self.heat_pump_info = heat_pump_info self._attr_device_info = DeviceInfo( - identifiers={(DOMAIN, coordinator.heatpump_id)}, - name=coordinator.readable_name, + identifiers={(DOMAIN, heat_pump_info.heatpump_id)}, + name=heat_pump_info.readable_name, manufacturer=MANUFACTURER, - model=coordinator.model, + model=heat_pump_info.model, ) diff --git a/homeassistant/components/weheat/manifest.json b/homeassistant/components/weheat/manifest.json index a408303d062c28f42adf20c5851b4ef34ad67b09..7297c6012139fc8fd6ae802b25c2d98d49fcf32c 100644 --- a/homeassistant/components/weheat/manifest.json +++ b/homeassistant/components/weheat/manifest.json @@ -6,5 +6,5 @@ "dependencies": ["application_credentials"], "documentation": "https://www.home-assistant.io/integrations/weheat", "iot_class": "cloud_polling", - "requirements": ["weheat==2025.2.22"] + "requirements": ["weheat==2025.2.26"] } diff --git a/homeassistant/components/weheat/sensor.py b/homeassistant/components/weheat/sensor.py index 615bfd30d18ef059ff8bb3521c0fa30ef5527d84..d3b758e41eb3bf9a20303b407fd6f73ff3f5d2a6 100644 --- a/homeassistant/components/weheat/sensor.py +++ b/homeassistant/components/weheat/sensor.py @@ -27,7 +27,12 @@ from .const import ( DISPLAY_PRECISION_WATER_TEMP, DISPLAY_PRECISION_WATTS, ) -from .coordinator import WeheatConfigEntry, WeheatDataUpdateCoordinator +from .coordinator import ( + HeatPumpInfo, + WeheatConfigEntry, + WeheatDataUpdateCoordinator, + WeheatEnergyUpdateCoordinator, +) from .entity import WeheatEntity # Coordinator is used to centralize the data updates @@ -142,22 +147,6 @@ SENSORS = [ else None ), ), - WeHeatSensorEntityDescription( - translation_key="electricity_used", - key="electricity_used", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - value_fn=lambda status: status.energy_total, - ), - WeHeatSensorEntityDescription( - translation_key="energy_output", - key="energy_output", - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - state_class=SensorStateClass.TOTAL_INCREASING, - value_fn=lambda status: status.energy_output, - ), WeHeatSensorEntityDescription( translation_key="compressor_rpm", key="compressor_rpm", @@ -174,7 +163,6 @@ SENSORS = [ ), ] - DHW_SENSORS = [ WeHeatSensorEntityDescription( translation_key="dhw_top_temperature", @@ -196,6 +184,25 @@ DHW_SENSORS = [ ), ] +ENERGY_SENSORS = [ + WeHeatSensorEntityDescription( + translation_key="electricity_used", + key="electricity_used", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda status: status.energy_total, + ), + WeHeatSensorEntityDescription( + translation_key="energy_output", + key="energy_output", + native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, + device_class=SensorDeviceClass.ENERGY, + state_class=SensorStateClass.TOTAL_INCREASING, + value_fn=lambda status: status.energy_output, + ), +] + async def async_setup_entry( hass: HomeAssistant, @@ -203,17 +210,39 @@ async def async_setup_entry( async_add_entities: AddConfigEntryEntitiesCallback, ) -> None: """Set up the sensors for weheat heat pump.""" - entities = [ - WeheatHeatPumpSensor(coordinator, entity_description) - for entity_description in SENSORS - for coordinator in entry.runtime_data - ] - entities.extend( - WeheatHeatPumpSensor(coordinator, entity_description) - for entity_description in DHW_SENSORS - for coordinator in entry.runtime_data - if coordinator.heat_pump_info.has_dhw - ) + + entities: list[WeheatHeatPumpSensor] = [] + for weheatdata in entry.runtime_data: + entities.extend( + WeheatHeatPumpSensor( + weheatdata.heat_pump_info, + weheatdata.data_coordinator, + entity_description, + ) + for entity_description in SENSORS + if entity_description.value_fn(weheatdata.data_coordinator.data) is not None + ) + if weheatdata.heat_pump_info.has_dhw: + entities.extend( + WeheatHeatPumpSensor( + weheatdata.heat_pump_info, + weheatdata.data_coordinator, + entity_description, + ) + for entity_description in DHW_SENSORS + if entity_description.value_fn(weheatdata.data_coordinator.data) + is not None + ) + entities.extend( + WeheatHeatPumpSensor( + weheatdata.heat_pump_info, + weheatdata.energy_coordinator, + entity_description, + ) + for entity_description in ENERGY_SENSORS + if entity_description.value_fn(weheatdata.energy_coordinator.data) + is not None + ) async_add_entities(entities) @@ -221,20 +250,21 @@ async def async_setup_entry( class WeheatHeatPumpSensor(WeheatEntity, SensorEntity): """Defines a Weheat heat pump sensor.""" - coordinator: WeheatDataUpdateCoordinator + heat_pump_info: HeatPumpInfo + coordinator: WeheatDataUpdateCoordinator | WeheatEnergyUpdateCoordinator entity_description: WeHeatSensorEntityDescription def __init__( self, - coordinator: WeheatDataUpdateCoordinator, + heat_pump_info: HeatPumpInfo, + coordinator: WeheatDataUpdateCoordinator | WeheatEnergyUpdateCoordinator, entity_description: WeHeatSensorEntityDescription, ) -> None: """Pass coordinator to CoordinatorEntity.""" - super().__init__(coordinator) - + super().__init__(heat_pump_info, coordinator) self.entity_description = entity_description - self._attr_unique_id = f"{coordinator.heatpump_id}_{entity_description.key}" + self._attr_unique_id = f"{heat_pump_info.heatpump_id}_{entity_description.key}" @property def native_value(self) -> StateType: diff --git a/requirements_all.txt b/requirements_all.txt index 0c4c22edb09c5193e67410c4a9452881483aaca1..10dda4e324ac978c6555dddb987b4612746cf821 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -3058,7 +3058,7 @@ webio-api==0.1.11 webmin-xmlrpc==0.0.2 # homeassistant.components.weheat -weheat==2025.2.22 +weheat==2025.2.26 # homeassistant.components.whirlpool whirlpool-sixth-sense==0.18.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8500ba955c01758e953360d996a9ba5c1833b54a..866d850c5d59c7c8be4af21bee6961a6e23a23ea 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -2462,7 +2462,7 @@ webio-api==0.1.11 webmin-xmlrpc==0.0.2 # homeassistant.components.weheat -weheat==2025.2.22 +weheat==2025.2.26 # homeassistant.components.whirlpool whirlpool-sixth-sense==0.18.12