Update Netatmo climate platform (#59974)

This commit is contained in:
Tobias Sauerwein 2021-12-02 10:31:54 +01:00 committed by GitHub
parent 3307e54363
commit 653fb5b637
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 626 additions and 754 deletions

View file

@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import Any, cast from typing import Any
import pyatmo import pyatmo
import voluptuous as vol import voluptuous as vol
@ -22,7 +22,6 @@ from homeassistant.components.climate.const import (
) )
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_BATTERY_LEVEL,
ATTR_SUGGESTED_AREA, ATTR_SUGGESTED_AREA,
ATTR_TEMPERATURE, ATTR_TEMPERATURE,
PRECISION_HALVES, PRECISION_HALVES,
@ -32,7 +31,6 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import PlatformNotReady from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import config_validation as cv, entity_platform 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.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -41,7 +39,6 @@ from .const import (
ATTR_HEATING_POWER_REQUEST, ATTR_HEATING_POWER_REQUEST,
ATTR_SCHEDULE_NAME, ATTR_SCHEDULE_NAME,
ATTR_SELECTED_SCHEDULE, ATTR_SELECTED_SCHEDULE,
DATA_DEVICE_IDS,
DATA_HANDLER, DATA_HANDLER,
DATA_HOMES, DATA_HOMES,
DATA_SCHEDULES, DATA_SCHEDULES,
@ -50,17 +47,15 @@ from .const import (
EVENT_TYPE_SCHEDULE, EVENT_TYPE_SCHEDULE,
EVENT_TYPE_SET_POINT, EVENT_TYPE_SET_POINT,
EVENT_TYPE_THERM_MODE, EVENT_TYPE_THERM_MODE,
MANUFACTURER,
SERVICE_SET_SCHEDULE, SERVICE_SET_SCHEDULE,
SIGNAL_NAME, SIGNAL_NAME,
TYPE_ENERGY, TYPE_ENERGY,
) )
from .data_handler import ( from .data_handler import (
HOMEDATA_DATA_CLASS_NAME, CLIMATE_STATE_CLASS_NAME,
HOMESTATUS_DATA_CLASS_NAME, CLIMATE_TOPOLOGY_CLASS_NAME,
NetatmoDataHandler, NetatmoDataHandler,
) )
from .helper import get_all_home_ids, update_climate_schedules
from .netatmo_entity_base import NetatmoBase from .netatmo_entity_base import NetatmoBase
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -125,44 +120,42 @@ async def async_setup_entry(
data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER]
await data_handler.register_data_class( 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 raise PlatformNotReady
entities = [] entities = []
for home_id in get_all_home_ids(home_data): for home_id in climate_topology.home_ids:
for room_id in home_data.rooms[home_id]: signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}"
signal_name = f"{HOMESTATUS_DATA_CLASS_NAME}-{home_id}" await data_handler.register_data_class(
await data_handler.register_data_class( CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id
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,
) )
) climate_state = data_handler.data[signal_name]
climate_topology.register_handler(home_id, climate_state.process_topology)
hass.data[DOMAIN][DATA_HOMES] = { for room in climate_state.homes[home_id].rooms.values():
home_id: home_data.get("name") if room.device_type is None or room.device_type.value not in [
for home_id, home_data in ( NA_THERM,
data_handler.data[HOMEDATA_DATA_CLASS_NAME].homes.items() 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) _LOGGER.debug("Adding climate devices %s", entities)
async_add_entities(entities, True) async_add_entities(entities, True)
platform = entity_platform.async_get_current_platform() platform = entity_platform.async_get_current_platform()
if home_data is not None: if climate_topology is not None:
platform.async_register_entity_service( platform.async_register_entity_service(
SERVICE_SET_SCHEDULE, SERVICE_SET_SCHEDULE,
{vol.Required(ATTR_SCHEDULE_NAME): cv.string}, {vol.Required(ATTR_SCHEDULE_NAME): cv.string},
@ -174,67 +167,61 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
"""Representation a Netatmo thermostat.""" """Representation a Netatmo thermostat."""
_attr_hvac_mode = HVAC_MODE_AUTO _attr_hvac_mode = HVAC_MODE_AUTO
_attr_hvac_modes = [HVAC_MODE_AUTO, HVAC_MODE_HEAT]
_attr_max_temp = DEFAULT_MAX_TEMP _attr_max_temp = DEFAULT_MAX_TEMP
_attr_preset_modes = SUPPORT_PRESET _attr_preset_modes = SUPPORT_PRESET
_attr_supported_features = SUPPORT_FLAGS
_attr_target_temperature_step = PRECISION_HALVES _attr_target_temperature_step = PRECISION_HALVES
_attr_temperature_unit = TEMP_CELSIUS _attr_temperature_unit = TEMP_CELSIUS
def __init__( def __init__(
self, data_handler: NetatmoDataHandler, home_id: str, room_id: str self, data_handler: NetatmoDataHandler, room: pyatmo.climate.NetatmoRoom
) -> None: ) -> None:
"""Initialize the sensor.""" """Initialize the sensor."""
ClimateEntity.__init__(self) ClimateEntity.__init__(self)
super().__init__(data_handler) super().__init__(data_handler)
self._id = room_id self._room = room
self._home_id = home_id 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( self._data_classes.extend(
[ [
{ {
"name": HOMEDATA_DATA_CLASS_NAME, "name": CLIMATE_TOPOLOGY_CLASS_NAME,
SIGNAL_NAME: HOMEDATA_DATA_CLASS_NAME, SIGNAL_NAME: CLIMATE_TOPOLOGY_CLASS_NAME,
}, },
{ {
"name": HOMESTATUS_DATA_CLASS_NAME, "name": CLIMATE_STATE_CLASS_NAME,
"home_id": self._home_id, "home_id": self._room.home.entity_id,
SIGNAL_NAME: self._home_status_class, SIGNAL_NAME: self._climate_state_class,
}, },
] ]
) )
self._home_status = self.data_handler.data[self._home_status_class] self._model: str = getattr(room.device_type, "value")
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._netatmo_type = TYPE_ENERGY self._netatmo_type = TYPE_ENERGY
self._device_name = self._data.rooms[home_id][room_id]["name"] self._attr_name = self._room.name
self._attr_name = f"{MANUFACTURER} {self._device_name}"
self._away: bool | None = None self._away: bool | None = None
self._support_flags = SUPPORT_FLAGS
self._battery_level = None
self._connected: bool | None = None self._connected: bool | None = None
self._away_temperature: float | None = None self._away_temperature: float | None = None
self._hg_temperature: float | None = None self._hg_temperature: float | None = None
self._boilerstatus: bool | None = None self._boilerstatus: bool | None = None
self._setpoint_duration = None
self._selected_schedule = None self._selected_schedule = None
self._attr_hvac_modes = [HVAC_MODE_AUTO, HVAC_MODE_HEAT]
if self._model == NA_THERM: if self._model == NA_THERM:
self._attr_hvac_modes.append(HVAC_MODE_OFF) 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: async def async_added_to_hass(self) -> None:
"""Entity created.""" """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 @callback
def handle_event(self, event: dict) -> None: def handle_event(self, event: dict) -> None:
"""Handle webhook events.""" """Handle webhook events."""
data = event["data"] data = event["data"]
if self._home_id != data["home_id"]: if self._room.home.entity_id != data["home_id"]:
return return
if data["event_type"] == EVENT_TYPE_SCHEDULE and "schedule_id" in data: if data["event_type"] == EVENT_TYPE_SCHEDULE and "schedule_id" in data:
self._selected_schedule = self.hass.data[DOMAIN][DATA_SCHEDULES][ self._selected_schedule = getattr(
self._home_id self.hass.data[DOMAIN][DATA_SCHEDULES][self._room.home.entity_id].get(
].get(data["schedule_id"]) data["schedule_id"]
self._attr_extra_state_attributes.update( ),
{"selected_schedule": self._selected_schedule} "name",
None,
) )
self._attr_extra_state_attributes[
ATTR_SELECTED_SCHEDULE
] = self._selected_schedule
self.async_write_ha_state() 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 return
home = data["home"] home = data["home"]
if self._home_id != home["id"]: if self._room.home.entity_id != home["id"]:
return return
if data["event_type"] == EVENT_TYPE_THERM_MODE: if data["event_type"] == EVENT_TYPE_THERM_MODE:
@ -292,12 +278,15 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
self._attr_target_temperature = self._away_temperature self._attr_target_temperature = self._away_temperature
elif self._attr_preset_mode == PRESET_SCHEDULE: elif self._attr_preset_mode == PRESET_SCHEDULE:
self.async_update_callback() 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() self.async_write_ha_state()
return return
for room in home.get("rooms", []): 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: if room["therm_setpoint_mode"] == STATE_NETATMO_OFF:
self._attr_hvac_mode = HVAC_MODE_OFF self._attr_hvac_mode = HVAC_MODE_OFF
self._attr_preset_mode = STATE_NETATMO_OFF self._attr_preset_mode = STATE_NETATMO_OFF
@ -318,31 +307,21 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
if ( if (
data["event_type"] == EVENT_TYPE_CANCEL_SET_POINT 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_update_callback()
self.async_write_ha_state() self.async_write_ha_state()
return 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 @property
def hvac_action(self) -> str | None: def hvac_action(self) -> str | None:
"""Return the current running hvac operation if supported.""" """Return the current running hvac operation if supported."""
if self._model == NA_THERM and self._boilerstatus is not None: if self._model == NA_THERM and self._boilerstatus is not None:
return CURRENT_HVAC_MAP_NETATMO[self._boilerstatus] return CURRENT_HVAC_MAP_NETATMO[self._boilerstatus]
# Maybe it is a valve # 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_HEAT
return CURRENT_HVAC_IDLE return CURRENT_HVAC_IDLE
@ -363,8 +342,8 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
await self.async_turn_on() await self.async_turn_on()
if self.target_temperature == 0: if self.target_temperature == 0:
await self._home_status.async_set_room_thermpoint( await self._climate_state.async_set_room_thermpoint(
self._id, self._room.entity_id,
STATE_NETATMO_HOME, STATE_NETATMO_HOME,
) )
@ -373,15 +352,15 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
and self._model == NA_VALVE and self._model == NA_VALVE
and self.hvac_mode == HVAC_MODE_HEAT and self.hvac_mode == HVAC_MODE_HEAT
): ):
await self._home_status.async_set_room_thermpoint( await self._climate_state.async_set_room_thermpoint(
self._id, self._room.entity_id,
STATE_NETATMO_HOME, STATE_NETATMO_HOME,
) )
elif ( elif (
preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX) and self._model == NA_VALVE preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX) and self._model == NA_VALVE
): ):
await self._home_status.async_set_room_thermpoint( await self._climate_state.async_set_room_thermpoint(
self._id, self._room.entity_id,
STATE_NETATMO_MANUAL, STATE_NETATMO_MANUAL,
DEFAULT_MAX_TEMP, DEFAULT_MAX_TEMP,
) )
@ -389,15 +368,17 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX) preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX)
and self.hvac_mode == HVAC_MODE_HEAT and self.hvac_mode == HVAC_MODE_HEAT
): ):
await self._home_status.async_set_room_thermpoint( await self._climate_state.async_set_room_thermpoint(
self._id, STATE_NETATMO_HOME self._room.entity_id, STATE_NETATMO_HOME
) )
elif preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX): elif preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX):
await self._home_status.async_set_room_thermpoint( await self._climate_state.async_set_room_thermpoint(
self._id, PRESET_MAP_NETATMO[preset_mode] self._room.entity_id, PRESET_MAP_NETATMO[preset_mode]
) )
elif preset_mode in (PRESET_SCHEDULE, PRESET_FROST_GUARD, PRESET_AWAY): 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: else:
_LOGGER.error("Preset mode '%s' not available", preset_mode) _LOGGER.error("Preset mode '%s' not available", preset_mode)
@ -407,8 +388,8 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
"""Set new target temperature for 2 hours.""" """Set new target temperature for 2 hours."""
if (temp := kwargs.get(ATTR_TEMPERATURE)) is None: if (temp := kwargs.get(ATTR_TEMPERATURE)) is None:
return return
await self._home_status.async_set_room_thermpoint( await self._climate_state.async_set_room_thermpoint(
self._id, STATE_NETATMO_MANUAL, min(temp, DEFAULT_MAX_TEMP) self._room.entity_id, STATE_NETATMO_MANUAL, min(temp, DEFAULT_MAX_TEMP)
) )
self.async_write_ha_state() self.async_write_ha_state()
@ -416,20 +397,22 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
async def async_turn_off(self) -> None: async def async_turn_off(self) -> None:
"""Turn the entity off.""" """Turn the entity off."""
if self._model == NA_VALVE: if self._model == NA_VALVE:
await self._home_status.async_set_room_thermpoint( await self._climate_state.async_set_room_thermpoint(
self._id, self._room.entity_id,
STATE_NETATMO_MANUAL, STATE_NETATMO_MANUAL,
DEFAULT_MIN_TEMP, DEFAULT_MIN_TEMP,
) )
elif self.hvac_mode != HVAC_MODE_OFF: elif self.hvac_mode != HVAC_MODE_OFF:
await self._home_status.async_set_room_thermpoint( await self._climate_state.async_set_room_thermpoint(
self._id, STATE_NETATMO_OFF self._room.entity_id, STATE_NETATMO_OFF
) )
self.async_write_ha_state() self.async_write_ha_state()
async def async_turn_on(self) -> None: async def async_turn_on(self) -> None:
"""Turn the entity on.""" """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() self.async_write_ha_state()
@property @property
@ -440,135 +423,57 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
@callback @callback
def async_update_callback(self) -> None: def async_update_callback(self) -> None:
"""Update the entity's state.""" """Update the entity's state."""
self._home_status = self.data_handler.data[self._home_status_class] if not self._room.reachable:
if self._home_status is None:
if self.available: if self.available:
self._connected = False self._connected = False
return 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._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] self._away = self._attr_hvac_mode == HVAC_MAP_NETATMO[STATE_NETATMO_AWAY]
if self._battery_level is not None: self._selected_schedule = getattr(
self._attr_extra_state_attributes[ATTR_BATTERY_LEVEL] = self._battery_level self._room.home.get_selected_schedule(), "name", None
)
self._attr_extra_state_attributes[
ATTR_SELECTED_SCHEDULE
] = self._selected_schedule
if self._model == NA_VALVE: if self._model == NA_VALVE:
self._attr_extra_state_attributes[ self._attr_extra_state_attributes[
ATTR_HEATING_POWER_REQUEST ATTR_HEATING_POWER_REQUEST
] = self._room_status.get("heating_power_request", 0) ] = self._room.heating_power_request
else:
if self._selected_schedule is not None: for module in self._room.modules.values():
self._attr_extra_state_attributes[ self._boilerstatus = module.boiler_status
ATTR_SELECTED_SCHEDULE break
] = 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 {}
async def _async_service_set_schedule(self, **kwargs: Any) -> None: async def _async_service_set_schedule(self, **kwargs: Any) -> None:
schedule_name = kwargs.get(ATTR_SCHEDULE_NAME) schedule_name = kwargs.get(ATTR_SCHEDULE_NAME)
schedule_id = None schedule_id = None
for sid, name in self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id].items(): for sid, schedule in self.hass.data[DOMAIN][DATA_SCHEDULES][
if name == schedule_name: self._room.home.entity_id
].items():
if schedule.name == schedule_name:
schedule_id = sid schedule_id = sid
break
if not schedule_id: if not schedule_id:
_LOGGER.error("%s is not a valid schedule", kwargs.get(ATTR_SCHEDULE_NAME)) _LOGGER.error("%s is not a valid schedule", kwargs.get(ATTR_SCHEDULE_NAME))
return return
await self._data.async_switch_home_schedule( await self._climate_state.async_switch_home_schedule(schedule_id=schedule_id)
home_id=self._home_id, schedule_id=schedule_id
)
_LOGGER.debug( _LOGGER.debug(
"Setting %s schedule to %s (%s)", "Setting %s schedule to %s (%s)",
self._home_id, self._room.home.entity_id,
kwargs.get(ATTR_SCHEDULE_NAME), kwargs.get(ATTR_SCHEDULE_NAME),
schedule_id, schedule_id,
) )
@ -577,5 +482,5 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
def device_info(self) -> DeviceInfo: def device_info(self) -> DeviceInfo:
"""Return the device info for the thermostat.""" """Return the device info for the thermostat."""
device_info: DeviceInfo = super().device_info 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 return device_info

View file

@ -32,23 +32,23 @@ _LOGGER = logging.getLogger(__name__)
CAMERA_DATA_CLASS_NAME = "AsyncCameraData" CAMERA_DATA_CLASS_NAME = "AsyncCameraData"
WEATHERSTATION_DATA_CLASS_NAME = "AsyncWeatherStationData" WEATHERSTATION_DATA_CLASS_NAME = "AsyncWeatherStationData"
HOMECOACH_DATA_CLASS_NAME = "AsyncHomeCoachData" HOMECOACH_DATA_CLASS_NAME = "AsyncHomeCoachData"
HOMEDATA_DATA_CLASS_NAME = "AsyncHomeData" CLIMATE_TOPOLOGY_CLASS_NAME = "AsyncClimateTopology"
HOMESTATUS_DATA_CLASS_NAME = "AsyncHomeStatus" CLIMATE_STATE_CLASS_NAME = "AsyncClimate"
PUBLICDATA_DATA_CLASS_NAME = "AsyncPublicData" PUBLICDATA_DATA_CLASS_NAME = "AsyncPublicData"
DATA_CLASSES = { DATA_CLASSES = {
WEATHERSTATION_DATA_CLASS_NAME: pyatmo.AsyncWeatherStationData, WEATHERSTATION_DATA_CLASS_NAME: pyatmo.AsyncWeatherStationData,
HOMECOACH_DATA_CLASS_NAME: pyatmo.AsyncHomeCoachData, HOMECOACH_DATA_CLASS_NAME: pyatmo.AsyncHomeCoachData,
CAMERA_DATA_CLASS_NAME: pyatmo.AsyncCameraData, CAMERA_DATA_CLASS_NAME: pyatmo.AsyncCameraData,
HOMEDATA_DATA_CLASS_NAME: pyatmo.AsyncHomeData, CLIMATE_TOPOLOGY_CLASS_NAME: pyatmo.AsyncClimateTopology,
HOMESTATUS_DATA_CLASS_NAME: pyatmo.AsyncHomeStatus, CLIMATE_STATE_CLASS_NAME: pyatmo.AsyncClimate,
PUBLICDATA_DATA_CLASS_NAME: pyatmo.AsyncPublicData, PUBLICDATA_DATA_CLASS_NAME: pyatmo.AsyncPublicData,
} }
BATCH_SIZE = 3 BATCH_SIZE = 3
DEFAULT_INTERVALS = { DEFAULT_INTERVALS = {
HOMEDATA_DATA_CLASS_NAME: 900, CLIMATE_TOPOLOGY_CLASS_NAME: 3600,
HOMESTATUS_DATA_CLASS_NAME: 300, CLIMATE_STATE_CLASS_NAME: 300,
CAMERA_DATA_CLASS_NAME: 900, CAMERA_DATA_CLASS_NAME: 900,
WEATHERSTATION_DATA_CLASS_NAME: 600, WEATHERSTATION_DATA_CLASS_NAME: 600,
HOMECOACH_DATA_CLASS_NAME: 300, HOMECOACH_DATA_CLASS_NAME: 300,

View file

@ -4,8 +4,6 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from uuid import UUID, uuid4 from uuid import UUID, uuid4
import pyatmo
@dataclass @dataclass
class NetatmoArea: class NetatmoArea:
@ -19,25 +17,3 @@ class NetatmoArea:
mode: str mode: str
show_on_map: bool show_on_map: bool
uuid: UUID = uuid4() 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
}

View file

@ -66,7 +66,7 @@ class NetatmoBase(Entity):
await self.data_handler.unregister_data_class(signal_name, None) await self.data_handler.unregister_data_class(signal_name, None)
registry = await self.hass.helpers.device_registry.async_get_registry() 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.hass.data[DOMAIN][DATA_DEVICE_IDS][self._id] = device.id
self.async_update_callback() self.async_update_callback()

View file

@ -2,7 +2,6 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
from typing import cast
import pyatmo import pyatmo
@ -22,8 +21,11 @@ from .const import (
SIGNAL_NAME, SIGNAL_NAME,
TYPE_ENERGY, TYPE_ENERGY,
) )
from .data_handler import HOMEDATA_DATA_CLASS_NAME, NetatmoDataHandler from .data_handler import (
from .helper import get_all_home_ids, update_climate_schedules CLIMATE_STATE_CLASS_NAME,
CLIMATE_TOPOLOGY_CLASS_NAME,
NetatmoDataHandler,
)
from .netatmo_entity_base import NetatmoBase from .netatmo_entity_base import NetatmoBase
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -36,25 +38,34 @@ async def async_setup_entry(
data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER]
await data_handler.register_data_class( 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 raise PlatformNotReady
hass.data[DOMAIN][DATA_SCHEDULES].update( entities = []
update_climate_schedules( for home_id in climate_topology.home_ids:
home_ids=get_all_home_ids(home_data), signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}"
schedules=data_handler.data[HOMEDATA_DATA_CLASS_NAME].schedules, 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 = [ entities = [
NetatmoScheduleSelect( NetatmoScheduleSelect(
data_handler, data_handler,
home_id, 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] for home_id in hass.data[DOMAIN][DATA_SCHEDULES]
] ]
@ -75,16 +86,28 @@ class NetatmoScheduleSelect(NetatmoBase, SelectEntity):
self._home_id = home_id 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( self._data_classes.extend(
[ [
{ {
"name": HOMEDATA_DATA_CLASS_NAME, "name": CLIMATE_TOPOLOGY_CLASS_NAME,
SIGNAL_NAME: HOMEDATA_DATA_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._attr_name = f"{MANUFACTURER} {self._device_name}"
self._model: str = "NATherm1" self._model: str = "NATherm1"
@ -92,9 +115,7 @@ class NetatmoScheduleSelect(NetatmoBase, SelectEntity):
self._attr_unique_id = f"{self._home_id}-schedule-select" self._attr_unique_id = f"{self._home_id}-schedule-select"
self._attr_current_option = self._data._get_selected_schedule( self._attr_current_option = getattr(self._home.get_selected_schedule(), "name")
home_id=self._home_id
).get("name")
self._attr_options = options self._attr_options = options
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
@ -119,23 +140,20 @@ class NetatmoScheduleSelect(NetatmoBase, SelectEntity):
return return
if data["event_type"] == EVENT_TYPE_SCHEDULE and "schedule_id" in data: if data["event_type"] == EVENT_TYPE_SCHEDULE and "schedule_id" in data:
self._attr_current_option = self.hass.data[DOMAIN][DATA_SCHEDULES][ self._attr_current_option = getattr(
self._home_id self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id].get(
].get(data["schedule_id"]) data["schedule_id"]
),
"name",
)
self.async_write_ha_state() 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: async def async_select_option(self, option: str) -> None:
"""Change the selected option.""" """Change the selected option."""
for sid, name in self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id].items(): for sid, schedule in self.hass.data[DOMAIN][DATA_SCHEDULES][
if name != option: self._home_id
].items():
if schedule.name != option:
continue continue
_LOGGER.debug( _LOGGER.debug(
"Setting %s schedule to %s (%s)", "Setting %s schedule to %s (%s)",
@ -143,25 +161,17 @@ class NetatmoScheduleSelect(NetatmoBase, SelectEntity):
option, option,
sid, sid,
) )
await self._data.async_switch_home_schedule( await self._climate_state.async_switch_home_schedule(schedule_id=sid)
home_id=self._home_id, schedule_id=sid
)
break break
@callback @callback
def async_update_callback(self) -> None: def async_update_callback(self) -> None:
"""Update the entity's state.""" """Update the entity's state."""
self._attr_current_option = ( self._attr_current_option = getattr(self._home.get_selected_schedule(), "name")
self._data._get_selected_schedule( # pylint: disable=protected-access self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id] = self._home.schedules
home_id=self._home_id self._attr_options = [
).get("name") schedule.name
) for schedule in self.hass.data[DOMAIN][DATA_SCHEDULES][
self.hass.data[DOMAIN][DATA_SCHEDULES][self._home_id] = { self._home_id
schedule_id: schedule_data.get("name") ].values()
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()
)

View file

@ -51,7 +51,7 @@ async def fake_post_request(*args, **kwargs):
if endpoint in "snapshot_720.jpg": if endpoint in "snapshot_720.jpg":
return b"test stream image bytes" return b"test stream image bytes"
elif endpoint in [ if endpoint in [
"setpersonsaway", "setpersonsaway",
"setpersonshome", "setpersonshome",
"setstate", "setstate",
@ -61,6 +61,10 @@ async def fake_post_request(*args, **kwargs):
]: ]:
payload = f'{{"{endpoint}": true}}' 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: else:
payload = json.loads(load_fixture(f"netatmo/{endpoint}.json")) payload = json.loads(load_fixture(f"netatmo/{endpoint}.json"))

View file

@ -1,430 +1,407 @@
{ {
"body": { "body": {
"homes": [ "homes": [
{ {
"id": "91763b24c43d3e344f424e8b", "id": "91763b24c43d3e344f424e8b",
"name": "MYHOME", "name": "MYHOME",
"altitude": 112, "altitude": 112,
"coordinates": [ "coordinates": [52.516263, 13.377726],
52.516263, "country": "DE",
13.377726 "timezone": "Europe/Berlin",
], "rooms": [
"country": "DE", {
"timezone": "Europe/Berlin", "id": "2746182631",
"rooms": [ "name": "Livingroom",
{ "type": "livingroom",
"id": "2746182631", "module_ids": ["12:34:56:00:01:ae"]
"name": "Livingroom", },
"type": "livingroom", {
"module_ids": [ "id": "3688132631",
"12:34:56:00:01:ae" "name": "Hall",
] "type": "custom",
}, "module_ids": ["12:34:56:00:f1:62"]
{ },
"id": "3688132631", {
"name": "Hall", "id": "2833524037",
"type": "custom", "name": "Entrada",
"module_ids": [ "type": "lobby",
"12:34:56:00:f1:62" "module_ids": ["12:34:56:03:a5:54"]
] },
}, {
{ "id": "2940411577",
"id": "2833524037", "name": "Cocina",
"name": "Entrada", "type": "kitchen",
"type": "lobby", "module_ids": ["12:34:56:03:a0:ac"]
"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"
}
], ],
"user": { "modules": [
"email": "john@doe.com", {
"language": "de-DE", "id": "12:34:56:00:fa:d0",
"locale": "de-DE", "type": "NAPlug",
"feel_like_algorithm": 0, "name": "Thermostat",
"unit_pressure": 0, "setup_date": 1494963356,
"unit_system": 0, "modules_bridged": [
"unit_wind": 0, "12:34:56:00:01:ae",
"id": "91763b24c43d3e344f424e8b" "12:34:56:03:a0:ac",
} "12:34:56:03:a5:54"
}, ]
"status": "ok", },
"time_exec": 0.056135892868042, {
"time_server": 1559171003 "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
} }

View file

@ -0,0 +1,12 @@
{
"status": "ok",
"time_server": 1559292041,
"body": {
"home": {
"modules": [],
"rooms": [],
"id": "91763b24c43d3e344f424e8c",
"persons": []
}
}
}

View file

@ -1,5 +1,5 @@
"""The tests for the Netatmo climate platform.""" """The tests for the Netatmo climate platform."""
from unittest.mock import Mock, patch from unittest.mock import patch
from homeassistant.components.climate import ( from homeassistant.components.climate import (
DOMAIN as CLIMATE_DOMAIN, DOMAIN as CLIMATE_DOMAIN,
@ -18,7 +18,6 @@ from homeassistant.components.climate.const import (
PRESET_AWAY, PRESET_AWAY,
PRESET_BOOST, PRESET_BOOST,
) )
from homeassistant.components.netatmo import climate
from homeassistant.components.netatmo.climate import PRESET_FROST_GUARD, PRESET_SCHEDULE from homeassistant.components.netatmo.climate import PRESET_FROST_GUARD, PRESET_SCHEDULE
from homeassistant.components.netatmo.const import ( from homeassistant.components.netatmo.const import (
ATTR_SCHEDULE_NAME, 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() await hass.async_block_till_done()
webhook_id = config_entry.data[CONF_WEBHOOK_ID] 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 hass.states.get(climate_entity_livingroom).state == "auto"
assert ( assert (
@ -214,7 +213,7 @@ async def test_service_preset_mode_frost_guard_thermostat(
await hass.async_block_till_done() await hass.async_block_till_done()
webhook_id = config_entry.data[CONF_WEBHOOK_ID] 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 hass.states.get(climate_entity_livingroom).state == "auto"
assert ( assert (
@ -287,7 +286,7 @@ async def test_service_preset_modes_thermostat(hass, config_entry, netatmo_auth)
await hass.async_block_till_done() await hass.async_block_till_done()
webhook_id = config_entry.data[CONF_WEBHOOK_ID] 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 hass.states.get(climate_entity_livingroom).state == "auto"
assert ( assert (
@ -415,11 +414,11 @@ async def test_service_schedule_thermostats(hass, config_entry, caplog, netatmo_
await hass.async_block_till_done() await hass.async_block_till_done()
webhook_id = config_entry.data[CONF_WEBHOOK_ID] webhook_id = config_entry.data[CONF_WEBHOOK_ID]
climate_entity_livingroom = "climate.netatmo_livingroom" climate_entity_livingroom = "climate.livingroom"
# Test setting a valid schedule # Test setting a valid schedule
with patch( with patch(
"pyatmo.thermostat.AsyncHomeData.async_switch_home_schedule" "pyatmo.climate.AsyncClimate.async_switch_home_schedule"
) as mock_switch_home_schedule: ) as mock_switch_home_schedule:
await hass.services.async_call( await hass.services.async_call(
"netatmo", "netatmo",
@ -429,7 +428,7 @@ async def test_service_schedule_thermostats(hass, config_entry, caplog, netatmo_
) )
await hass.async_block_till_done() await hass.async_block_till_done()
mock_switch_home_schedule.assert_called_once_with( 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 # 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 # Test setting an invalid schedule
with patch( with patch(
"pyatmo.thermostat.AsyncHomeData.async_switch_home_schedule" "pyatmo.climate.AsyncClimate.async_switch_home_schedule"
) as mock_switch_home_schedule: ) as mock_switch_home_schedule:
await hass.services.async_call( await hass.services.async_call(
"netatmo", "netatmo",
@ -472,7 +471,7 @@ async def test_service_preset_mode_already_boost_valves(
await hass.async_block_till_done() await hass.async_block_till_done()
webhook_id = config_entry.data[CONF_WEBHOOK_ID] 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 hass.states.get(climate_entity_entrada).state == "auto"
assert ( assert (
@ -550,7 +549,7 @@ async def test_service_preset_mode_boost_valves(hass, config_entry, netatmo_auth
await hass.async_block_till_done() await hass.async_block_till_done()
webhook_id = config_entry.data[CONF_WEBHOOK_ID] 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" # Test service setting the preset mode to "boost"
assert hass.states.get(climate_entity_entrada).state == "auto" 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( await hass.services.async_call(
CLIMATE_DOMAIN, CLIMATE_DOMAIN,
SERVICE_SET_PRESET_MODE, 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, blocking=True,
) )
await hass.async_block_till_done() 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() await hass.async_block_till_done()
webhook_id = config_entry.data[CONF_WEBHOOK_ID] 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 # Test turning valve off
await hass.services.async_call( 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() await hass.async_block_till_done()
webhook_id = config_entry.data[CONF_WEBHOOK_ID] webhook_id = config_entry.data[CONF_WEBHOOK_ID]
climate_entity_entrada = "climate.netatmo_entrada" climate_entity_entrada = "climate.entrada"
# Test turning valve on # Test turning valve on
await hass.services.async_call( 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" 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): async def test_webhook_home_id_mismatch(hass, config_entry, netatmo_auth):
"""Test service turn on for valves.""" """Test service turn on for valves."""
with selected_platforms(["climate"]): 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() await hass.async_block_till_done()
webhook_id = config_entry.data[CONF_WEBHOOK_ID] 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 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() await hass.async_block_till_done()
webhook_id = config_entry.data[CONF_WEBHOOK_ID] 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 # Fake backend response for valve being turned on
response = { response = {

View file

@ -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) await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION)
# Assert webhook is established successfully # 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" assert hass.states.get(climate_entity_livingroom).state == "auto"
await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK) await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK)
assert hass.states.get(climate_entity_livingroom).state == "heat" 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.""" """Test handling of invalid token."""
async def fake_ensure_valid_token(*args, **kwargs): async def fake_ensure_valid_token(*args, **kwargs):
print("fake_ensure_valid_token")
raise aiohttp.ClientResponseError( raise aiohttp.ClientResponseError(
request_info=aiohttp.client.RequestInfo( request_info=aiohttp.client.RequestInfo(
url="http://example.com", url="http://example.com",

View file

@ -38,7 +38,7 @@ async def test_select_schedule_thermostats(hass, config_entry, caplog, netatmo_a
# Test setting a different schedule # Test setting a different schedule
with patch( with patch(
"pyatmo.thermostat.AsyncHomeData.async_switch_home_schedule" "pyatmo.climate.AsyncClimate.async_switch_home_schedule"
) as mock_switch_home_schedule: ) as mock_switch_home_schedule:
await hass.services.async_call( await hass.services.async_call(
SELECT_DOMAIN, SELECT_DOMAIN,
@ -51,7 +51,7 @@ async def test_select_schedule_thermostats(hass, config_entry, caplog, netatmo_a
) )
await hass.async_block_till_done() await hass.async_block_till_done()
mock_switch_home_schedule.assert_called_once_with( mock_switch_home_schedule.assert_called_once_with(
home_id="91763b24c43d3e344f424e8b", schedule_id="591b54a2764ff4d50d8b5795" schedule_id="591b54a2764ff4d50d8b5795"
) )
# Fake backend response changing schedule # Fake backend response changing schedule