From 653fb5b637a55aa383683004bde1c7c12408fb20 Mon Sep 17 00:00:00 2001 From: Tobias Sauerwein Date: Thu, 2 Dec 2021 10:31:54 +0100 Subject: [PATCH] Update Netatmo climate platform (#59974) --- homeassistant/components/netatmo/climate.py | 327 +++---- .../components/netatmo/data_handler.py | 12 +- homeassistant/components/netatmo/helper.py | 24 - .../components/netatmo/netatmo_entity_base.py | 2 +- homeassistant/components/netatmo/select.py | 106 ++- tests/components/netatmo/common.py | 6 +- .../netatmo/fixtures/homesdata.json | 833 +++++++++--------- ... homestatus_91763b24c43d3e344f424e8b.json} | 0 .../homestatus_91763b24c43d3e344f424e8c.json | 12 + tests/components/netatmo/test_climate.py | 51 +- tests/components/netatmo/test_init.py | 3 +- tests/components/netatmo/test_select.py | 4 +- 12 files changed, 626 insertions(+), 754 deletions(-) rename tests/components/netatmo/fixtures/{homestatus.json => homestatus_91763b24c43d3e344f424e8b.json} (100%) create mode 100644 tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8c.json diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 10c8ba182f0..726c0ed43c8 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any, cast +from typing import Any import pyatmo import voluptuous as vol @@ -22,7 +22,6 @@ from homeassistant.components.climate.const import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - ATTR_BATTERY_LEVEL, ATTR_SUGGESTED_AREA, ATTR_TEMPERATURE, PRECISION_HALVES, @@ -32,7 +31,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.device_registry import async_get_registry from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -41,7 +39,6 @@ from .const import ( ATTR_HEATING_POWER_REQUEST, ATTR_SCHEDULE_NAME, ATTR_SELECTED_SCHEDULE, - DATA_DEVICE_IDS, DATA_HANDLER, DATA_HOMES, DATA_SCHEDULES, @@ -50,17 +47,15 @@ from .const import ( EVENT_TYPE_SCHEDULE, EVENT_TYPE_SET_POINT, EVENT_TYPE_THERM_MODE, - MANUFACTURER, SERVICE_SET_SCHEDULE, SIGNAL_NAME, TYPE_ENERGY, ) from .data_handler import ( - HOMEDATA_DATA_CLASS_NAME, - HOMESTATUS_DATA_CLASS_NAME, + CLIMATE_STATE_CLASS_NAME, + CLIMATE_TOPOLOGY_CLASS_NAME, NetatmoDataHandler, ) -from .helper import get_all_home_ids, update_climate_schedules from .netatmo_entity_base import NetatmoBase _LOGGER = logging.getLogger(__name__) @@ -125,44 +120,42 @@ async def async_setup_entry( data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] await data_handler.register_data_class( - HOMEDATA_DATA_CLASS_NAME, HOMEDATA_DATA_CLASS_NAME, None + CLIMATE_TOPOLOGY_CLASS_NAME, CLIMATE_TOPOLOGY_CLASS_NAME, None ) - home_data = data_handler.data.get(HOMEDATA_DATA_CLASS_NAME) + climate_topology = data_handler.data.get(CLIMATE_TOPOLOGY_CLASS_NAME) - if not home_data or home_data.raw_data == {}: + if not climate_topology or climate_topology.raw_data == {}: raise PlatformNotReady entities = [] - for home_id in get_all_home_ids(home_data): - for room_id in home_data.rooms[home_id]: - signal_name = f"{HOMESTATUS_DATA_CLASS_NAME}-{home_id}" - await data_handler.register_data_class( - HOMESTATUS_DATA_CLASS_NAME, signal_name, None, home_id=home_id - ) - home_status = data_handler.data.get(signal_name) - if home_status and room_id in home_status.rooms: - entities.append(NetatmoThermostat(data_handler, home_id, room_id)) - - hass.data[DOMAIN][DATA_SCHEDULES].update( - update_climate_schedules( - home_ids=get_all_home_ids(home_data), - schedules=data_handler.data[HOMEDATA_DATA_CLASS_NAME].schedules, + for home_id in climate_topology.home_ids: + signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}" + await data_handler.register_data_class( + CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id ) - ) + climate_state = data_handler.data[signal_name] + climate_topology.register_handler(home_id, climate_state.process_topology) - hass.data[DOMAIN][DATA_HOMES] = { - home_id: home_data.get("name") - for home_id, home_data in ( - data_handler.data[HOMEDATA_DATA_CLASS_NAME].homes.items() - ) - } + for room in climate_state.homes[home_id].rooms.values(): + if room.device_type is None or room.device_type.value not in [ + NA_THERM, + NA_VALVE, + ]: + continue + entities.append(NetatmoThermostat(data_handler, room)) + + hass.data[DOMAIN][DATA_SCHEDULES][home_id] = climate_state.homes[ + home_id + ].schedules + + hass.data[DOMAIN][DATA_HOMES][home_id] = climate_state.homes[home_id].name _LOGGER.debug("Adding climate devices %s", entities) async_add_entities(entities, True) platform = entity_platform.async_get_current_platform() - if home_data is not None: + if climate_topology is not None: platform.async_register_entity_service( SERVICE_SET_SCHEDULE, {vol.Required(ATTR_SCHEDULE_NAME): cv.string}, @@ -174,67 +167,61 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): """Representation a Netatmo thermostat.""" _attr_hvac_mode = HVAC_MODE_AUTO - _attr_hvac_modes = [HVAC_MODE_AUTO, HVAC_MODE_HEAT] _attr_max_temp = DEFAULT_MAX_TEMP _attr_preset_modes = SUPPORT_PRESET + _attr_supported_features = SUPPORT_FLAGS _attr_target_temperature_step = PRECISION_HALVES _attr_temperature_unit = TEMP_CELSIUS def __init__( - self, data_handler: NetatmoDataHandler, home_id: str, room_id: str + self, data_handler: NetatmoDataHandler, room: pyatmo.climate.NetatmoRoom ) -> None: """Initialize the sensor.""" ClimateEntity.__init__(self) super().__init__(data_handler) - self._id = room_id - self._home_id = home_id + self._room = room + self._id = self._room.entity_id - self._home_status_class = f"{HOMESTATUS_DATA_CLASS_NAME}-{self._home_id}" + self._climate_state_class = ( + f"{CLIMATE_STATE_CLASS_NAME}-{self._room.home.entity_id}" + ) + self._climate_state: pyatmo.AsyncClimate = data_handler.data[ + self._climate_state_class + ] self._data_classes.extend( [ { - "name": HOMEDATA_DATA_CLASS_NAME, - SIGNAL_NAME: HOMEDATA_DATA_CLASS_NAME, + "name": CLIMATE_TOPOLOGY_CLASS_NAME, + SIGNAL_NAME: CLIMATE_TOPOLOGY_CLASS_NAME, }, { - "name": HOMESTATUS_DATA_CLASS_NAME, - "home_id": self._home_id, - SIGNAL_NAME: self._home_status_class, + "name": CLIMATE_STATE_CLASS_NAME, + "home_id": self._room.home.entity_id, + SIGNAL_NAME: self._climate_state_class, }, ] ) - self._home_status = self.data_handler.data[self._home_status_class] - self._room_status = self._home_status.rooms[room_id] - self._room_data: dict = self._data.rooms[home_id][room_id] - - self._model: str = NA_VALVE - for module in self._room_data.get("module_ids", []): - if self._home_status.thermostats.get(module): - self._model = NA_THERM - break + self._model: str = getattr(room.device_type, "value") self._netatmo_type = TYPE_ENERGY - self._device_name = self._data.rooms[home_id][room_id]["name"] - self._attr_name = f"{MANUFACTURER} {self._device_name}" + self._attr_name = self._room.name self._away: bool | None = None - self._support_flags = SUPPORT_FLAGS - self._battery_level = None self._connected: bool | None = None self._away_temperature: float | None = None self._hg_temperature: float | None = None self._boilerstatus: bool | None = None - self._setpoint_duration = None self._selected_schedule = None + self._attr_hvac_modes = [HVAC_MODE_AUTO, HVAC_MODE_HEAT] if self._model == NA_THERM: self._attr_hvac_modes.append(HVAC_MODE_OFF) - self._attr_unique_id = f"{self._id}-{self._model}" + self._attr_unique_id = f"{self._room.entity_id}-{self._model}" async def async_added_to_hass(self) -> None: """Entity created.""" @@ -254,33 +241,32 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): ) ) - registry = await async_get_registry(self.hass) - device = registry.async_get_device({(DOMAIN, self._id)}, set()) - assert device - self.hass.data[DOMAIN][DATA_DEVICE_IDS][self._home_id] = device.id - @callback def handle_event(self, event: dict) -> None: """Handle webhook events.""" data = event["data"] - if self._home_id != data["home_id"]: + if self._room.home.entity_id != data["home_id"]: return if data["event_type"] == EVENT_TYPE_SCHEDULE and "schedule_id" in data: - self._selected_schedule = self.hass.data[DOMAIN][DATA_SCHEDULES][ - self._home_id - ].get(data["schedule_id"]) - self._attr_extra_state_attributes.update( - {"selected_schedule": self._selected_schedule} + self._selected_schedule = getattr( + self.hass.data[DOMAIN][DATA_SCHEDULES][self._room.home.entity_id].get( + data["schedule_id"] + ), + "name", + None, ) + self._attr_extra_state_attributes[ + ATTR_SELECTED_SCHEDULE + ] = self._selected_schedule self.async_write_ha_state() - self.data_handler.async_force_update(self._home_status_class) + self.data_handler.async_force_update(self._climate_state_class) return home = data["home"] - if self._home_id != home["id"]: + if self._room.home.entity_id != home["id"]: return if data["event_type"] == EVENT_TYPE_THERM_MODE: @@ -292,12 +278,15 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self._attr_target_temperature = self._away_temperature elif self._attr_preset_mode == PRESET_SCHEDULE: self.async_update_callback() - self.data_handler.async_force_update(self._home_status_class) + self.data_handler.async_force_update(self._climate_state_class) self.async_write_ha_state() return for room in home.get("rooms", []): - if data["event_type"] == EVENT_TYPE_SET_POINT and self._id == room["id"]: + if ( + data["event_type"] == EVENT_TYPE_SET_POINT + and self._room.entity_id == room["id"] + ): if room["therm_setpoint_mode"] == STATE_NETATMO_OFF: self._attr_hvac_mode = HVAC_MODE_OFF self._attr_preset_mode = STATE_NETATMO_OFF @@ -318,31 +307,21 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): if ( data["event_type"] == EVENT_TYPE_CANCEL_SET_POINT - and self._id == room["id"] + and self._room.entity_id == room["id"] ): self.async_update_callback() self.async_write_ha_state() return - @property - def _data(self) -> pyatmo.AsyncHomeData: - """Return data for this entity.""" - return cast( - pyatmo.AsyncHomeData, self.data_handler.data[self._data_classes[0]["name"]] - ) - - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return self._support_flags - @property def hvac_action(self) -> str | None: """Return the current running hvac operation if supported.""" if self._model == NA_THERM and self._boilerstatus is not None: return CURRENT_HVAC_MAP_NETATMO[self._boilerstatus] # Maybe it is a valve - if self._room_status and self._room_status.get("heating_power_request", 0) > 0: + if ( + heating_req := getattr(self._room, "heating_power_request", 0) + ) is not None and heating_req > 0: return CURRENT_HVAC_HEAT return CURRENT_HVAC_IDLE @@ -363,8 +342,8 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): await self.async_turn_on() if self.target_temperature == 0: - await self._home_status.async_set_room_thermpoint( - self._id, + await self._climate_state.async_set_room_thermpoint( + self._room.entity_id, STATE_NETATMO_HOME, ) @@ -373,15 +352,15 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): and self._model == NA_VALVE and self.hvac_mode == HVAC_MODE_HEAT ): - await self._home_status.async_set_room_thermpoint( - self._id, + await self._climate_state.async_set_room_thermpoint( + self._room.entity_id, STATE_NETATMO_HOME, ) elif ( preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX) and self._model == NA_VALVE ): - await self._home_status.async_set_room_thermpoint( - self._id, + await self._climate_state.async_set_room_thermpoint( + self._room.entity_id, STATE_NETATMO_MANUAL, DEFAULT_MAX_TEMP, ) @@ -389,15 +368,17 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX) and self.hvac_mode == HVAC_MODE_HEAT ): - await self._home_status.async_set_room_thermpoint( - self._id, STATE_NETATMO_HOME + await self._climate_state.async_set_room_thermpoint( + self._room.entity_id, STATE_NETATMO_HOME ) elif preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX): - await self._home_status.async_set_room_thermpoint( - self._id, PRESET_MAP_NETATMO[preset_mode] + await self._climate_state.async_set_room_thermpoint( + self._room.entity_id, PRESET_MAP_NETATMO[preset_mode] ) elif preset_mode in (PRESET_SCHEDULE, PRESET_FROST_GUARD, PRESET_AWAY): - await self._home_status.async_set_thermmode(PRESET_MAP_NETATMO[preset_mode]) + await self._climate_state.async_set_thermmode( + PRESET_MAP_NETATMO[preset_mode] + ) else: _LOGGER.error("Preset mode '%s' not available", preset_mode) @@ -407,8 +388,8 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): """Set new target temperature for 2 hours.""" if (temp := kwargs.get(ATTR_TEMPERATURE)) is None: return - await self._home_status.async_set_room_thermpoint( - self._id, STATE_NETATMO_MANUAL, min(temp, DEFAULT_MAX_TEMP) + await self._climate_state.async_set_room_thermpoint( + self._room.entity_id, STATE_NETATMO_MANUAL, min(temp, DEFAULT_MAX_TEMP) ) self.async_write_ha_state() @@ -416,20 +397,22 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): async def async_turn_off(self) -> None: """Turn the entity off.""" if self._model == NA_VALVE: - await self._home_status.async_set_room_thermpoint( - self._id, + await self._climate_state.async_set_room_thermpoint( + self._room.entity_id, STATE_NETATMO_MANUAL, DEFAULT_MIN_TEMP, ) elif self.hvac_mode != HVAC_MODE_OFF: - await self._home_status.async_set_room_thermpoint( - self._id, STATE_NETATMO_OFF + await self._climate_state.async_set_room_thermpoint( + self._room.entity_id, STATE_NETATMO_OFF ) self.async_write_ha_state() async def async_turn_on(self) -> None: """Turn the entity on.""" - await self._home_status.async_set_room_thermpoint(self._id, STATE_NETATMO_HOME) + await self._climate_state.async_set_room_thermpoint( + self._room.entity_id, STATE_NETATMO_HOME + ) self.async_write_ha_state() @property @@ -440,135 +423,57 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): @callback def async_update_callback(self) -> None: """Update the entity's state.""" - self._home_status = self.data_handler.data[self._home_status_class] - if self._home_status is None: + if not self._room.reachable: if self.available: self._connected = False return - self._room_status = self._home_status.rooms.get(self._id) - self._room_data = self._data.rooms.get(self._home_id, {}).get(self._id, {}) - - if not self._room_status or not self._room_data: - if self._connected: - _LOGGER.info( - "The thermostat in room %s seems to be out of reach", - self._device_name, - ) - - self._connected = False - return - - roomstatus = {"roomID": self._room_status.get("id", {})} - if self._room_status.get("reachable"): - roomstatus.update(self._build_room_status()) - - self._away_temperature = self._data.get_away_temp(self._home_id) - self._hg_temperature = self._data.get_hg_temp(self._home_id) - self._setpoint_duration = self._data.setpoint_duration[self._home_id] - self._selected_schedule = roomstatus.get("selected_schedule") - - if "current_temperature" not in roomstatus: - return - - self._attr_current_temperature = roomstatus["current_temperature"] - self._attr_target_temperature = roomstatus["target_temperature"] - self._attr_preset_mode = NETATMO_MAP_PRESET[roomstatus["setpoint_mode"]] - self._attr_hvac_mode = HVAC_MAP_NETATMO[self._attr_preset_mode] - self._battery_level = roomstatus.get("battery_state") self._connected = True + self._away_temperature = self._room.home.get_away_temp() + self._hg_temperature = self._room.home.get_hg_temp() + self._attr_current_temperature = self._room.therm_measured_temperature + self._attr_target_temperature = self._room.therm_setpoint_temperature + self._attr_preset_mode = NETATMO_MAP_PRESET[ + getattr(self._room, "therm_setpoint_mode", STATE_NETATMO_SCHEDULE) + ] + self._attr_hvac_mode = HVAC_MAP_NETATMO[self._attr_preset_mode] self._away = self._attr_hvac_mode == HVAC_MAP_NETATMO[STATE_NETATMO_AWAY] - if self._battery_level is not None: - self._attr_extra_state_attributes[ATTR_BATTERY_LEVEL] = self._battery_level + self._selected_schedule = getattr( + self._room.home.get_selected_schedule(), "name", None + ) + self._attr_extra_state_attributes[ + ATTR_SELECTED_SCHEDULE + ] = self._selected_schedule if self._model == NA_VALVE: self._attr_extra_state_attributes[ ATTR_HEATING_POWER_REQUEST - ] = self._room_status.get("heating_power_request", 0) - - if self._selected_schedule is not None: - self._attr_extra_state_attributes[ - ATTR_SELECTED_SCHEDULE - ] = self._selected_schedule - - def _build_room_status(self) -> dict: - """Construct room status.""" - try: - roomstatus = { - "roomname": self._room_data["name"], - "target_temperature": self._room_status["therm_setpoint_temperature"], - "setpoint_mode": self._room_status["therm_setpoint_mode"], - "current_temperature": self._room_status["therm_measured_temperature"], - "module_type": self._data.get_thermostat_type( - home_id=self._home_id, room_id=self._id - ), - "module_id": None, - "heating_status": None, - "heating_power_request": None, - "selected_schedule": self._data._get_selected_schedule( # pylint: disable=protected-access - home_id=self._home_id - ).get( - "name" - ), - } - - batterylevel = None - for module_id in self._room_data["module_ids"]: - if ( - self._data.modules[self._home_id][module_id]["type"] == NA_THERM - or roomstatus["module_id"] is None - ): - roomstatus["module_id"] = module_id - if roomstatus["module_type"] == NA_THERM: - self._boilerstatus = self._home_status.boiler_status( - roomstatus["module_id"] - ) - roomstatus["heating_status"] = self._boilerstatus - batterylevel = self._home_status.thermostats[ - roomstatus["module_id"] - ].get("battery_state") - elif roomstatus["module_type"] == NA_VALVE: - roomstatus["heating_power_request"] = self._room_status[ - "heating_power_request" - ] - roomstatus["heating_status"] = roomstatus["heating_power_request"] > 0 - if self._boilerstatus is not None: - roomstatus["heating_status"] = ( - self._boilerstatus and roomstatus["heating_status"] - ) - batterylevel = self._home_status.valves[roomstatus["module_id"]].get( - "battery_state" - ) - - if batterylevel: - roomstatus["battery_state"] = batterylevel - - return roomstatus - - except KeyError as err: - _LOGGER.error("Update of room %s failed. Error: %s", self._id, err) - - return {} + ] = self._room.heating_power_request + else: + for module in self._room.modules.values(): + self._boilerstatus = module.boiler_status + break async def _async_service_set_schedule(self, **kwargs: Any) -> None: schedule_name = kwargs.get(ATTR_SCHEDULE_NAME) schedule_id = None - for sid, name in self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id].items(): - if name == schedule_name: + for sid, schedule in self.hass.data[DOMAIN][DATA_SCHEDULES][ + self._room.home.entity_id + ].items(): + if schedule.name == schedule_name: schedule_id = sid + break if not schedule_id: _LOGGER.error("%s is not a valid schedule", kwargs.get(ATTR_SCHEDULE_NAME)) return - await self._data.async_switch_home_schedule( - home_id=self._home_id, schedule_id=schedule_id - ) + await self._climate_state.async_switch_home_schedule(schedule_id=schedule_id) _LOGGER.debug( "Setting %s schedule to %s (%s)", - self._home_id, + self._room.home.entity_id, kwargs.get(ATTR_SCHEDULE_NAME), schedule_id, ) @@ -577,5 +482,5 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): def device_info(self) -> DeviceInfo: """Return the device info for the thermostat.""" device_info: DeviceInfo = super().device_info - device_info[ATTR_SUGGESTED_AREA] = self._room_data["name"] + device_info[ATTR_SUGGESTED_AREA] = self._room.name return device_info diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py index 8cd0f2047ed..97321b0da53 100644 --- a/homeassistant/components/netatmo/data_handler.py +++ b/homeassistant/components/netatmo/data_handler.py @@ -32,23 +32,23 @@ _LOGGER = logging.getLogger(__name__) CAMERA_DATA_CLASS_NAME = "AsyncCameraData" WEATHERSTATION_DATA_CLASS_NAME = "AsyncWeatherStationData" HOMECOACH_DATA_CLASS_NAME = "AsyncHomeCoachData" -HOMEDATA_DATA_CLASS_NAME = "AsyncHomeData" -HOMESTATUS_DATA_CLASS_NAME = "AsyncHomeStatus" +CLIMATE_TOPOLOGY_CLASS_NAME = "AsyncClimateTopology" +CLIMATE_STATE_CLASS_NAME = "AsyncClimate" PUBLICDATA_DATA_CLASS_NAME = "AsyncPublicData" DATA_CLASSES = { WEATHERSTATION_DATA_CLASS_NAME: pyatmo.AsyncWeatherStationData, HOMECOACH_DATA_CLASS_NAME: pyatmo.AsyncHomeCoachData, CAMERA_DATA_CLASS_NAME: pyatmo.AsyncCameraData, - HOMEDATA_DATA_CLASS_NAME: pyatmo.AsyncHomeData, - HOMESTATUS_DATA_CLASS_NAME: pyatmo.AsyncHomeStatus, + CLIMATE_TOPOLOGY_CLASS_NAME: pyatmo.AsyncClimateTopology, + CLIMATE_STATE_CLASS_NAME: pyatmo.AsyncClimate, PUBLICDATA_DATA_CLASS_NAME: pyatmo.AsyncPublicData, } BATCH_SIZE = 3 DEFAULT_INTERVALS = { - HOMEDATA_DATA_CLASS_NAME: 900, - HOMESTATUS_DATA_CLASS_NAME: 300, + CLIMATE_TOPOLOGY_CLASS_NAME: 3600, + CLIMATE_STATE_CLASS_NAME: 300, CAMERA_DATA_CLASS_NAME: 900, WEATHERSTATION_DATA_CLASS_NAME: 600, HOMECOACH_DATA_CLASS_NAME: 300, diff --git a/homeassistant/components/netatmo/helper.py b/homeassistant/components/netatmo/helper.py index d824013ed27..ea30e059d3a 100644 --- a/homeassistant/components/netatmo/helper.py +++ b/homeassistant/components/netatmo/helper.py @@ -4,8 +4,6 @@ from __future__ import annotations from dataclasses import dataclass from uuid import UUID, uuid4 -import pyatmo - @dataclass class NetatmoArea: @@ -19,25 +17,3 @@ class NetatmoArea: mode: str show_on_map: bool uuid: UUID = uuid4() - - -def get_all_home_ids(home_data: pyatmo.HomeData | None) -> list[str]: - """Get all the home ids returned by NetAtmo API.""" - if home_data is None: - return [] - return [ - home_data.homes[home_id]["id"] - for home_id in home_data.homes - if "modules" in home_data.homes[home_id] - ] - - -def update_climate_schedules(home_ids: list[str], schedules: dict) -> dict: - """Get updated list of all climate schedules.""" - return { - home_id: { - schedule_id: schedule_data.get("name") - for schedule_id, schedule_data in schedules[home_id].items() - } - for home_id in home_ids - } diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py index 5a497275eaf..56f25e04906 100644 --- a/homeassistant/components/netatmo/netatmo_entity_base.py +++ b/homeassistant/components/netatmo/netatmo_entity_base.py @@ -66,7 +66,7 @@ class NetatmoBase(Entity): await self.data_handler.unregister_data_class(signal_name, None) registry = await self.hass.helpers.device_registry.async_get_registry() - device = registry.async_get_device({(DOMAIN, self._id)}, set()) + device = registry.async_get_device({(DOMAIN, self._id)}) self.hass.data[DOMAIN][DATA_DEVICE_IDS][self._id] = device.id self.async_update_callback() diff --git a/homeassistant/components/netatmo/select.py b/homeassistant/components/netatmo/select.py index f5ab43bbd12..9902155be73 100644 --- a/homeassistant/components/netatmo/select.py +++ b/homeassistant/components/netatmo/select.py @@ -2,7 +2,6 @@ from __future__ import annotations import logging -from typing import cast import pyatmo @@ -22,8 +21,11 @@ from .const import ( SIGNAL_NAME, TYPE_ENERGY, ) -from .data_handler import HOMEDATA_DATA_CLASS_NAME, NetatmoDataHandler -from .helper import get_all_home_ids, update_climate_schedules +from .data_handler import ( + CLIMATE_STATE_CLASS_NAME, + CLIMATE_TOPOLOGY_CLASS_NAME, + NetatmoDataHandler, +) from .netatmo_entity_base import NetatmoBase _LOGGER = logging.getLogger(__name__) @@ -36,25 +38,34 @@ async def async_setup_entry( data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] await data_handler.register_data_class( - HOMEDATA_DATA_CLASS_NAME, HOMEDATA_DATA_CLASS_NAME, None + CLIMATE_TOPOLOGY_CLASS_NAME, CLIMATE_TOPOLOGY_CLASS_NAME, None ) - home_data = data_handler.data.get(HOMEDATA_DATA_CLASS_NAME) + climate_topology = data_handler.data.get(CLIMATE_TOPOLOGY_CLASS_NAME) - if not home_data or home_data.raw_data == {}: + if not climate_topology or climate_topology.raw_data == {}: raise PlatformNotReady - hass.data[DOMAIN][DATA_SCHEDULES].update( - update_climate_schedules( - home_ids=get_all_home_ids(home_data), - schedules=data_handler.data[HOMEDATA_DATA_CLASS_NAME].schedules, + entities = [] + for home_id in climate_topology.home_ids: + signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}" + await data_handler.register_data_class( + CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id ) - ) + climate_state = data_handler.data.get(signal_name) + climate_topology.register_handler(home_id, climate_state.process_topology) + + hass.data[DOMAIN][DATA_SCHEDULES][home_id] = climate_state.homes[ + home_id + ].schedules entities = [ NetatmoScheduleSelect( data_handler, home_id, - list(hass.data[DOMAIN][DATA_SCHEDULES][home_id].values()), + [ + schedule.name + for schedule in hass.data[DOMAIN][DATA_SCHEDULES][home_id].values() + ], ) for home_id in hass.data[DOMAIN][DATA_SCHEDULES] ] @@ -75,16 +86,28 @@ class NetatmoScheduleSelect(NetatmoBase, SelectEntity): self._home_id = home_id + self._climate_state_class = f"{CLIMATE_STATE_CLASS_NAME}-{self._home_id}" + self._climate_state: pyatmo.AsyncClimate = data_handler.data[ + self._climate_state_class + ] + + self._home = self._climate_state.homes[self._home_id] + self._data_classes.extend( [ { - "name": HOMEDATA_DATA_CLASS_NAME, - SIGNAL_NAME: HOMEDATA_DATA_CLASS_NAME, + "name": CLIMATE_TOPOLOGY_CLASS_NAME, + SIGNAL_NAME: CLIMATE_TOPOLOGY_CLASS_NAME, + }, + { + "name": CLIMATE_STATE_CLASS_NAME, + "home_id": self._home_id, + SIGNAL_NAME: self._climate_state_class, }, ] ) - self._device_name = self._data.homes[home_id]["name"] + self._device_name = self._home.name self._attr_name = f"{MANUFACTURER} {self._device_name}" self._model: str = "NATherm1" @@ -92,9 +115,7 @@ class NetatmoScheduleSelect(NetatmoBase, SelectEntity): self._attr_unique_id = f"{self._home_id}-schedule-select" - self._attr_current_option = self._data._get_selected_schedule( - home_id=self._home_id - ).get("name") + self._attr_current_option = getattr(self._home.get_selected_schedule(), "name") self._attr_options = options async def async_added_to_hass(self) -> None: @@ -119,23 +140,20 @@ class NetatmoScheduleSelect(NetatmoBase, SelectEntity): return if data["event_type"] == EVENT_TYPE_SCHEDULE and "schedule_id" in data: - self._attr_current_option = self.hass.data[DOMAIN][DATA_SCHEDULES][ - self._home_id - ].get(data["schedule_id"]) + self._attr_current_option = getattr( + self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id].get( + data["schedule_id"] + ), + "name", + ) self.async_write_ha_state() - @property - def _data(self) -> pyatmo.AsyncHomeData: - """Return data for this entity.""" - return cast( - pyatmo.AsyncHomeData, - self.data_handler.data[self._data_classes[0]["name"]], - ) - async def async_select_option(self, option: str) -> None: """Change the selected option.""" - for sid, name in self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id].items(): - if name != option: + for sid, schedule in self.hass.data[DOMAIN][DATA_SCHEDULES][ + self._home_id + ].items(): + if schedule.name != option: continue _LOGGER.debug( "Setting %s schedule to %s (%s)", @@ -143,25 +161,17 @@ class NetatmoScheduleSelect(NetatmoBase, SelectEntity): option, sid, ) - await self._data.async_switch_home_schedule( - home_id=self._home_id, schedule_id=sid - ) + await self._climate_state.async_switch_home_schedule(schedule_id=sid) break @callback def async_update_callback(self) -> None: """Update the entity's state.""" - self._attr_current_option = ( - self._data._get_selected_schedule( # pylint: disable=protected-access - home_id=self._home_id - ).get("name") - ) - self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id] = { - schedule_id: schedule_data.get("name") - for schedule_id, schedule_data in ( - self._data.schedules[self._home_id].items() - ) - } - self._attr_options = list( - self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id].values() - ) + self._attr_current_option = getattr(self._home.get_selected_schedule(), "name") + self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id] = self._home.schedules + self._attr_options = [ + schedule.name + for schedule in self.hass.data[DOMAIN][DATA_SCHEDULES][ + self._home_id + ].values() + ] diff --git a/tests/components/netatmo/common.py b/tests/components/netatmo/common.py index f2c03ac7de1..55083171a1a 100644 --- a/tests/components/netatmo/common.py +++ b/tests/components/netatmo/common.py @@ -51,7 +51,7 @@ async def fake_post_request(*args, **kwargs): if endpoint in "snapshot_720.jpg": return b"test stream image bytes" - elif endpoint in [ + if endpoint in [ "setpersonsaway", "setpersonshome", "setstate", @@ -61,6 +61,10 @@ async def fake_post_request(*args, **kwargs): ]: payload = f'{{"{endpoint}": true}}' + elif endpoint == "homestatus": + home_id = kwargs.get("params", {}).get("home_id") + payload = json.loads(load_fixture(f"netatmo/{endpoint}_{home_id}.json")) + else: payload = json.loads(load_fixture(f"netatmo/{endpoint}.json")) diff --git a/tests/components/netatmo/fixtures/homesdata.json b/tests/components/netatmo/fixtures/homesdata.json index 9c5e985218f..fd63a0c200f 100644 --- a/tests/components/netatmo/fixtures/homesdata.json +++ b/tests/components/netatmo/fixtures/homesdata.json @@ -1,430 +1,407 @@ { - "body": { - "homes": [ - { - "id": "91763b24c43d3e344f424e8b", - "name": "MYHOME", - "altitude": 112, - "coordinates": [ - 52.516263, - 13.377726 - ], - "country": "DE", - "timezone": "Europe/Berlin", - "rooms": [ - { - "id": "2746182631", - "name": "Livingroom", - "type": "livingroom", - "module_ids": [ - "12:34:56:00:01:ae" - ] - }, - { - "id": "3688132631", - "name": "Hall", - "type": "custom", - "module_ids": [ - "12:34:56:00:f1:62" - ] - }, - { - "id": "2833524037", - "name": "Entrada", - "type": "lobby", - "module_ids": [ - "12:34:56:03:a5:54" - ] - }, - { - "id": "2940411577", - "name": "Cocina", - "type": "kitchen", - "module_ids": [ - "12:34:56:03:a0:ac" - ] - } - ], - "modules": [ - { - "id": "12:34:56:00:fa:d0", - "type": "NAPlug", - "name": "Thermostat", - "setup_date": 1494963356, - "modules_bridged": [ - "12:34:56:00:01:ae", - "12:34:56:03:a0:ac", - "12:34:56:03:a5:54" - ] - }, - { - "id": "12:34:56:00:01:ae", - "type": "NATherm1", - "name": "Livingroom", - "setup_date": 1494963356, - "room_id": "2746182631", - "bridge": "12:34:56:00:fa:d0" - }, - { - "id": "12:34:56:03:a5:54", - "type": "NRV", - "name": "Valve1", - "setup_date": 1554549767, - "room_id": "2833524037", - "bridge": "12:34:56:00:fa:d0" - }, - { - "id": "12:34:56:03:a0:ac", - "type": "NRV", - "name": "Valve2", - "setup_date": 1554554444, - "room_id": "2940411577", - "bridge": "12:34:56:00:fa:d0" - }, - { - "id": "12:34:56:00:f1:62", - "type": "NACamera", - "name": "Hall", - "setup_date": 1544828430, - "room_id": "3688132631" - } - ], - "schedules": [ - { - "zones": [ - { - "type": 0, - "name": "Comfort", - "rooms_temp": [ - { - "temp": 21, - "room_id": "2746182631" - } - ], - "id": 0 - }, - { - "type": 1, - "name": "Night", - "rooms_temp": [ - { - "temp": 17, - "room_id": "2746182631" - } - ], - "id": 1 - }, - { - "type": 5, - "name": "Eco", - "rooms_temp": [ - { - "temp": 17, - "room_id": "2746182631" - } - ], - "id": 4 - } - ], - "timetable": [ - { - "zone_id": 1, - "m_offset": 0 - }, - { - "zone_id": 0, - "m_offset": 360 - }, - { - "zone_id": 4, - "m_offset": 420 - }, - { - "zone_id": 0, - "m_offset": 960 - }, - { - "zone_id": 1, - "m_offset": 1410 - }, - { - "zone_id": 0, - "m_offset": 1800 - }, - { - "zone_id": 4, - "m_offset": 1860 - }, - { - "zone_id": 0, - "m_offset": 2400 - }, - { - "zone_id": 1, - "m_offset": 2850 - }, - { - "zone_id": 0, - "m_offset": 3240 - }, - { - "zone_id": 4, - "m_offset": 3300 - }, - { - "zone_id": 0, - "m_offset": 3840 - }, - { - "zone_id": 1, - "m_offset": 4290 - }, - { - "zone_id": 0, - "m_offset": 4680 - }, - { - "zone_id": 4, - "m_offset": 4740 - }, - { - "zone_id": 0, - "m_offset": 5280 - }, - { - "zone_id": 1, - "m_offset": 5730 - }, - { - "zone_id": 0, - "m_offset": 6120 - }, - { - "zone_id": 4, - "m_offset": 6180 - }, - { - "zone_id": 0, - "m_offset": 6720 - }, - { - "zone_id": 1, - "m_offset": 7170 - }, - { - "zone_id": 0, - "m_offset": 7620 - }, - { - "zone_id": 1, - "m_offset": 8610 - }, - { - "zone_id": 0, - "m_offset": 9060 - }, - { - "zone_id": 1, - "m_offset": 10050 - } - ], - "hg_temp": 7, - "away_temp": 14, - "name": "Default", - "selected": true, - "id": "591b54a2764ff4d50d8b5795", - "type": "therm" - }, - { - "zones": [ - { - "type": 0, - "name": "Comfort", - "rooms_temp": [ - { - "temp": 21, - "room_id": "2746182631" - } - ], - "id": 0 - }, - { - "type": 1, - "name": "Night", - "rooms_temp": [ - { - "temp": 17, - "room_id": "2746182631" - } - ], - "id": 1 - }, - { - "type": 5, - "name": "Eco", - "rooms_temp": [ - { - "temp": 17, - "room_id": "2746182631" - } - ], - "id": 4 - } - ], - "timetable": [ - { - "zone_id": 1, - "m_offset": 0 - }, - { - "zone_id": 0, - "m_offset": 360 - }, - { - "zone_id": 4, - "m_offset": 420 - }, - { - "zone_id": 0, - "m_offset": 960 - }, - { - "zone_id": 1, - "m_offset": 1410 - }, - { - "zone_id": 0, - "m_offset": 1800 - }, - { - "zone_id": 4, - "m_offset": 1860 - }, - { - "zone_id": 0, - "m_offset": 2400 - }, - { - "zone_id": 1, - "m_offset": 2850 - }, - { - "zone_id": 0, - "m_offset": 3240 - }, - { - "zone_id": 4, - "m_offset": 3300 - }, - { - "zone_id": 0, - "m_offset": 3840 - }, - { - "zone_id": 1, - "m_offset": 4290 - }, - { - "zone_id": 0, - "m_offset": 4680 - }, - { - "zone_id": 4, - "m_offset": 4740 - }, - { - "zone_id": 0, - "m_offset": 5280 - }, - { - "zone_id": 1, - "m_offset": 5730 - }, - { - "zone_id": 0, - "m_offset": 6120 - }, - { - "zone_id": 4, - "m_offset": 6180 - }, - { - "zone_id": 0, - "m_offset": 6720 - }, - { - "zone_id": 1, - "m_offset": 7170 - }, - { - "zone_id": 0, - "m_offset": 7620 - }, - { - "zone_id": 1, - "m_offset": 8610 - }, - { - "zone_id": 0, - "m_offset": 9060 - }, - { - "zone_id": 1, - "m_offset": 10050 - } - ], - "hg_temp": 7, - "away_temp": 14, - "name": "Winter", - "id": "b1b54a2f45795764f59d50d8", - "type": "therm" - } - ], - "therm_setpoint_default_duration": 120, - "persons": [ - { - "id": "91827374-7e04-5298-83ad-a0cb8372dff1", - "pseudo": "John Doe", - "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d7" - }, - { - "id": "91827375-7e04-5298-83ae-a0cb8372dff2", - "pseudo": "Jane Doe", - "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72" - }, - { - "id": "91827376-7e04-5298-83af-a0cb8372dff3", - "pseudo": "Richard Doe", - "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c2d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d74b808a89f8" - } - ], - "therm_mode": "schedule" - }, - { - "id": "91763b24c43d3e344f424e8c", - "altitude": 112, - "coordinates": [ - 52.516263, - 13.377726 - ], - "country": "DE", - "timezone": "Europe/Berlin", - "therm_setpoint_default_duration": 180, - "therm_mode": "schedule" - } + "body": { + "homes": [ + { + "id": "91763b24c43d3e344f424e8b", + "name": "MYHOME", + "altitude": 112, + "coordinates": [52.516263, 13.377726], + "country": "DE", + "timezone": "Europe/Berlin", + "rooms": [ + { + "id": "2746182631", + "name": "Livingroom", + "type": "livingroom", + "module_ids": ["12:34:56:00:01:ae"] + }, + { + "id": "3688132631", + "name": "Hall", + "type": "custom", + "module_ids": ["12:34:56:00:f1:62"] + }, + { + "id": "2833524037", + "name": "Entrada", + "type": "lobby", + "module_ids": ["12:34:56:03:a5:54"] + }, + { + "id": "2940411577", + "name": "Cocina", + "type": "kitchen", + "module_ids": ["12:34:56:03:a0:ac"] + } ], - "user": { - "email": "john@doe.com", - "language": "de-DE", - "locale": "de-DE", - "feel_like_algorithm": 0, - "unit_pressure": 0, - "unit_system": 0, - "unit_wind": 0, - "id": "91763b24c43d3e344f424e8b" - } - }, - "status": "ok", - "time_exec": 0.056135892868042, - "time_server": 1559171003 -} \ No newline at end of file + "modules": [ + { + "id": "12:34:56:00:fa:d0", + "type": "NAPlug", + "name": "Thermostat", + "setup_date": 1494963356, + "modules_bridged": [ + "12:34:56:00:01:ae", + "12:34:56:03:a0:ac", + "12:34:56:03:a5:54" + ] + }, + { + "id": "12:34:56:00:01:ae", + "type": "NATherm1", + "name": "Livingroom", + "setup_date": 1494963356, + "room_id": "2746182631", + "bridge": "12:34:56:00:fa:d0" + }, + { + "id": "12:34:56:03:a5:54", + "type": "NRV", + "name": "Valve1", + "setup_date": 1554549767, + "room_id": "2833524037", + "bridge": "12:34:56:00:fa:d0" + }, + { + "id": "12:34:56:03:a0:ac", + "type": "NRV", + "name": "Valve2", + "setup_date": 1554554444, + "room_id": "2940411577", + "bridge": "12:34:56:00:fa:d0" + }, + { + "id": "12:34:56:00:f1:62", + "type": "NACamera", + "name": "Hall", + "setup_date": 1544828430, + "room_id": "3688132631" + } + ], + "schedules": [ + { + "zones": [ + { + "type": 0, + "name": "Comfort", + "rooms_temp": [ + { + "temp": 21, + "room_id": "2746182631" + } + ], + "id": 0 + }, + { + "type": 1, + "name": "Night", + "rooms_temp": [ + { + "temp": 17, + "room_id": "2746182631" + } + ], + "id": 1 + }, + { + "type": 5, + "name": "Eco", + "rooms_temp": [ + { + "temp": 17, + "room_id": "2746182631" + } + ], + "id": 4 + } + ], + "timetable": [ + { + "zone_id": 1, + "m_offset": 0 + }, + { + "zone_id": 0, + "m_offset": 360 + }, + { + "zone_id": 4, + "m_offset": 420 + }, + { + "zone_id": 0, + "m_offset": 960 + }, + { + "zone_id": 1, + "m_offset": 1410 + }, + { + "zone_id": 0, + "m_offset": 1800 + }, + { + "zone_id": 4, + "m_offset": 1860 + }, + { + "zone_id": 0, + "m_offset": 2400 + }, + { + "zone_id": 1, + "m_offset": 2850 + }, + { + "zone_id": 0, + "m_offset": 3240 + }, + { + "zone_id": 4, + "m_offset": 3300 + }, + { + "zone_id": 0, + "m_offset": 3840 + }, + { + "zone_id": 1, + "m_offset": 4290 + }, + { + "zone_id": 0, + "m_offset": 4680 + }, + { + "zone_id": 4, + "m_offset": 4740 + }, + { + "zone_id": 0, + "m_offset": 5280 + }, + { + "zone_id": 1, + "m_offset": 5730 + }, + { + "zone_id": 0, + "m_offset": 6120 + }, + { + "zone_id": 4, + "m_offset": 6180 + }, + { + "zone_id": 0, + "m_offset": 6720 + }, + { + "zone_id": 1, + "m_offset": 7170 + }, + { + "zone_id": 0, + "m_offset": 7620 + }, + { + "zone_id": 1, + "m_offset": 8610 + }, + { + "zone_id": 0, + "m_offset": 9060 + }, + { + "zone_id": 1, + "m_offset": 10050 + } + ], + "hg_temp": 7, + "away_temp": 14, + "name": "Default", + "selected": true, + "id": "591b54a2764ff4d50d8b5795", + "type": "therm" + }, + { + "zones": [ + { + "type": 0, + "name": "Comfort", + "rooms_temp": [ + { + "temp": 21, + "room_id": "2746182631" + } + ], + "id": 0 + }, + { + "type": 1, + "name": "Night", + "rooms_temp": [ + { + "temp": 17, + "room_id": "2746182631" + } + ], + "id": 1 + }, + { + "type": 5, + "name": "Eco", + "rooms_temp": [ + { + "temp": 17, + "room_id": "2746182631" + } + ], + "id": 4 + } + ], + "timetable": [ + { + "zone_id": 1, + "m_offset": 0 + }, + { + "zone_id": 0, + "m_offset": 360 + }, + { + "zone_id": 4, + "m_offset": 420 + }, + { + "zone_id": 0, + "m_offset": 960 + }, + { + "zone_id": 1, + "m_offset": 1410 + }, + { + "zone_id": 0, + "m_offset": 1800 + }, + { + "zone_id": 4, + "m_offset": 1860 + }, + { + "zone_id": 0, + "m_offset": 2400 + }, + { + "zone_id": 1, + "m_offset": 2850 + }, + { + "zone_id": 0, + "m_offset": 3240 + }, + { + "zone_id": 4, + "m_offset": 3300 + }, + { + "zone_id": 0, + "m_offset": 3840 + }, + { + "zone_id": 1, + "m_offset": 4290 + }, + { + "zone_id": 0, + "m_offset": 4680 + }, + { + "zone_id": 4, + "m_offset": 4740 + }, + { + "zone_id": 0, + "m_offset": 5280 + }, + { + "zone_id": 1, + "m_offset": 5730 + }, + { + "zone_id": 0, + "m_offset": 6120 + }, + { + "zone_id": 4, + "m_offset": 6180 + }, + { + "zone_id": 0, + "m_offset": 6720 + }, + { + "zone_id": 1, + "m_offset": 7170 + }, + { + "zone_id": 0, + "m_offset": 7620 + }, + { + "zone_id": 1, + "m_offset": 8610 + }, + { + "zone_id": 0, + "m_offset": 9060 + }, + { + "zone_id": 1, + "m_offset": 10050 + } + ], + "hg_temp": 7, + "away_temp": 14, + "name": "Winter", + "id": "b1b54a2f45795764f59d50d8", + "type": "therm" + } + ], + "therm_setpoint_default_duration": 120, + "persons": [ + { + "id": "91827374-7e04-5298-83ad-a0cb8372dff1", + "pseudo": "John Doe", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d7" + }, + { + "id": "91827375-7e04-5298-83ae-a0cb8372dff2", + "pseudo": "Jane Doe", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c24b808a89f8d1730039d2c69928b029d67fc40cb2d7fb69ecdf2bb8b72" + }, + { + "id": "91827376-7e04-5298-83af-a0cb8372dff3", + "pseudo": "Richard Doe", + "url": "https://netatmocameraimage.blob.core.windows.net/production/d74fad765b9100ef480720a9a4a95c2d1730fb69ecdf2bb8b72039d2c69928b029d67fc40cb2d74b808a89f8" + } + ], + "therm_mode": "schedule" + } + ], + "user": { + "email": "john@doe.com", + "language": "de-DE", + "locale": "de-DE", + "feel_like_algorithm": 0, + "unit_pressure": 0, + "unit_system": 0, + "unit_wind": 0, + "id": "91763b24c43d3e344f424e8b" + } + }, + "status": "ok", + "time_exec": 0.056135892868042, + "time_server": 1559171003 +} diff --git a/tests/components/netatmo/fixtures/homestatus.json b/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8b.json similarity index 100% rename from tests/components/netatmo/fixtures/homestatus.json rename to tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8b.json diff --git a/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8c.json b/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8c.json new file mode 100644 index 00000000000..d950c82a6a5 --- /dev/null +++ b/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8c.json @@ -0,0 +1,12 @@ +{ + "status": "ok", + "time_server": 1559292041, + "body": { + "home": { + "modules": [], + "rooms": [], + "id": "91763b24c43d3e344f424e8c", + "persons": [] + } + } +} diff --git a/tests/components/netatmo/test_climate.py b/tests/components/netatmo/test_climate.py index ef7f8884e2e..b61081252a0 100644 --- a/tests/components/netatmo/test_climate.py +++ b/tests/components/netatmo/test_climate.py @@ -1,5 +1,5 @@ """The tests for the Netatmo climate platform.""" -from unittest.mock import Mock, patch +from unittest.mock import patch from homeassistant.components.climate import ( DOMAIN as CLIMATE_DOMAIN, @@ -18,7 +18,6 @@ from homeassistant.components.climate.const import ( PRESET_AWAY, PRESET_BOOST, ) -from homeassistant.components.netatmo import climate from homeassistant.components.netatmo.climate import PRESET_FROST_GUARD, PRESET_SCHEDULE from homeassistant.components.netatmo.const import ( ATTR_SCHEDULE_NAME, @@ -37,7 +36,7 @@ async def test_webhook_event_handling_thermostats(hass, config_entry, netatmo_au await hass.async_block_till_done() webhook_id = config_entry.data[CONF_WEBHOOK_ID] - climate_entity_livingroom = "climate.netatmo_livingroom" + climate_entity_livingroom = "climate.livingroom" assert hass.states.get(climate_entity_livingroom).state == "auto" assert ( @@ -214,7 +213,7 @@ async def test_service_preset_mode_frost_guard_thermostat( await hass.async_block_till_done() webhook_id = config_entry.data[CONF_WEBHOOK_ID] - climate_entity_livingroom = "climate.netatmo_livingroom" + climate_entity_livingroom = "climate.livingroom" assert hass.states.get(climate_entity_livingroom).state == "auto" assert ( @@ -287,7 +286,7 @@ async def test_service_preset_modes_thermostat(hass, config_entry, netatmo_auth) await hass.async_block_till_done() webhook_id = config_entry.data[CONF_WEBHOOK_ID] - climate_entity_livingroom = "climate.netatmo_livingroom" + climate_entity_livingroom = "climate.livingroom" assert hass.states.get(climate_entity_livingroom).state == "auto" assert ( @@ -415,11 +414,11 @@ async def test_service_schedule_thermostats(hass, config_entry, caplog, netatmo_ await hass.async_block_till_done() webhook_id = config_entry.data[CONF_WEBHOOK_ID] - climate_entity_livingroom = "climate.netatmo_livingroom" + climate_entity_livingroom = "climate.livingroom" # Test setting a valid schedule with patch( - "pyatmo.thermostat.AsyncHomeData.async_switch_home_schedule" + "pyatmo.climate.AsyncClimate.async_switch_home_schedule" ) as mock_switch_home_schedule: await hass.services.async_call( "netatmo", @@ -429,7 +428,7 @@ async def test_service_schedule_thermostats(hass, config_entry, caplog, netatmo_ ) await hass.async_block_till_done() mock_switch_home_schedule.assert_called_once_with( - home_id="91763b24c43d3e344f424e8b", schedule_id="b1b54a2f45795764f59d50d8" + schedule_id="b1b54a2f45795764f59d50d8" ) # Fake backend response for valve being turned on @@ -448,7 +447,7 @@ async def test_service_schedule_thermostats(hass, config_entry, caplog, netatmo_ # Test setting an invalid schedule with patch( - "pyatmo.thermostat.AsyncHomeData.async_switch_home_schedule" + "pyatmo.climate.AsyncClimate.async_switch_home_schedule" ) as mock_switch_home_schedule: await hass.services.async_call( "netatmo", @@ -472,7 +471,7 @@ async def test_service_preset_mode_already_boost_valves( await hass.async_block_till_done() webhook_id = config_entry.data[CONF_WEBHOOK_ID] - climate_entity_entrada = "climate.netatmo_entrada" + climate_entity_entrada = "climate.entrada" assert hass.states.get(climate_entity_entrada).state == "auto" assert ( @@ -550,7 +549,7 @@ async def test_service_preset_mode_boost_valves(hass, config_entry, netatmo_auth await hass.async_block_till_done() webhook_id = config_entry.data[CONF_WEBHOOK_ID] - climate_entity_entrada = "climate.netatmo_entrada" + climate_entity_entrada = "climate.entrada" # Test service setting the preset mode to "boost" assert hass.states.get(climate_entity_entrada).state == "auto" @@ -602,7 +601,7 @@ async def test_service_preset_mode_invalid(hass, config_entry, caplog, netatmo_a await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: "climate.netatmo_cocina", ATTR_PRESET_MODE: "invalid"}, + {ATTR_ENTITY_ID: "climate.cocina", ATTR_PRESET_MODE: "invalid"}, blocking=True, ) await hass.async_block_till_done() @@ -618,7 +617,12 @@ async def test_valves_service_turn_off(hass, config_entry, netatmo_auth): await hass.async_block_till_done() webhook_id = config_entry.data[CONF_WEBHOOK_ID] - climate_entity_entrada = "climate.netatmo_entrada" + climate_entity_entrada = "climate.entrada" + + assert hass.states.get(climate_entity_entrada).attributes["hvac_modes"] == [ + "auto", + "heat", + ] # Test turning valve off await hass.services.async_call( @@ -663,7 +667,7 @@ async def test_valves_service_turn_on(hass, config_entry, netatmo_auth): await hass.async_block_till_done() webhook_id = config_entry.data[CONF_WEBHOOK_ID] - climate_entity_entrada = "climate.netatmo_entrada" + climate_entity_entrada = "climate.entrada" # Test turning valve on await hass.services.async_call( @@ -700,21 +704,6 @@ async def test_valves_service_turn_on(hass, config_entry, netatmo_auth): assert hass.states.get(climate_entity_entrada).state == "auto" -async def test_get_all_home_ids(): - """Test extracting all home ids returned by NetAtmo API.""" - # Test with backend returning no data - assert climate.get_all_home_ids(None) == [] - - # Test with fake data - home_data = Mock() - home_data.homes = { - "123": {"id": "123", "name": "Home 1", "modules": [], "therm_schedules": []}, - "987": {"id": "987", "name": "Home 2", "modules": [], "therm_schedules": []}, - } - expected = ["123", "987"] - assert climate.get_all_home_ids(home_data) == expected - - async def test_webhook_home_id_mismatch(hass, config_entry, netatmo_auth): """Test service turn on for valves.""" with selected_platforms(["climate"]): @@ -723,7 +712,7 @@ async def test_webhook_home_id_mismatch(hass, config_entry, netatmo_auth): await hass.async_block_till_done() webhook_id = config_entry.data[CONF_WEBHOOK_ID] - climate_entity_entrada = "climate.netatmo_entrada" + climate_entity_entrada = "climate.entrada" assert hass.states.get(climate_entity_entrada).state == "auto" @@ -761,7 +750,7 @@ async def test_webhook_set_point(hass, config_entry, netatmo_auth): await hass.async_block_till_done() webhook_id = config_entry.data[CONF_WEBHOOK_ID] - climate_entity_entrada = "climate.netatmo_entrada" + climate_entity_entrada = "climate.entrada" # Fake backend response for valve being turned on response = { diff --git a/tests/components/netatmo/test_init.py b/tests/components/netatmo/test_init.py index 2d0c43ac3f6..418854a61a2 100644 --- a/tests/components/netatmo/test_init.py +++ b/tests/components/netatmo/test_init.py @@ -136,7 +136,7 @@ async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION) # Assert webhook is established successfully - climate_entity_livingroom = "climate.netatmo_livingroom" + climate_entity_livingroom = "climate.livingroom" assert hass.states.get(climate_entity_livingroom).state == "auto" await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK) assert hass.states.get(climate_entity_livingroom).state == "heat" @@ -440,7 +440,6 @@ async def test_setup_component_invalid_token(hass, config_entry): """Test handling of invalid token.""" async def fake_ensure_valid_token(*args, **kwargs): - print("fake_ensure_valid_token") raise aiohttp.ClientResponseError( request_info=aiohttp.client.RequestInfo( url="http://example.com", diff --git a/tests/components/netatmo/test_select.py b/tests/components/netatmo/test_select.py index f0e7cde7359..de357ffda89 100644 --- a/tests/components/netatmo/test_select.py +++ b/tests/components/netatmo/test_select.py @@ -38,7 +38,7 @@ async def test_select_schedule_thermostats(hass, config_entry, caplog, netatmo_a # Test setting a different schedule with patch( - "pyatmo.thermostat.AsyncHomeData.async_switch_home_schedule" + "pyatmo.climate.AsyncClimate.async_switch_home_schedule" ) as mock_switch_home_schedule: await hass.services.async_call( SELECT_DOMAIN, @@ -51,7 +51,7 @@ async def test_select_schedule_thermostats(hass, config_entry, caplog, netatmo_a ) await hass.async_block_till_done() mock_switch_home_schedule.assert_called_once_with( - home_id="91763b24c43d3e344f424e8b", schedule_id="591b54a2764ff4d50d8b5795" + schedule_id="591b54a2764ff4d50d8b5795" ) # Fake backend response changing schedule