diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py
index 36847c85515..65b321d25aa 100644
--- a/homeassistant/components/netatmo/__init__.py
+++ b/homeassistant/components/netatmo/__init__.py
@@ -8,6 +8,7 @@ import secrets
import aiohttp
import pyatmo
+from pyatmo.const import ALL_SCOPES as NETATMO_SCOPES
import voluptuous as vol
from homeassistant.components import cloud
@@ -51,7 +52,6 @@ from .const import (
DATA_PERSONS,
DATA_SCHEDULES,
DOMAIN,
- NETATMO_SCOPES,
PLATFORMS,
WEBHOOK_DEACTIVATION,
WEBHOOK_PUSH_TYPE,
@@ -150,10 +150,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
}
data_handler = NetatmoDataHandler(hass, entry)
- await data_handler.async_setup()
hass.data[DOMAIN][entry.entry_id][DATA_HANDLER] = data_handler
-
- await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
+ await data_handler.async_setup()
async def unregister_webhook(
call_or_event_or_dt: ServiceCall | Event | datetime | None,
@@ -208,10 +206,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
_LOGGER.info("Register Netatmo webhook: %s", webhook_url)
except pyatmo.ApiError as err:
_LOGGER.error("Error during webhook registration - %s", err)
-
- entry.async_on_unload(
- hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook)
- )
+ else:
+ entry.async_on_unload(
+ hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook)
+ )
async def manage_cloudhook(state: cloud.CloudConnectionState) -> None:
if state is cloud.CloudConnectionState.CLOUD_CONNECTED:
diff --git a/homeassistant/components/netatmo/api.py b/homeassistant/components/netatmo/api.py
index e13032dc399..0b36745338e 100644
--- a/homeassistant/components/netatmo/api.py
+++ b/homeassistant/components/netatmo/api.py
@@ -7,7 +7,7 @@ import pyatmo
from homeassistant.helpers import config_entry_oauth2_flow
-class AsyncConfigEntryNetatmoAuth(pyatmo.auth.AbstractAsyncAuth):
+class AsyncConfigEntryNetatmoAuth(pyatmo.AbstractAsyncAuth):
"""Provide Netatmo authentication tied to an OAuth2 based config entry."""
def __init__(
diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py
index 3235d16479c..9254ff6e284 100644
--- a/homeassistant/components/netatmo/camera.py
+++ b/homeassistant/components/netatmo/camera.py
@@ -5,13 +5,14 @@ import logging
from typing import Any, cast
import aiohttp
-import pyatmo
+from pyatmo import ApiError as NetatmoApiError, modules as NaModules
+from pyatmo.event import Event as NaEvent
import voluptuous as vol
from homeassistant.components.camera import Camera, CameraEntityFeature
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
-from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
+from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -20,28 +21,24 @@ from .const import (
ATTR_CAMERA_LIGHT_MODE,
ATTR_PERSON,
ATTR_PERSONS,
- ATTR_PSEUDO,
CAMERA_LIGHT_MODES,
+ CONF_URL_SECURITY,
DATA_CAMERAS,
DATA_EVENTS,
- DATA_HANDLER,
- DATA_PERSONS,
DOMAIN,
EVENT_TYPE_LIGHT_MODE,
EVENT_TYPE_OFF,
EVENT_TYPE_ON,
MANUFACTURER,
- MODELS,
+ NETATMO_CREATE_CAMERA,
SERVICE_SET_CAMERA_LIGHT,
SERVICE_SET_PERSON_AWAY,
SERVICE_SET_PERSONS_HOME,
- SIGNAL_NAME,
- TYPE_SECURITY,
WEBHOOK_LIGHT_MODE,
WEBHOOK_NACAMERA_CONNECTION,
WEBHOOK_PUSH_TYPE,
)
-from .data_handler import CAMERA_DATA_CLASS_NAME, NetatmoDataHandler
+from .data_handler import EVENT, HOME, SIGNAL_NAME, NetatmoDevice
from .netatmo_entity_base import NetatmoBase
_LOGGER = logging.getLogger(__name__)
@@ -53,42 +50,15 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Netatmo camera platform."""
- data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER]
- data_class = data_handler.data.get(CAMERA_DATA_CLASS_NAME)
+ @callback
+ def _create_entity(netatmo_device: NetatmoDevice) -> None:
+ entity = NetatmoCamera(netatmo_device)
+ async_add_entities([entity])
- if not data_class or not data_class.raw_data:
- raise PlatformNotReady
-
- all_cameras = []
- for home in data_class.cameras.values():
- for camera in home.values():
- all_cameras.append(camera)
-
- entities = [
- NetatmoCamera(
- data_handler,
- camera["id"],
- camera["type"],
- camera["home_id"],
- DEFAULT_QUALITY,
- )
- for camera in all_cameras
- ]
-
- for home in data_class.homes.values():
- if home.get("id") is None:
- continue
-
- hass.data[DOMAIN][DATA_PERSONS][home["id"]] = {
- person_id: person_data.get(ATTR_PSEUDO)
- for person_id, person_data in data_handler.data[CAMERA_DATA_CLASS_NAME]
- .persons[home["id"]]
- .items()
- }
-
- _LOGGER.debug("Adding cameras %s", entities)
- async_add_entities(entities, True)
+ entry.async_on_unload(
+ async_dispatcher_connect(hass, NETATMO_CREATE_CAMERA, _create_entity)
+ )
platform = entity_platform.async_get_current_platform()
@@ -118,41 +88,44 @@ class NetatmoCamera(NetatmoBase, Camera):
def __init__(
self,
- data_handler: NetatmoDataHandler,
- camera_id: str,
- camera_type: str,
- home_id: str,
- quality: str,
+ netatmo_device: NetatmoDevice,
) -> None:
"""Set up for access to the Netatmo camera images."""
Camera.__init__(self)
- super().__init__(data_handler)
+ super().__init__(netatmo_device.data_handler)
- self._publishers.append(
- {"name": CAMERA_DATA_CLASS_NAME, SIGNAL_NAME: CAMERA_DATA_CLASS_NAME}
- )
-
- self._id = camera_id
- self._home_id = home_id
- self._device_name = self._data.get_camera(camera_id=camera_id)["name"]
- self._model = camera_type
- self._netatmo_type = TYPE_SECURITY
+ self._camera = cast(NaModules.Camera, netatmo_device.device)
+ self._id = self._camera.entity_id
+ self._home_id = self._camera.home.entity_id
+ self._device_name = self._camera.name
+ self._model = self._camera.device_type
+ self._config_url = CONF_URL_SECURITY
self._attr_unique_id = f"{self._id}-{self._model}"
- self._quality = quality
- self._vpnurl: str | None = None
- self._localurl: str | None = None
- self._status: str | None = None
- self._sd_status: str | None = None
- self._alim_status: str | None = None
- self._is_local: str | None = None
+ self._quality = DEFAULT_QUALITY
+ self._monitoring: bool | None = None
self._light_state = None
+ self._publishers.extend(
+ [
+ {
+ "name": HOME,
+ "home_id": self._home_id,
+ SIGNAL_NAME: f"{HOME}-{self._home_id}",
+ },
+ {
+ "name": EVENT,
+ "home_id": self._home_id,
+ SIGNAL_NAME: f"{EVENT}-{self._home_id}",
+ },
+ ]
+ )
+
async def async_added_to_hass(self) -> None:
"""Entity created."""
await super().async_added_to_hass()
for event_type in (EVENT_TYPE_LIGHT_MODE, EVENT_TYPE_OFF, EVENT_TYPE_ON):
- self.data_handler.config_entry.async_on_unload(
+ self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"signal-{DOMAIN}-webhook-{event_type}",
@@ -173,13 +146,13 @@ class NetatmoCamera(NetatmoBase, Camera):
if data["home_id"] == self._home_id and data["camera_id"] == self._id:
if data[WEBHOOK_PUSH_TYPE] in ("NACamera-off", "NACamera-disconnection"):
self._attr_is_streaming = False
- self._status = "off"
+ self._monitoring = False
elif data[WEBHOOK_PUSH_TYPE] in (
"NACamera-on",
WEBHOOK_NACAMERA_CONNECTION,
):
self._attr_is_streaming = True
- self._status = "on"
+ self._monitoring = True
elif data[WEBHOOK_PUSH_TYPE] == WEBHOOK_LIGHT_MODE:
self._light_state = data["sub_type"]
self._attr_extra_state_attributes.update(
@@ -189,128 +162,107 @@ class NetatmoCamera(NetatmoBase, Camera):
self.async_write_ha_state()
return
- @property
- def _data(self) -> pyatmo.AsyncCameraData:
- """Return data for this entity."""
- return cast(
- pyatmo.AsyncCameraData,
- self.data_handler.data[self._publishers[0]["name"]],
- )
-
async def async_camera_image(
self, width: int | None = None, height: int | None = None
) -> bytes | None:
"""Return a still image response from the camera."""
try:
- return cast(
- bytes, await self._data.async_get_live_snapshot(camera_id=self._id)
- )
+ return cast(bytes, await self._camera.async_get_live_snapshot())
except (
aiohttp.ClientPayloadError,
aiohttp.ContentTypeError,
aiohttp.ServerDisconnectedError,
aiohttp.ClientConnectorError,
- pyatmo.exceptions.ApiError,
+ NetatmoApiError,
) as err:
_LOGGER.debug("Could not fetch live camera image (%s)", err)
return None
@property
- def available(self) -> bool:
- """Return True if entity is available."""
- return bool(self._alim_status == "on" or self._status == "disconnected")
-
- @property
- def motion_detection_enabled(self) -> bool:
- """Return the camera motion detection status."""
- return bool(self._status == "on")
-
- @property
- def is_on(self) -> bool:
- """Return true if on."""
- return self.is_streaming
+ def supported_features(self) -> int:
+ """Return supported features."""
+ supported_features: int = CameraEntityFeature.ON_OFF
+ if self._model != "NDB":
+ supported_features |= CameraEntityFeature.STREAM
+ return supported_features
async def async_turn_off(self) -> None:
"""Turn off camera."""
- await self._data.async_set_state(
- home_id=self._home_id, camera_id=self._id, monitoring="off"
- )
+ await self._camera.async_monitoring_off()
async def async_turn_on(self) -> None:
"""Turn on camera."""
- await self._data.async_set_state(
- home_id=self._home_id, camera_id=self._id, monitoring="on"
- )
+ await self._camera.async_monitoring_on()
async def stream_source(self) -> str:
"""Return the stream source."""
- url = "{0}/live/files/{1}/index.m3u8"
- if self._localurl:
- return url.format(self._localurl, self._quality)
- return url.format(self._vpnurl, self._quality)
+ if self._camera.is_local:
+ await self._camera.async_update_camera_urls()
- @property
- def model(self) -> str:
- """Return the camera model."""
- return MODELS[self._model]
+ if self._camera.local_url:
+ return "{}/live/files/{}/index.m3u8".format(
+ self._camera.local_url, self._quality
+ )
+ return f"{self._camera.vpn_url}/live/files/{self._quality}/index.m3u8"
@callback
def async_update_callback(self) -> None:
"""Update the entity's state."""
- camera = self._data.get_camera(self._id)
- self._vpnurl, self._localurl = self._data.camera_urls(self._id)
- self._status = camera.get("status")
- self._sd_status = camera.get("sd_status")
- self._alim_status = camera.get("alim_status")
- self._is_local = camera.get("is_local")
- self._attr_is_streaming = bool(self._status == "on")
+ self._attr_is_on = self._camera.alim_status is not None
+ self._attr_available = self._camera.alim_status is not None
- if self._model == "NACamera": # Smart Indoor Camera
- self.hass.data[DOMAIN][DATA_EVENTS][self._id] = self.process_events(
- self._data.events.get(self._id, {})
- )
- elif self._model == "NOC": # Smart Outdoor Camera
- self.hass.data[DOMAIN][DATA_EVENTS][self._id] = self.process_events(
- self._data.outdoor_events.get(self._id, {})
- )
+ if self._camera.monitoring is not None:
+ self._attr_is_streaming = self._camera.monitoring
+ self._attr_motion_detection_enabled = self._camera.monitoring
+
+ self.hass.data[DOMAIN][DATA_EVENTS][self._id] = self.process_events(
+ self._camera.events
+ )
self._attr_extra_state_attributes.update(
{
"id": self._id,
- "status": self._status,
- "sd_status": self._sd_status,
- "alim_status": self._alim_status,
- "is_local": self._is_local,
- "vpn_url": self._vpnurl,
- "local_url": self._localurl,
+ "monitoring": self._monitoring,
+ "sd_status": self._camera.sd_status,
+ "alim_status": self._camera.alim_status,
+ "is_local": self._camera.is_local,
+ "vpn_url": self._camera.vpn_url,
+ "local_url": self._camera.local_url,
"light_state": self._light_state,
}
)
- def process_events(self, events: dict) -> dict:
+ def process_events(self, event_list: list[NaEvent]) -> dict:
"""Add meta data to events."""
- for event in events.values():
- if "video_id" not in event:
+ events = {}
+ for event in event_list:
+ if not (video_id := event.video_id):
continue
- if self._is_local:
- event[
- "media_url"
- ] = f"{self._localurl}/vod/{event['video_id']}/files/{self._quality}/index.m3u8"
- else:
- event[
- "media_url"
- ] = f"{self._vpnurl}/vod/{event['video_id']}/files/{self._quality}/index.m3u8"
+ event_data = event.__dict__
+ event_data["subevents"] = [
+ event.__dict__
+ for event in event_data.get("subevents", [])
+ if not isinstance(event, dict)
+ ]
+ event_data["media_url"] = self.get_video_url(video_id)
+ events[event.event_time] = event_data
return events
+ def get_video_url(self, video_id: str) -> str:
+ """Get video url."""
+ if self._camera.is_local:
+ return f"{self._camera.local_url}/vod/{video_id}/files/{self._quality}/index.m3u8"
+ return f"{self._camera.vpn_url}/vod/{video_id}/files/{self._quality}/index.m3u8"
+
def fetch_person_ids(self, persons: list[str | None]) -> list[str]:
- """Fetch matching person ids for give list of persons."""
+ """Fetch matching person ids for given list of persons."""
person_ids = []
person_id_errors = []
for person in persons:
person_id = None
- for pid, data in self._data.persons[self._home_id].items():
- if data.get("pseudo") == person:
+ for pid, data in self._camera.home.persons.items():
+ if data.pseudo == person:
person_ids.append(pid)
person_id = pid
break
@@ -328,9 +280,7 @@ class NetatmoCamera(NetatmoBase, Camera):
persons = kwargs.get(ATTR_PERSONS, [])
person_ids = self.fetch_person_ids(persons)
- await self._data.async_set_persons_home(
- person_ids=person_ids, home_id=self._home_id
- )
+ await self._camera.home.async_set_persons_home(person_ids=person_ids)
_LOGGER.debug("Set %s as at home", persons)
async def _service_set_person_away(self, **kwargs: Any) -> None:
@@ -339,9 +289,8 @@ class NetatmoCamera(NetatmoBase, Camera):
person_ids = self.fetch_person_ids([person] if person else [])
person_id = next(iter(person_ids), None)
- await self._data.async_set_persons_away(
+ await self._camera.home.async_set_persons_away(
person_id=person_id,
- home_id=self._home_id,
)
if person_id:
@@ -351,10 +300,11 @@ class NetatmoCamera(NetatmoBase, Camera):
async def _service_set_camera_light(self, **kwargs: Any) -> None:
"""Service to set light mode."""
+ if not isinstance(self._camera, NaModules.netatmo.NOC):
+ raise HomeAssistantError(
+ f"{self._model} <{self._device_name}> does not have a floodlight"
+ )
+
mode = str(kwargs.get(ATTR_CAMERA_LIGHT_MODE))
_LOGGER.debug("Turn %s camera light for '%s'", mode, self._attr_name)
- await self._data.async_set_state(
- home_id=self._home_id,
- camera_id=self._id,
- floodlight=mode,
- )
+ await self._camera.async_set_floodlight_state(mode)
diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py
index 6b30989dd8f..400004ee4d1 100644
--- a/homeassistant/components/netatmo/climate.py
+++ b/homeassistant/components/netatmo/climate.py
@@ -2,15 +2,16 @@
from __future__ import annotations
import logging
-from typing import Any
+from typing import Any, cast
-import pyatmo
+from pyatmo.modules import NATherm1
import voluptuous as vol
from homeassistant.components.climate import (
DEFAULT_MIN_TEMP,
PRESET_AWAY,
PRESET_BOOST,
+ PRESET_HOME,
ClimateEntity,
ClimateEntityFeature,
HVACAction,
@@ -25,12 +26,8 @@ from homeassistant.const import (
TEMP_CELSIUS,
)
from homeassistant.core import HomeAssistant, callback
-from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import config_validation as cv, entity_platform
-from homeassistant.helpers.dispatcher import (
- async_dispatcher_connect,
- async_dispatcher_send,
-)
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@@ -38,25 +35,17 @@ from .const import (
ATTR_HEATING_POWER_REQUEST,
ATTR_SCHEDULE_NAME,
ATTR_SELECTED_SCHEDULE,
- DATA_HANDLER,
- DATA_HOMES,
+ CONF_URL_ENERGY,
DATA_SCHEDULES,
DOMAIN,
EVENT_TYPE_CANCEL_SET_POINT,
EVENT_TYPE_SCHEDULE,
EVENT_TYPE_SET_POINT,
EVENT_TYPE_THERM_MODE,
- NETATMO_CREATE_BATTERY,
+ NETATMO_CREATE_CLIMATE,
SERVICE_SET_SCHEDULE,
- SIGNAL_NAME,
- TYPE_ENERGY,
-)
-from .data_handler import (
- CLIMATE_STATE_CLASS_NAME,
- CLIMATE_TOPOLOGY_CLASS_NAME,
- NetatmoDataHandler,
- NetatmoDevice,
)
+from .data_handler import HOME, SIGNAL_NAME, NetatmoRoom
from .netatmo_entity_base import NetatmoBase
_LOGGER = logging.getLogger(__name__)
@@ -65,6 +54,9 @@ PRESET_FROST_GUARD = "Frost Guard"
PRESET_SCHEDULE = "Schedule"
PRESET_MANUAL = "Manual"
+SUPPORT_FLAGS = (
+ ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
+)
SUPPORT_PRESET = [PRESET_AWAY, PRESET_BOOST, PRESET_FROST_GUARD, PRESET_SCHEDULE]
STATE_NETATMO_SCHEDULE = "schedule"
@@ -116,51 +108,22 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Netatmo energy platform."""
- data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER]
- climate_topology = data_handler.data.get(CLIMATE_TOPOLOGY_CLASS_NAME)
+ @callback
+ def _create_entity(netatmo_device: NetatmoRoom) -> None:
+ entity = NetatmoThermostat(netatmo_device)
+ async_add_entities([entity])
- if not climate_topology or climate_topology.raw_data == {}:
- raise PlatformNotReady
-
- entities = []
- for home_id in climate_topology.home_ids:
- signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}"
-
- await data_handler.subscribe(
- CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id
- )
-
- if (climate_state := data_handler.data[signal_name]) is None:
- continue
-
- climate_topology.register_handler(home_id, climate_state.process_topology)
-
- 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)
+ entry.async_on_unload(
+ async_dispatcher_connect(hass, NETATMO_CREATE_CLIMATE, _create_entity)
+ )
platform = entity_platform.async_get_current_platform()
-
- if climate_topology is not None:
- platform.async_register_entity_service(
- SERVICE_SET_SCHEDULE,
- {vol.Required(ATTR_SCHEDULE_NAME): cv.string},
- "_async_service_set_schedule",
- )
+ platform.async_register_entity_service(
+ SERVICE_SET_SCHEDULE,
+ {vol.Required(ATTR_SCHEDULE_NAME): cv.string},
+ "_async_service_set_schedule",
+ )
class NetatmoThermostat(NetatmoBase, ClimateEntity):
@@ -169,42 +132,33 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
_attr_hvac_mode = HVACMode.AUTO
_attr_max_temp = DEFAULT_MAX_TEMP
_attr_preset_modes = SUPPORT_PRESET
- _attr_supported_features = (
- ClimateEntityFeature.TARGET_TEMPERATURE | ClimateEntityFeature.PRESET_MODE
- )
+ _attr_supported_features = SUPPORT_FLAGS
_attr_target_temperature_step = PRECISION_HALVES
_attr_temperature_unit = TEMP_CELSIUS
- def __init__(
- self, data_handler: NetatmoDataHandler, room: pyatmo.climate.NetatmoRoom
- ) -> None:
+ def __init__(self, netatmo_device: NetatmoRoom) -> None:
"""Initialize the sensor."""
ClimateEntity.__init__(self)
- super().__init__(data_handler)
+ super().__init__(netatmo_device.data_handler)
- self._room = room
+ self._room = netatmo_device.room
self._id = self._room.entity_id
+ self._home_id = self._room.home.entity_id
- self._signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{self._room.home.entity_id}"
- self._climate_state: pyatmo.AsyncClimate = data_handler.data[self._signal_name]
-
+ self._signal_name = f"{HOME}-{self._home_id}"
self._publishers.extend(
[
{
- "name": CLIMATE_TOPOLOGY_CLASS_NAME,
- SIGNAL_NAME: CLIMATE_TOPOLOGY_CLASS_NAME,
- },
- {
- "name": CLIMATE_STATE_CLASS_NAME,
+ "name": HOME,
"home_id": self._room.home.entity_id,
SIGNAL_NAME: self._signal_name,
},
]
)
- self._model: str = getattr(room.device_type, "value")
+ self._model: str = f"{self._room.climate_type}"
- self._netatmo_type = TYPE_ENERGY
+ self._config_url = CONF_URL_ENERGY
self._attr_name = self._room.name
self._away: bool | None = None
@@ -231,7 +185,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
EVENT_TYPE_CANCEL_SET_POINT,
EVENT_TYPE_SCHEDULE,
):
- self.data_handler.config_entry.async_on_unload(
+ self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"signal-{DOMAIN}-webhook-{event_type}",
@@ -239,21 +193,6 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
)
)
- for module in self._room.modules.values():
- if getattr(module.device_type, "value") not in [NA_THERM, NA_VALVE]:
- continue
-
- async_dispatcher_send(
- self.hass,
- NETATMO_CREATE_BATTERY,
- NetatmoDevice(
- self.data_handler,
- module,
- self._id,
- self._signal_name,
- ),
- )
-
@callback
def handle_event(self, event: dict) -> None:
"""Handle webhook events."""
@@ -289,7 +228,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
self._attr_target_temperature = self._hg_temperature
elif self._attr_preset_mode == PRESET_AWAY:
self._attr_target_temperature = self._away_temperature
- elif self._attr_preset_mode == PRESET_SCHEDULE:
+ elif self._attr_preset_mode in [PRESET_SCHEDULE, PRESET_HOME]:
self.async_update_callback()
self.data_handler.async_force_update(self._signal_name)
self.async_write_ha_state()
@@ -322,6 +261,10 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
data["event_type"] == EVENT_TYPE_CANCEL_SET_POINT
and self._room.entity_id == room["id"]
):
+ if self._attr_hvac_mode == HVACMode.OFF:
+ self._attr_hvac_mode = HVACMode.AUTO
+ self._attr_preset_mode = PRESET_MAP_NETATMO[PRESET_SCHEDULE]
+
self.async_update_callback()
self.async_write_ha_state()
return
@@ -329,7 +272,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
@property
def hvac_action(self) -> HVACAction:
"""Return the current running hvac operation if supported."""
- if self._model == NA_THERM and self._boilerstatus is not None:
+ if self._model != NA_VALVE and self._boilerstatus is not None:
return CURRENT_HVAC_MAP_NETATMO[self._boilerstatus]
# Maybe it is a valve
if (
@@ -343,55 +286,36 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
if hvac_mode == HVACMode.OFF:
await self.async_turn_off()
elif hvac_mode == HVACMode.AUTO:
- if self.hvac_mode == HVACMode.OFF:
- await self.async_turn_on()
await self.async_set_preset_mode(PRESET_SCHEDULE)
elif hvac_mode == HVACMode.HEAT:
await self.async_set_preset_mode(PRESET_BOOST)
async def async_set_preset_mode(self, preset_mode: str) -> None:
"""Set new preset mode."""
- if self.hvac_mode == HVACMode.OFF:
- await self.async_turn_on()
-
- if self.target_temperature == 0:
- await self._climate_state.async_set_room_thermpoint(
- self._room.entity_id,
- STATE_NETATMO_HOME,
- )
-
if (
preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX)
and self._model == NA_VALVE
- and self.hvac_mode == HVACMode.HEAT
+ and self._attr_hvac_mode == HVACMode.HEAT
):
- await self._climate_state.async_set_room_thermpoint(
- self._room.entity_id,
+ await self._room.async_therm_set(
STATE_NETATMO_HOME,
)
elif (
preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX) and self._model == NA_VALVE
):
- await self._climate_state.async_set_room_thermpoint(
- self._room.entity_id,
+ await self._room.async_therm_set(
STATE_NETATMO_MANUAL,
DEFAULT_MAX_TEMP,
)
elif (
preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX)
- and self.hvac_mode == HVACMode.HEAT
+ and self._attr_hvac_mode == HVACMode.HEAT
):
- await self._climate_state.async_set_room_thermpoint(
- self._room.entity_id, STATE_NETATMO_HOME
- )
+ await self._room.async_therm_set(STATE_NETATMO_HOME)
elif preset_mode in (PRESET_BOOST, STATE_NETATMO_MAX):
- await self._climate_state.async_set_room_thermpoint(
- self._room.entity_id, PRESET_MAP_NETATMO[preset_mode]
- )
+ await self._room.async_therm_set(PRESET_MAP_NETATMO[preset_mode])
elif preset_mode in (PRESET_SCHEDULE, PRESET_FROST_GUARD, PRESET_AWAY):
- await self._climate_state.async_set_thermmode(
- PRESET_MAP_NETATMO[preset_mode]
- )
+ await self._room.async_therm_set(PRESET_MAP_NETATMO[preset_mode])
else:
_LOGGER.error("Preset mode '%s' not available", preset_mode)
@@ -399,33 +323,25 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature for 2 hours."""
- if (temp := kwargs.get(ATTR_TEMPERATURE)) is None:
- return
- await self._climate_state.async_set_room_thermpoint(
- self._room.entity_id, STATE_NETATMO_MANUAL, min(temp, DEFAULT_MAX_TEMP)
+ await self._room.async_therm_set(
+ STATE_NETATMO_MANUAL, min(kwargs[ATTR_TEMPERATURE], DEFAULT_MAX_TEMP)
)
-
self.async_write_ha_state()
async def async_turn_off(self) -> None:
"""Turn the entity off."""
if self._model == NA_VALVE:
- await self._climate_state.async_set_room_thermpoint(
- self._room.entity_id,
+ await self._room.async_therm_set(
STATE_NETATMO_MANUAL,
DEFAULT_MIN_TEMP,
)
- elif self.hvac_mode != HVACMode.OFF:
- await self._climate_state.async_set_room_thermpoint(
- self._room.entity_id, STATE_NETATMO_OFF
- )
+ elif self._attr_hvac_mode != HVACMode.OFF:
+ await self._room.async_therm_set(STATE_NETATMO_OFF)
self.async_write_ha_state()
async def async_turn_on(self) -> None:
"""Turn the entity on."""
- await self._climate_state.async_set_room_thermpoint(
- self._room.entity_id, STATE_NETATMO_HOME
- )
+ await self._room.async_therm_set(STATE_NETATMO_HOME)
self.async_write_ha_state()
@property
@@ -466,8 +382,11 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
] = self._room.heating_power_request
else:
for module in self._room.modules.values():
- self._boilerstatus = module.boiler_status
- break
+ if hasattr(module, "boiler_status"):
+ module = cast(NATherm1, module)
+ if module.boiler_status is not None:
+ self._boilerstatus = module.boiler_status
+ break
async def _async_service_set_schedule(self, **kwargs: Any) -> None:
schedule_name = kwargs.get(ATTR_SCHEDULE_NAME)
@@ -483,7 +402,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity):
_LOGGER.error("%s is not a valid schedule", kwargs.get(ATTR_SCHEDULE_NAME))
return
- await self._climate_state.async_switch_home_schedule(schedule_id=schedule_id)
+ await self._room.home.async_switch_schedule(schedule_id=schedule_id)
_LOGGER.debug(
"Setting %s schedule to %s (%s)",
self._room.home.entity_id,
diff --git a/homeassistant/components/netatmo/config_flow.py b/homeassistant/components/netatmo/config_flow.py
index ba63c76ad66..99fa195b118 100644
--- a/homeassistant/components/netatmo/config_flow.py
+++ b/homeassistant/components/netatmo/config_flow.py
@@ -6,6 +6,7 @@ import logging
from typing import Any
import uuid
+from pyatmo.const import ALL_SCOPES
import voluptuous as vol
from homeassistant import config_entries
@@ -25,7 +26,6 @@ from .const import (
CONF_UUID,
CONF_WEATHER_AREAS,
DOMAIN,
- NETATMO_SCOPES,
)
_LOGGER = logging.getLogger(__name__)
@@ -54,7 +54,7 @@ class NetatmoFlowHandler(
@property
def extra_authorize_data(self) -> dict:
"""Extra data that needs to be appended to the authorize url."""
- return {"scope": " ".join(NETATMO_SCOPES)}
+ return {"scope": " ".join(ALL_SCOPES)}
async def async_step_user(self, user_input: dict | None = None) -> FlowResult:
"""Handle a flow start."""
diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py
index 6bd66fa9644..e93d0c91a07 100644
--- a/homeassistant/components/netatmo/const.py
+++ b/homeassistant/components/netatmo/const.py
@@ -10,60 +10,17 @@ DEFAULT_ATTRIBUTION = f"Data provided by {MANUFACTURER}"
PLATFORMS = [
Platform.CAMERA,
Platform.CLIMATE,
+ Platform.COVER,
Platform.LIGHT,
Platform.SELECT,
Platform.SENSOR,
+ Platform.SWITCH,
]
-NETATMO_SCOPES = [
- "access_camera",
- "access_presence",
- "read_camera",
- "read_homecoach",
- "read_presence",
- "read_smokedetector",
- "read_station",
- "read_thermostat",
- "write_camera",
- "write_presence",
- "write_thermostat",
-]
-
-MODEL_NAPLUG = "Relay"
-MODEL_NATHERM1 = "Smart Thermostat"
-MODEL_NRV = "Smart Radiator Valves"
-MODEL_NOC = "Smart Outdoor Camera"
-MODEL_NACAMERA = "Smart Indoor Camera"
-MODEL_NSD = "Smart Smoke Alarm"
-MODEL_NACAMDOORTAG = "Smart Door and Window Sensors"
-MODEL_NHC = "Smart Indoor Air Quality Monitor"
-MODEL_NAMAIN = "Smart Home Weather station – indoor module"
-MODEL_NAMODULE1 = "Smart Home Weather station – outdoor module"
-MODEL_NAMODULE4 = "Smart Additional Indoor module"
-MODEL_NAMODULE3 = "Smart Rain Gauge"
-MODEL_NAMODULE2 = "Smart Anemometer"
-MODEL_PUBLIC = "Public Weather stations"
-
-MODELS = {
- "NAPlug": MODEL_NAPLUG,
- "NATherm1": MODEL_NATHERM1,
- "NRV": MODEL_NRV,
- "NACamera": MODEL_NACAMERA,
- "NOC": MODEL_NOC,
- "NSD": MODEL_NSD,
- "NACamDoorTag": MODEL_NACAMDOORTAG,
- "NHC": MODEL_NHC,
- "NAMain": MODEL_NAMAIN,
- "NAModule1": MODEL_NAMODULE1,
- "NAModule4": MODEL_NAMODULE4,
- "NAModule3": MODEL_NAMODULE3,
- "NAModule2": MODEL_NAMODULE2,
- "public": MODEL_PUBLIC,
-}
-
-TYPE_SECURITY = "security"
-TYPE_ENERGY = "energy"
-TYPE_WEATHER = "weather"
+CONF_URL_SECURITY = "https://home.netatmo.com/security"
+CONF_URL_ENERGY = "https://my.netatmo.com/app/energy"
+CONF_URL_WEATHER = "https://my.netatmo.com/app/weather"
+CONF_URL_CONTROL = "https://home.netatmo.com/control"
AUTH = "netatmo_auth"
CONF_PUBLIC = "public_sensor_config"
@@ -71,7 +28,18 @@ CAMERA_DATA = "netatmo_camera"
HOME_DATA = "netatmo_home_data"
DATA_HANDLER = "netatmo_data_handler"
SIGNAL_NAME = "signal_name"
+
NETATMO_CREATE_BATTERY = "netatmo_create_battery"
+NETATMO_CREATE_CAMERA = "netatmo_create_camera"
+NETATMO_CREATE_CAMERA_LIGHT = "netatmo_create_camera_light"
+NETATMO_CREATE_CLIMATE = "netatmo_create_climate"
+NETATMO_CREATE_COVER = "netatmo_create_cover"
+NETATMO_CREATE_LIGHT = "netatmo_create_light"
+NETATMO_CREATE_ROOM_SENSOR = "netatmo_create_room_sensor"
+NETATMO_CREATE_SELECT = "netatmo_create_select"
+NETATMO_CREATE_SENSOR = "netatmo_create_sensor"
+NETATMO_CREATE_SWITCH = "netatmo_create_switch"
+NETATMO_CREATE_WEATHER_SENSOR = "netatmo_create_weather_sensor"
CONF_AREA_NAME = "area_name"
CONF_CLOUDHOOK_URL = "cloudhook_url"
diff --git a/homeassistant/components/netatmo/cover.py b/homeassistant/components/netatmo/cover.py
new file mode 100644
index 00000000000..6d755d828d3
--- /dev/null
+++ b/homeassistant/components/netatmo/cover.py
@@ -0,0 +1,110 @@
+"""Support for Netatmo/Bubendorff covers."""
+from __future__ import annotations
+
+import logging
+from typing import Any, cast
+
+from pyatmo import modules as NaModules
+
+from homeassistant.components.cover import (
+ ATTR_POSITION,
+ CoverDeviceClass,
+ CoverEntity,
+ CoverEntityFeature,
+)
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+
+from .const import CONF_URL_CONTROL, NETATMO_CREATE_COVER
+from .data_handler import HOME, SIGNAL_NAME, NetatmoDevice
+from .netatmo_entity_base import NetatmoBase
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(
+ hass: HomeAssistant,
+ entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
+) -> None:
+ """Set up the Netatmo cover platform."""
+
+ @callback
+ def _create_entity(netatmo_device: NetatmoDevice) -> None:
+ entity = NetatmoCover(netatmo_device)
+ _LOGGER.debug("Adding cover %s", entity)
+ async_add_entities([entity])
+
+ entry.async_on_unload(
+ async_dispatcher_connect(hass, NETATMO_CREATE_COVER, _create_entity)
+ )
+
+
+class NetatmoCover(NetatmoBase, CoverEntity):
+ """Representation of a Netatmo cover device."""
+
+ _attr_supported_features = (
+ CoverEntityFeature.OPEN
+ | CoverEntityFeature.CLOSE
+ | CoverEntityFeature.STOP
+ | CoverEntityFeature.SET_POSITION
+ )
+
+ def __init__(self, netatmo_device: NetatmoDevice) -> None:
+ """Initialize the Netatmo device."""
+ super().__init__(netatmo_device.data_handler)
+
+ self._cover = cast(NaModules.Shutter, netatmo_device.device)
+
+ self._id = self._cover.entity_id
+ self._attr_name = self._device_name = self._cover.name
+ self._model = self._cover.device_type
+ self._config_url = CONF_URL_CONTROL
+
+ self._home_id = self._cover.home.entity_id
+ self._attr_is_closed = self._cover.current_position == 0
+
+ self._signal_name = f"{HOME}-{self._home_id}"
+ self._publishers.extend(
+ [
+ {
+ "name": HOME,
+ "home_id": self._home_id,
+ SIGNAL_NAME: self._signal_name,
+ },
+ ]
+ )
+ self._attr_unique_id = f"{self._id}-{self._model}"
+
+ async def async_close_cover(self, **kwargs: Any) -> None:
+ """Close the cover."""
+ await self._cover.async_close()
+ self._attr_is_closed = True
+ self.async_write_ha_state()
+
+ async def async_open_cover(self, **kwargs: Any) -> None:
+ """Open the cover."""
+ await self._cover.async_open()
+ self._attr_is_closed = False
+ self.async_write_ha_state()
+
+ async def async_stop_cover(self, **kwargs: Any) -> None:
+ """Stop the cover."""
+ await self._cover.async_stop()
+
+ async def async_set_cover_position(self, **kwargs: Any) -> None:
+ """Move the cover shutter to a specific position."""
+ await self._cover.async_set_target_position(kwargs[ATTR_POSITION])
+
+ @property
+ def device_class(self) -> str:
+ """Return the device class."""
+ return CoverDeviceClass.SHUTTER
+
+ @callback
+ def async_update_callback(self) -> None:
+ """Update the entity's state."""
+ self._attr_is_closed = self._cover.current_position == 0
+ self._attr_current_cover_position = self._cover.current_position
diff --git a/homeassistant/components/netatmo/data_handler.py b/homeassistant/components/netatmo/data_handler.py
index 50a3bed17ff..a376e6ee187 100644
--- a/homeassistant/components/netatmo/data_handler.py
+++ b/homeassistant/components/netatmo/data_handler.py
@@ -11,16 +11,34 @@ from time import time
from typing import Any
import pyatmo
+from pyatmo.modules.device_types import DeviceCategory as NetatmoDeviceCategory
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
-from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.dispatcher import (
+ async_dispatcher_connect,
+ async_dispatcher_send,
+)
from homeassistant.helpers.event import async_track_time_interval
from .const import (
AUTH,
+ DATA_PERSONS,
+ DATA_SCHEDULES,
DOMAIN,
MANUFACTURER,
+ NETATMO_CREATE_BATTERY,
+ NETATMO_CREATE_CAMERA,
+ NETATMO_CREATE_CAMERA_LIGHT,
+ NETATMO_CREATE_CLIMATE,
+ NETATMO_CREATE_COVER,
+ NETATMO_CREATE_LIGHT,
+ NETATMO_CREATE_ROOM_SENSOR,
+ NETATMO_CREATE_SELECT,
+ NETATMO_CREATE_SENSOR,
+ NETATMO_CREATE_SWITCH,
+ NETATMO_CREATE_WEATHER_SENSOR,
+ PLATFORMS,
WEBHOOK_ACTIVATION,
WEBHOOK_DEACTIVATION,
WEBHOOK_NACAMERA_CONNECTION,
@@ -29,30 +47,31 @@ from .const import (
_LOGGER = logging.getLogger(__name__)
-CAMERA_DATA_CLASS_NAME = "AsyncCameraData"
-WEATHERSTATION_DATA_CLASS_NAME = "AsyncWeatherStationData"
-HOMECOACH_DATA_CLASS_NAME = "AsyncHomeCoachData"
-CLIMATE_TOPOLOGY_CLASS_NAME = "AsyncClimateTopology"
-CLIMATE_STATE_CLASS_NAME = "AsyncClimate"
-PUBLICDATA_DATA_CLASS_NAME = "AsyncPublicData"
+SIGNAL_NAME = "signal_name"
+ACCOUNT = "account"
+HOME = "home"
+WEATHER = "weather"
+AIR_CARE = "air_care"
+PUBLIC = "public"
+EVENT = "event"
-DATA_CLASSES = {
- WEATHERSTATION_DATA_CLASS_NAME: pyatmo.AsyncWeatherStationData,
- HOMECOACH_DATA_CLASS_NAME: pyatmo.AsyncHomeCoachData,
- CAMERA_DATA_CLASS_NAME: pyatmo.AsyncCameraData,
- CLIMATE_TOPOLOGY_CLASS_NAME: pyatmo.AsyncClimateTopology,
- CLIMATE_STATE_CLASS_NAME: pyatmo.AsyncClimate,
- PUBLICDATA_DATA_CLASS_NAME: pyatmo.AsyncPublicData,
+PUBLISHERS = {
+ ACCOUNT: "async_update_topology",
+ HOME: "async_update_status",
+ WEATHER: "async_update_weather_stations",
+ AIR_CARE: "async_update_air_care",
+ PUBLIC: "async_update_public_weather",
+ EVENT: "async_update_events",
}
BATCH_SIZE = 3
DEFAULT_INTERVALS = {
- 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,
- PUBLICDATA_DATA_CLASS_NAME: 600,
+ ACCOUNT: 10800,
+ HOME: 300,
+ WEATHER: 600,
+ AIR_CARE: 300,
+ PUBLIC: 600,
+ EVENT: 600,
}
SCAN_INTERVAL = 60
@@ -62,7 +81,27 @@ class NetatmoDevice:
"""Netatmo device class."""
data_handler: NetatmoDataHandler
- device: pyatmo.climate.NetatmoModule
+ device: pyatmo.modules.Module
+ parent_id: str
+ signal_name: str
+
+
+@dataclass
+class NetatmoHome:
+ """Netatmo home class."""
+
+ data_handler: NetatmoDataHandler
+ home: pyatmo.Home
+ parent_id: str
+ signal_name: str
+
+
+@dataclass
+class NetatmoRoom:
+ """Netatmo room class."""
+
+ data_handler: NetatmoDataHandler
+ room: pyatmo.Room
parent_id: str
signal_name: str
@@ -74,25 +113,27 @@ class NetatmoPublisher:
name: str
interval: int
next_scan: float
- subscriptions: list[CALLBACK_TYPE | None]
+ subscriptions: set[CALLBACK_TYPE | None]
+ method: str
+ kwargs: dict
class NetatmoDataHandler:
"""Manages the Netatmo data handling."""
+ account: pyatmo.AsyncAccount
+
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
"""Initialize self."""
self.hass = hass
self.config_entry = config_entry
self._auth = hass.data[DOMAIN][config_entry.entry_id][AUTH]
self.publisher: dict[str, NetatmoPublisher] = {}
- self.data: dict = {}
self._queue: deque = deque()
self._webhook: bool = False
async def async_setup(self) -> None:
"""Set up the Netatmo data handler."""
-
async_track_time_interval(
self.hass, self.async_update, timedelta(seconds=SCAN_INTERVAL)
)
@@ -105,17 +146,14 @@ class NetatmoDataHandler:
)
)
- await asyncio.gather(
- *[
- self.subscribe(data_class, data_class, None)
- for data_class in (
- CLIMATE_TOPOLOGY_CLASS_NAME,
- CAMERA_DATA_CLASS_NAME,
- WEATHERSTATION_DATA_CLASS_NAME,
- HOMECOACH_DATA_CLASS_NAME,
- )
- ]
+ self.account = pyatmo.AsyncAccount(self._auth)
+
+ await self.subscribe(ACCOUNT, ACCOUNT, None)
+
+ await self.hass.config_entries.async_forward_entry_setups(
+ self.config_entry, PLATFORMS
)
+ await self.async_dispatch()
async def async_update(self, event_time: datetime) -> None:
"""
@@ -153,19 +191,17 @@ class NetatmoDataHandler:
elif event["data"][WEBHOOK_PUSH_TYPE] == WEBHOOK_NACAMERA_CONNECTION:
_LOGGER.debug("%s camera reconnected", MANUFACTURER)
- self.async_force_update(CAMERA_DATA_CLASS_NAME)
+ self.async_force_update(ACCOUNT)
async def async_fetch_data(self, signal_name: str) -> None:
"""Fetch data and notify."""
- if self.data[signal_name] is None:
- return
-
try:
- await self.data[signal_name].async_update()
+ await getattr(self.account, self.publisher[signal_name].method)(
+ **self.publisher[signal_name].kwargs
+ )
except pyatmo.NoDevice as err:
_LOGGER.debug(err)
- self.data[signal_name] = None
except pyatmo.ApiError as err:
_LOGGER.debug(err)
@@ -188,18 +224,21 @@ class NetatmoDataHandler:
"""Subscribe to publisher."""
if signal_name in self.publisher:
if update_callback not in self.publisher[signal_name].subscriptions:
- self.publisher[signal_name].subscriptions.append(update_callback)
+ self.publisher[signal_name].subscriptions.add(update_callback)
return
+ if publisher == "public":
+ kwargs = {"area_id": self.account.register_public_weather_area(**kwargs)}
+
self.publisher[signal_name] = NetatmoPublisher(
name=signal_name,
interval=DEFAULT_INTERVALS[publisher],
next_scan=time() + DEFAULT_INTERVALS[publisher],
- subscriptions=[update_callback],
+ subscriptions={update_callback},
+ method=PUBLISHERS[publisher],
+ kwargs=kwargs,
)
- self.data[signal_name] = DATA_CLASSES[publisher](self._auth, **kwargs)
-
try:
await self.async_fetch_data(signal_name)
except KeyError:
@@ -213,15 +252,158 @@ class NetatmoDataHandler:
self, signal_name: str, update_callback: CALLBACK_TYPE | None
) -> None:
"""Unsubscribe from publisher."""
+ if update_callback in self.publisher[signal_name].subscriptions:
+ return
+
self.publisher[signal_name].subscriptions.remove(update_callback)
if not self.publisher[signal_name].subscriptions:
self._queue.remove(self.publisher[signal_name])
self.publisher.pop(signal_name)
- self.data.pop(signal_name)
_LOGGER.debug("Publisher %s removed", signal_name)
@property
def webhook(self) -> bool:
"""Return the webhook state."""
return self._webhook
+
+ async def async_dispatch(self) -> None:
+ """Dispatch the creation of entities."""
+ await self.subscribe(WEATHER, WEATHER, None)
+ await self.subscribe(AIR_CARE, AIR_CARE, None)
+
+ self.setup_air_care()
+
+ for home in self.account.homes.values():
+ signal_home = f"{HOME}-{home.entity_id}"
+
+ await self.subscribe(HOME, signal_home, None, home_id=home.entity_id)
+ await self.subscribe(EVENT, signal_home, None, home_id=home.entity_id)
+
+ self.setup_climate_schedule_select(home, signal_home)
+ self.setup_rooms(home, signal_home)
+ self.setup_modules(home, signal_home)
+
+ self.hass.data[DOMAIN][DATA_PERSONS][home.entity_id] = {
+ person.entity_id: person.pseudo for person in home.persons.values()
+ }
+
+ def setup_air_care(self) -> None:
+ """Set up home coach/air care modules."""
+ for module in self.account.modules.values():
+ if module.device_category is NetatmoDeviceCategory.air_care:
+ async_dispatcher_send(
+ self.hass,
+ NETATMO_CREATE_WEATHER_SENSOR,
+ NetatmoDevice(
+ self,
+ module,
+ AIR_CARE,
+ AIR_CARE,
+ ),
+ )
+
+ def setup_modules(self, home: pyatmo.Home, signal_home: str) -> None:
+ """Set up modules."""
+ netatmo_type_signal_map = {
+ NetatmoDeviceCategory.camera: [
+ NETATMO_CREATE_CAMERA,
+ NETATMO_CREATE_CAMERA_LIGHT,
+ ],
+ NetatmoDeviceCategory.dimmer: [NETATMO_CREATE_LIGHT],
+ NetatmoDeviceCategory.shutter: [NETATMO_CREATE_COVER],
+ NetatmoDeviceCategory.switch: [
+ NETATMO_CREATE_LIGHT,
+ NETATMO_CREATE_SWITCH,
+ NETATMO_CREATE_SENSOR,
+ ],
+ NetatmoDeviceCategory.meter: [NETATMO_CREATE_SENSOR],
+ }
+ for module in home.modules.values():
+ if not module.device_category:
+ continue
+
+ for signal in netatmo_type_signal_map.get(module.device_category, []):
+ async_dispatcher_send(
+ self.hass,
+ signal,
+ NetatmoDevice(
+ self,
+ module,
+ home.entity_id,
+ signal_home,
+ ),
+ )
+ if module.device_category is NetatmoDeviceCategory.weather:
+ async_dispatcher_send(
+ self.hass,
+ NETATMO_CREATE_WEATHER_SENSOR,
+ NetatmoDevice(
+ self,
+ module,
+ home.entity_id,
+ WEATHER,
+ ),
+ )
+
+ def setup_rooms(self, home: pyatmo.Home, signal_home: str) -> None:
+ """Set up rooms."""
+ for room in home.rooms.values():
+ if NetatmoDeviceCategory.climate in room.features:
+ async_dispatcher_send(
+ self.hass,
+ NETATMO_CREATE_CLIMATE,
+ NetatmoRoom(
+ self,
+ room,
+ home.entity_id,
+ signal_home,
+ ),
+ )
+
+ for module in room.modules.values():
+ if module.device_category is NetatmoDeviceCategory.climate:
+ async_dispatcher_send(
+ self.hass,
+ NETATMO_CREATE_BATTERY,
+ NetatmoDevice(
+ self,
+ module,
+ room.entity_id,
+ signal_home,
+ ),
+ )
+
+ if "humidity" in room.features:
+ async_dispatcher_send(
+ self.hass,
+ NETATMO_CREATE_ROOM_SENSOR,
+ NetatmoRoom(
+ self,
+ room,
+ room.entity_id,
+ signal_home,
+ ),
+ )
+
+ def setup_climate_schedule_select(
+ self, home: pyatmo.Home, signal_home: str
+ ) -> None:
+ """Set up climate schedule per home."""
+ if NetatmoDeviceCategory.climate in [
+ next(iter(x)) for x in [room.features for room in home.rooms.values()] if x
+ ]:
+ self.hass.data[DOMAIN][DATA_SCHEDULES][home.entity_id] = self.account.homes[
+ home.entity_id
+ ].schedules
+
+ async_dispatcher_send(
+ self.hass,
+ NETATMO_CREATE_SELECT,
+ NetatmoHome(
+ self,
+ home,
+ home.entity_id,
+ signal_home,
+ ),
+ )
diff --git a/homeassistant/components/netatmo/device_trigger.py b/homeassistant/components/netatmo/device_trigger.py
index 955671e3dc1..b037f45533f 100644
--- a/homeassistant/components/netatmo/device_trigger.py
+++ b/homeassistant/components/netatmo/device_trigger.py
@@ -31,10 +31,6 @@ from .const import (
DOMAIN,
EVENT_TYPE_THERM_MODE,
INDOOR_CAMERA_TRIGGERS,
- MODEL_NACAMERA,
- MODEL_NATHERM1,
- MODEL_NOC,
- MODEL_NRV,
NETATMO_EVENT,
OUTDOOR_CAMERA_TRIGGERS,
)
@@ -42,10 +38,10 @@ from .const import (
CONF_SUBTYPE = "subtype"
DEVICES = {
- MODEL_NACAMERA: INDOOR_CAMERA_TRIGGERS,
- MODEL_NOC: OUTDOOR_CAMERA_TRIGGERS,
- MODEL_NATHERM1: CLIMATE_TRIGGERS,
- MODEL_NRV: CLIMATE_TRIGGERS,
+ "NACamera": INDOOR_CAMERA_TRIGGERS,
+ "NOC": OUTDOOR_CAMERA_TRIGGERS,
+ "NATherm1": CLIMATE_TRIGGERS,
+ "NRV": CLIMATE_TRIGGERS,
}
SUBTYPES = {
@@ -76,7 +72,7 @@ async def async_validate_trigger_config(
device_registry = dr.async_get(hass)
device = device_registry.async_get(config[CONF_DEVICE_ID])
- if not device:
+ if not device or device.model is None:
raise InvalidDeviceAutomationConfig(
f"Trigger invalid, device with ID {config[CONF_DEVICE_ID]} not found"
)
diff --git a/homeassistant/components/netatmo/diagnostics.py b/homeassistant/components/netatmo/diagnostics.py
index 6c82c7f1db7..cac9c695f19 100644
--- a/homeassistant/components/netatmo/diagnostics.py
+++ b/homeassistant/components/netatmo/diagnostics.py
@@ -6,7 +6,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import DATA_HANDLER, DOMAIN
-from .data_handler import CLIMATE_TOPOLOGY_CLASS_NAME, NetatmoDataHandler
+from .data_handler import ACCOUNT, NetatmoDataHandler
TO_REDACT = {
"access_token",
@@ -45,8 +45,8 @@ async def async_get_config_entry_diagnostics(
TO_REDACT,
),
"data": {
- CLIMATE_TOPOLOGY_CLASS_NAME: async_redact_data(
- getattr(data_handler.data[CLIMATE_TOPOLOGY_CLASS_NAME], "raw_data"),
+ ACCOUNT: async_redact_data(
+ getattr(data_handler.account, "raw_data"),
TO_REDACT,
)
},
diff --git a/homeassistant/components/netatmo/light.py b/homeassistant/components/netatmo/light.py
index 6567ae770f2..b3e352eb7d8 100644
--- a/homeassistant/components/netatmo/light.py
+++ b/homeassistant/components/netatmo/light.py
@@ -4,25 +4,25 @@ from __future__ import annotations
import logging
from typing import Any, cast
-import pyatmo
+from pyatmo import modules as NaModules
-from homeassistant.components.light import ColorMode, LightEntity
+from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
-from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import (
- DATA_HANDLER,
+ CONF_URL_CONTROL,
+ CONF_URL_SECURITY,
DOMAIN,
EVENT_TYPE_LIGHT_MODE,
- SIGNAL_NAME,
- TYPE_SECURITY,
+ NETATMO_CREATE_CAMERA_LIGHT,
+ NETATMO_CREATE_LIGHT,
WEBHOOK_LIGHT_MODE,
WEBHOOK_PUSH_TYPE,
)
-from .data_handler import CAMERA_DATA_CLASS_NAME, NetatmoDataHandler
+from .data_handler import HOME, SIGNAL_NAME, NetatmoDevice
from .netatmo_entity_base import NetatmoBase
_LOGGER = logging.getLogger(__name__)
@@ -32,66 +32,73 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Netatmo camera light platform."""
- data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER]
- data_class = data_handler.data.get(CAMERA_DATA_CLASS_NAME)
- if not data_class or data_class.raw_data == {}:
- raise PlatformNotReady
+ @callback
+ def _create_camera_light_entity(netatmo_device: NetatmoDevice) -> None:
+ if not hasattr(netatmo_device.device, "floodlight"):
+ return
- all_cameras = []
- for home in data_handler.data[CAMERA_DATA_CLASS_NAME].cameras.values():
- for camera in home.values():
- all_cameras.append(camera)
+ entity = NetatmoCameraLight(netatmo_device)
+ async_add_entities([entity])
- entities = [
- NetatmoLight(
- data_handler,
- camera["id"],
- camera["type"],
- camera["home_id"],
+ entry.async_on_unload(
+ async_dispatcher_connect(
+ hass, NETATMO_CREATE_CAMERA_LIGHT, _create_camera_light_entity
)
- for camera in all_cameras
- if camera["type"] == "NOC"
- ]
+ )
- _LOGGER.debug("Adding camera lights %s", entities)
- async_add_entities(entities, True)
+ @callback
+ def _create_entity(netatmo_device: NetatmoDevice) -> None:
+ if not hasattr(netatmo_device.device, "brightness"):
+ return
+
+ entity = NetatmoLight(netatmo_device)
+ _LOGGER.debug("Adding light %s", entity)
+ async_add_entities([entity])
+
+ entry.async_on_unload(
+ async_dispatcher_connect(hass, NETATMO_CREATE_LIGHT, _create_entity)
+ )
-class NetatmoLight(NetatmoBase, LightEntity):
+class NetatmoCameraLight(NetatmoBase, LightEntity):
"""Representation of a Netatmo Presence camera light."""
- _attr_color_mode = ColorMode.ONOFF
_attr_has_entity_name = True
- _attr_supported_color_modes = {ColorMode.ONOFF}
def __init__(
self,
- data_handler: NetatmoDataHandler,
- camera_id: str,
- camera_type: str,
- home_id: str,
+ netatmo_device: NetatmoDevice,
) -> None:
"""Initialize a Netatmo Presence camera light."""
LightEntity.__init__(self)
- super().__init__(data_handler)
+ super().__init__(netatmo_device.data_handler)
- self._publishers.append(
- {"name": CAMERA_DATA_CLASS_NAME, SIGNAL_NAME: CAMERA_DATA_CLASS_NAME}
- )
- self._id = camera_id
- self._home_id = home_id
- self._model = camera_type
- self._netatmo_type = TYPE_SECURITY
- self._device_name: str = self._data.get_camera(camera_id)["name"]
+ self._camera = cast(NaModules.NOC, netatmo_device.device)
+ self._id = self._camera.entity_id
+ self._home_id = self._camera.home.entity_id
+ self._device_name = self._camera.name
+ self._model = self._camera.device_type
+ self._config_url = CONF_URL_SECURITY
self._is_on = False
self._attr_unique_id = f"{self._id}-light"
+ self._signal_name = f"{HOME}-{self._home_id}"
+ self._publishers.extend(
+ [
+ {
+ "name": HOME,
+ "home_id": self._camera.home.entity_id,
+ SIGNAL_NAME: self._signal_name,
+ },
+ ]
+ )
+
async def async_added_to_hass(self) -> None:
"""Entity created."""
await super().async_added_to_hass()
- self.data_handler.config_entry.async_on_unload(
+ self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"signal-{DOMAIN}-webhook-{EVENT_TYPE_LIGHT_MODE}",
@@ -117,14 +124,6 @@ class NetatmoLight(NetatmoBase, LightEntity):
self.async_write_ha_state()
return
- @property
- def _data(self) -> pyatmo.AsyncCameraData:
- """Return data for this entity."""
- return cast(
- pyatmo.AsyncCameraData,
- self.data_handler.data[self._publishers[0]["name"]],
- )
-
@property
def available(self) -> bool:
"""If the webhook is not established, mark as unavailable."""
@@ -138,22 +137,79 @@ class NetatmoLight(NetatmoBase, LightEntity):
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn camera floodlight on."""
_LOGGER.debug("Turn camera '%s' on", self.name)
- await self._data.async_set_state(
- home_id=self._home_id,
- camera_id=self._id,
- floodlight="on",
- )
+ await self._camera.async_floodlight_on()
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn camera floodlight into auto mode."""
_LOGGER.debug("Turn camera '%s' to auto mode", self.name)
- await self._data.async_set_state(
- home_id=self._home_id,
- camera_id=self._id,
- floodlight="auto",
- )
+ await self._camera.async_floodlight_auto()
@callback
def async_update_callback(self) -> None:
"""Update the entity's state."""
- self._is_on = bool(self._data.get_light_state(self._id) == "on")
+ self._is_on = bool(self._camera.floodlight == "on")
+
+
+class NetatmoLight(NetatmoBase, LightEntity):
+ """Representation of a dimmable light by Legrand/BTicino."""
+
+ def __init__(
+ self,
+ netatmo_device: NetatmoDevice,
+ ) -> None:
+ """Initialize a Netatmo light."""
+ super().__init__(netatmo_device.data_handler)
+
+ self._dimmer = cast(NaModules.NLFN, netatmo_device.device)
+ self._id = self._dimmer.entity_id
+ self._home_id = self._dimmer.home.entity_id
+ self._device_name = self._dimmer.name
+ self._attr_name = f"{self._device_name}"
+ self._model = self._dimmer.device_type
+ self._config_url = CONF_URL_CONTROL
+ self._attr_brightness = 0
+ self._attr_unique_id = f"{self._id}-light"
+
+ self._attr_supported_color_modes: set[str] = set()
+
+ if not self._attr_supported_color_modes and self._dimmer.brightness is not None:
+ self._attr_supported_color_modes.add(ColorMode.BRIGHTNESS)
+
+ self._signal_name = f"{HOME}-{self._home_id}"
+ self._publishers.extend(
+ [
+ {
+ "name": HOME,
+ "home_id": self._dimmer.home.entity_id,
+ SIGNAL_NAME: self._signal_name,
+ },
+ ]
+ )
+
+ @property
+ def is_on(self) -> bool:
+ """Return true if light is on."""
+ return self._dimmer.on is True
+
+ async def async_turn_on(self, **kwargs: Any) -> None:
+ """Turn light on."""
+ _LOGGER.debug("Turn light '%s' on", self.name)
+ if ATTR_BRIGHTNESS in kwargs:
+ await self._dimmer.async_set_brightness(kwargs[ATTR_BRIGHTNESS])
+
+ else:
+ await self._dimmer.async_on()
+
+ async def async_turn_off(self, **kwargs: Any) -> None:
+ """Turn light off."""
+ _LOGGER.debug("Turn light '%s' off", self.name)
+ await self._dimmer.async_off()
+
+ @callback
+ def async_update_callback(self) -> None:
+ """Update the entity's state."""
+ if self._dimmer.brightness is not None:
+ # Netatmo uses a range of [0, 100] to control brightness
+ self._attr_brightness = round((self._dimmer.brightness / 100) * 255)
+ else:
+ self._attr_brightness = None
diff --git a/homeassistant/components/netatmo/manifest.json b/homeassistant/components/netatmo/manifest.json
index 2081f9bd274..b198c43bb39 100644
--- a/homeassistant/components/netatmo/manifest.json
+++ b/homeassistant/components/netatmo/manifest.json
@@ -2,7 +2,7 @@
"domain": "netatmo",
"name": "Netatmo",
"documentation": "https://www.home-assistant.io/integrations/netatmo",
- "requirements": ["pyatmo==6.2.4"],
+ "requirements": ["pyatmo==7.0.1"],
"after_dependencies": ["cloud", "media_source"],
"dependencies": ["application_credentials", "webhook"],
"codeowners": ["@cgtobi"],
diff --git a/homeassistant/components/netatmo/media_source.py b/homeassistant/components/netatmo/media_source.py
index 948d162a613..58bf2f93c96 100644
--- a/homeassistant/components/netatmo/media_source.py
+++ b/homeassistant/components/netatmo/media_source.py
@@ -72,21 +72,13 @@ class NetatmoSource(MediaSource):
self, source: str, camera_id: str, event_id: int | None = None
) -> BrowseMediaSource:
if event_id and event_id in self.events[camera_id]:
- created = dt.datetime.fromtimestamp(event_id)
- if self.events[camera_id][event_id]["type"] == "outdoor":
- thumbnail = (
- self.events[camera_id][event_id]["event_list"][0]
- .get("snapshot", {})
- .get("url")
- )
- message = remove_html_tags(
- self.events[camera_id][event_id]["event_list"][0]["message"]
- )
- else:
- thumbnail = (
- self.events[camera_id][event_id].get("snapshot", {}).get("url")
- )
- message = remove_html_tags(self.events[camera_id][event_id]["message"])
+ created = dt.datetime.fromtimestamp(
+ self.events[camera_id][event_id]["event_time"]
+ )
+ thumbnail = self.events[camera_id][event_id].get("snapshot", {}).get("url")
+ message = remove_html_tags(
+ self.events[camera_id][event_id].get("message", "")
+ )
title = f"{created} - {message}"
else:
title = self.hass.data[DOMAIN][DATA_CAMERAS].get(camera_id, MANUFACTURER)
diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py
index e8a346ccd84..081d06f5d4f 100644
--- a/homeassistant/components/netatmo/netatmo_entity_base.py
+++ b/homeassistant/components/netatmo/netatmo_entity_base.py
@@ -3,20 +3,18 @@ from __future__ import annotations
from typing import Any
+from pyatmo.modules.device_types import (
+ DEVICE_DESCRIPTION_MAP,
+ DeviceType as NetatmoDeviceType,
+)
+
from homeassistant.const import ATTR_ATTRIBUTION
from homeassistant.core import callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.entity import DeviceInfo, Entity
-from .const import (
- DATA_DEVICE_IDS,
- DEFAULT_ATTRIBUTION,
- DOMAIN,
- MANUFACTURER,
- MODELS,
- SIGNAL_NAME,
-)
-from .data_handler import PUBLICDATA_DATA_CLASS_NAME, NetatmoDataHandler
+from .const import DATA_DEVICE_IDS, DEFAULT_ATTRIBUTION, DOMAIN, SIGNAL_NAME
+from .data_handler import PUBLIC, NetatmoDataHandler
class NetatmoBase(Entity):
@@ -30,38 +28,38 @@ class NetatmoBase(Entity):
self._device_name: str = ""
self._id: str = ""
self._model: str = ""
- self._netatmo_type: str = ""
+ self._config_url: str = ""
self._attr_name = None
self._attr_unique_id = None
self._attr_extra_state_attributes = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION}
async def async_added_to_hass(self) -> None:
"""Entity created."""
- for data_class in self._publishers:
- signal_name = data_class[SIGNAL_NAME]
+ for publisher in self._publishers:
+ signal_name = publisher[SIGNAL_NAME]
- if "home_id" in data_class:
+ if "home_id" in publisher:
await self.data_handler.subscribe(
- data_class["name"],
+ publisher["name"],
signal_name,
self.async_update_callback,
- home_id=data_class["home_id"],
+ home_id=publisher["home_id"],
)
- elif data_class["name"] == PUBLICDATA_DATA_CLASS_NAME:
+ elif publisher["name"] == PUBLIC:
await self.data_handler.subscribe(
- data_class["name"],
+ publisher["name"],
signal_name,
self.async_update_callback,
- lat_ne=data_class["lat_ne"],
- lon_ne=data_class["lon_ne"],
- lat_sw=data_class["lat_sw"],
- lon_sw=data_class["lon_sw"],
+ lat_ne=publisher["lat_ne"],
+ lon_ne=publisher["lon_ne"],
+ lat_sw=publisher["lat_sw"],
+ lon_sw=publisher["lon_sw"],
)
else:
await self.data_handler.subscribe(
- data_class["name"], signal_name, self.async_update_callback
+ publisher["name"], signal_name, self.async_update_callback
)
for sub in self.data_handler.publisher[signal_name].subscriptions:
@@ -78,9 +76,9 @@ class NetatmoBase(Entity):
"""Run when entity will be removed from hass."""
await super().async_will_remove_from_hass()
- for data_class in self._publishers:
+ for publisher in self._publishers:
await self.data_handler.unsubscribe(
- data_class[SIGNAL_NAME], self.async_update_callback
+ publisher[SIGNAL_NAME], self.async_update_callback
)
@callback
@@ -91,10 +89,13 @@ class NetatmoBase(Entity):
@property
def device_info(self) -> DeviceInfo:
"""Return the device info for the sensor."""
+ manufacturer, model = DEVICE_DESCRIPTION_MAP[
+ getattr(NetatmoDeviceType, self._model)
+ ]
return DeviceInfo(
- configuration_url=f"https://my.netatmo.com/app/{self._netatmo_type}",
+ configuration_url=self._config_url,
identifiers={(DOMAIN, self._id)},
name=self._device_name,
- manufacturer=MANUFACTURER,
- model=MODELS[self._model],
+ manufacturer=manufacturer,
+ model=model,
)
diff --git a/homeassistant/components/netatmo/select.py b/homeassistant/components/netatmo/select.py
index 62e6ef25969..3651ae05e88 100644
--- a/homeassistant/components/netatmo/select.py
+++ b/homeassistant/components/netatmo/select.py
@@ -3,29 +3,20 @@ from __future__ import annotations
import logging
-import pyatmo
-
from homeassistant.components.select import SelectEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
-from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import (
- DATA_HANDLER,
+ CONF_URL_ENERGY,
DATA_SCHEDULES,
DOMAIN,
EVENT_TYPE_SCHEDULE,
- MANUFACTURER,
- SIGNAL_NAME,
- TYPE_ENERGY,
-)
-from .data_handler import (
- CLIMATE_STATE_CLASS_NAME,
- CLIMATE_TOPOLOGY_CLASS_NAME,
- NetatmoDataHandler,
+ NETATMO_CREATE_SELECT,
)
+from .data_handler import HOME, SIGNAL_NAME, NetatmoHome
from .netatmo_entity_base import NetatmoBase
_LOGGER = logging.getLogger(__name__)
@@ -35,100 +26,66 @@ async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up the Netatmo energy platform schedule selector."""
- data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER]
- climate_topology = data_handler.data.get(CLIMATE_TOPOLOGY_CLASS_NAME)
+ @callback
+ def _create_entity(netatmo_home: NetatmoHome) -> None:
+ entity = NetatmoScheduleSelect(netatmo_home)
+ async_add_entities([entity])
- if not climate_topology or climate_topology.raw_data == {}:
- raise PlatformNotReady
-
- entities = []
- for home_id in climate_topology.home_ids:
- signal_name = f"{CLIMATE_STATE_CLASS_NAME}-{home_id}"
-
- await data_handler.subscribe(
- CLIMATE_STATE_CLASS_NAME, signal_name, None, home_id=home_id
- )
-
- if (climate_state := data_handler.data[signal_name]) is None:
- continue
-
- 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,
- [schedule.name for schedule in schedules.values()],
- )
- for home_id, schedules in hass.data[DOMAIN][DATA_SCHEDULES].items()
- if schedules
- ]
-
- _LOGGER.debug("Adding climate schedule select entities %s", entities)
- async_add_entities(entities, True)
+ entry.async_on_unload(
+ async_dispatcher_connect(hass, NETATMO_CREATE_SELECT, _create_entity)
+ )
class NetatmoScheduleSelect(NetatmoBase, SelectEntity):
"""Representation a Netatmo thermostat schedule selector."""
def __init__(
- self, data_handler: NetatmoDataHandler, home_id: str, options: list
+ self,
+ netatmo_home: NetatmoHome,
) -> None:
"""Initialize the select entity."""
SelectEntity.__init__(self)
- super().__init__(data_handler)
+ super().__init__(netatmo_home.data_handler)
- 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._home = netatmo_home.home
+ self._home_id = self._home.entity_id
+ self._signal_name = netatmo_home.signal_name
self._publishers.extend(
[
{
- "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,
+ "name": HOME,
+ "home_id": self._home.entity_id,
+ SIGNAL_NAME: self._signal_name,
},
]
)
self._device_name = self._home.name
- self._attr_name = f"{MANUFACTURER} {self._device_name}"
+ self._attr_name = f"{self._device_name}"
self._model: str = "NATherm1"
- self._netatmo_type = TYPE_ENERGY
+ self._config_url = CONF_URL_ENERGY
self._attr_unique_id = f"{self._home_id}-schedule-select"
self._attr_current_option = getattr(self._home.get_selected_schedule(), "name")
- self._attr_options = options
+ self._attr_options = [
+ schedule.name for schedule in self._home.schedules.values()
+ ]
async def async_added_to_hass(self) -> None:
"""Entity created."""
await super().async_added_to_hass()
- for event_type in (EVENT_TYPE_SCHEDULE,):
- self.data_handler.config_entry.async_on_unload(
- async_dispatcher_connect(
- self.hass,
- f"signal-{DOMAIN}-webhook-{event_type}",
- self.handle_event,
- )
+ self.async_on_remove(
+ async_dispatcher_connect(
+ self.hass,
+ f"signal-{DOMAIN}-webhook-{EVENT_TYPE_SCHEDULE}",
+ self.handle_event,
)
+ )
@callback
def handle_event(self, event: dict) -> None:
@@ -160,7 +117,7 @@ class NetatmoScheduleSelect(NetatmoBase, SelectEntity):
option,
sid,
)
- await self._climate_state.async_switch_home_schedule(schedule_id=sid)
+ await self._home.async_switch_schedule(schedule_id=sid)
break
@callback
@@ -169,8 +126,5 @@ class NetatmoScheduleSelect(NetatmoBase, SelectEntity):
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()
+ schedule.name for schedule in self._home.schedules.values()
]
diff --git a/homeassistant/components/netatmo/sensor.py b/homeassistant/components/netatmo/sensor.py
index bec9af96442..ff555ecd472 100644
--- a/homeassistant/components/netatmo/sensor.py
+++ b/homeassistant/components/netatmo/sensor.py
@@ -1,9 +1,9 @@
-"""Support for the Netatmo Weather Service."""
+"""Support for the Netatmo sensors."""
from __future__ import annotations
from dataclasses import dataclass
import logging
-from typing import NamedTuple, cast
+from typing import cast
import pyatmo
@@ -21,15 +21,15 @@ from homeassistant.const import (
DEGREE,
LENGTH_MILLIMETERS,
PERCENTAGE,
+ POWER_WATT,
PRESSURE_MBAR,
- SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
SOUND_PRESSURE_DB,
SPEED_KILOMETERS_PER_HOUR,
TEMP_CELSIUS,
)
from homeassistant.core import HomeAssistant, callback
-from homeassistant.exceptions import PlatformNotReady
from homeassistant.helpers import device_registry as dr
+from homeassistant.helpers.device_registry import async_entries_for_config_entry
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
@@ -38,20 +38,18 @@ from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import (
+ CONF_URL_ENERGY,
+ CONF_URL_WEATHER,
CONF_WEATHER_AREAS,
DATA_HANDLER,
DOMAIN,
NETATMO_CREATE_BATTERY,
+ NETATMO_CREATE_ROOM_SENSOR,
+ NETATMO_CREATE_SENSOR,
+ NETATMO_CREATE_WEATHER_SENSOR,
SIGNAL_NAME,
- TYPE_WEATHER,
-)
-from .data_handler import (
- HOMECOACH_DATA_CLASS_NAME,
- PUBLICDATA_DATA_CLASS_NAME,
- WEATHERSTATION_DATA_CLASS_NAME,
- NetatmoDataHandler,
- NetatmoDevice,
)
+from .data_handler import HOME, PUBLIC, NetatmoDataHandler, NetatmoDevice, NetatmoRoom
from .helper import NetatmoArea
from .netatmo_entity_base import NetatmoBase
@@ -62,10 +60,12 @@ SUPPORTED_PUBLIC_SENSOR_TYPES: tuple[str, ...] = (
"pressure",
"humidity",
"rain",
- "windstrength",
- "guststrength",
+ "wind_strength",
+ "gust_strength",
"sum_rain_1",
"sum_rain_24",
+ "wind_angle",
+ "gust_angle",
)
@@ -85,7 +85,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = (
NetatmoSensorEntityDescription(
key="temperature",
name="Temperature",
- netatmo_name="Temperature",
+ netatmo_name="temperature",
entity_registry_enabled_default=True,
native_unit_of_measurement=TEMP_CELSIUS,
state_class=SensorStateClass.MEASUREMENT,
@@ -101,7 +101,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = (
NetatmoSensorEntityDescription(
key="co2",
name="CO2",
- netatmo_name="CO2",
+ netatmo_name="co2",
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
entity_registry_enabled_default=True,
state_class=SensorStateClass.MEASUREMENT,
@@ -110,7 +110,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = (
NetatmoSensorEntityDescription(
key="pressure",
name="Pressure",
- netatmo_name="Pressure",
+ netatmo_name="pressure",
entity_registry_enabled_default=True,
native_unit_of_measurement=PRESSURE_MBAR,
state_class=SensorStateClass.MEASUREMENT,
@@ -126,7 +126,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = (
NetatmoSensorEntityDescription(
key="noise",
name="Noise",
- netatmo_name="Noise",
+ netatmo_name="noise",
entity_registry_enabled_default=True,
native_unit_of_measurement=SOUND_PRESSURE_DB,
icon="mdi:volume-high",
@@ -135,7 +135,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = (
NetatmoSensorEntityDescription(
key="humidity",
name="Humidity",
- netatmo_name="Humidity",
+ netatmo_name="humidity",
entity_registry_enabled_default=True,
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
@@ -144,7 +144,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = (
NetatmoSensorEntityDescription(
key="rain",
name="Rain",
- netatmo_name="Rain",
+ netatmo_name="rain",
entity_registry_enabled_default=True,
native_unit_of_measurement=LENGTH_MILLIMETERS,
state_class=SensorStateClass.MEASUREMENT,
@@ -156,7 +156,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = (
netatmo_name="sum_rain_1",
entity_registry_enabled_default=False,
native_unit_of_measurement=LENGTH_MILLIMETERS,
- state_class=SensorStateClass.TOTAL_INCREASING,
+ state_class=SensorStateClass.TOTAL,
icon="mdi:weather-rainy",
),
NetatmoSensorEntityDescription(
@@ -171,7 +171,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = (
NetatmoSensorEntityDescription(
key="battery_percent",
name="Battery Percent",
- netatmo_name="battery_percent",
+ netatmo_name="battery",
entity_registry_enabled_default=True,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=PERCENTAGE,
@@ -181,14 +181,14 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = (
NetatmoSensorEntityDescription(
key="windangle",
name="Direction",
- netatmo_name="WindAngle",
+ netatmo_name="wind_direction",
entity_registry_enabled_default=True,
icon="mdi:compass-outline",
),
NetatmoSensorEntityDescription(
key="windangle_value",
name="Angle",
- netatmo_name="WindAngle",
+ netatmo_name="wind_angle",
entity_registry_enabled_default=False,
native_unit_of_measurement=DEGREE,
icon="mdi:compass-outline",
@@ -197,7 +197,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = (
NetatmoSensorEntityDescription(
key="windstrength",
name="Wind Strength",
- netatmo_name="WindStrength",
+ netatmo_name="wind_strength",
entity_registry_enabled_default=True,
native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR,
icon="mdi:weather-windy",
@@ -206,14 +206,14 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = (
NetatmoSensorEntityDescription(
key="gustangle",
name="Gust Direction",
- netatmo_name="GustAngle",
+ netatmo_name="gust_direction",
entity_registry_enabled_default=False,
icon="mdi:compass-outline",
),
NetatmoSensorEntityDescription(
key="gustangle_value",
name="Gust Angle",
- netatmo_name="GustAngle",
+ netatmo_name="gust_angle",
entity_registry_enabled_default=False,
native_unit_of_measurement=DEGREE,
icon="mdi:compass-outline",
@@ -222,7 +222,7 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = (
NetatmoSensorEntityDescription(
key="guststrength",
name="Gust Strength",
- netatmo_name="GustStrength",
+ netatmo_name="gust_strength",
entity_registry_enabled_default=False,
native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR,
icon="mdi:weather-windy",
@@ -239,39 +239,19 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = (
NetatmoSensorEntityDescription(
key="rf_status",
name="Radio",
- netatmo_name="rf_status",
+ netatmo_name="rf_strength",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
icon="mdi:signal",
),
- NetatmoSensorEntityDescription(
- key="rf_status_lvl",
- name="Radio Level",
- netatmo_name="rf_status",
- entity_registry_enabled_default=False,
- entity_category=EntityCategory.DIAGNOSTIC,
- native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
- state_class=SensorStateClass.MEASUREMENT,
- device_class=SensorDeviceClass.SIGNAL_STRENGTH,
- ),
NetatmoSensorEntityDescription(
key="wifi_status",
name="Wifi",
- netatmo_name="wifi_status",
+ netatmo_name="wifi_strength",
entity_registry_enabled_default=False,
entity_category=EntityCategory.DIAGNOSTIC,
icon="mdi:wifi",
),
- NetatmoSensorEntityDescription(
- key="wifi_status_lvl",
- name="Wifi Level",
- netatmo_name="wifi_status",
- entity_registry_enabled_default=False,
- entity_category=EntityCategory.DIAGNOSTIC,
- native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
- state_class=SensorStateClass.MEASUREMENT,
- device_class=SensorDeviceClass.SIGNAL_STRENGTH,
- ),
NetatmoSensorEntityDescription(
key="health_idx",
name="Health",
@@ -279,136 +259,110 @@ SENSOR_TYPES: tuple[NetatmoSensorEntityDescription, ...] = (
entity_registry_enabled_default=True,
icon="mdi:cloud",
),
+ NetatmoSensorEntityDescription(
+ key="power",
+ name="Power",
+ netatmo_name="power",
+ entity_registry_enabled_default=True,
+ native_unit_of_measurement=POWER_WATT,
+ state_class=SensorStateClass.TOTAL,
+ device_class=SensorDeviceClass.POWER,
+ ),
)
SENSOR_TYPES_KEYS = [desc.key for desc in SENSOR_TYPES]
-MODULE_TYPE_OUTDOOR = "NAModule1"
-MODULE_TYPE_WIND = "NAModule2"
-MODULE_TYPE_RAIN = "NAModule3"
-MODULE_TYPE_INDOOR = "NAModule4"
-
-
-class BatteryData(NamedTuple):
- """Metadata for a batter."""
-
- full: int
- high: int
- medium: int
- low: int
-
-
-BATTERY_VALUES = {
- MODULE_TYPE_WIND: BatteryData(
- full=5590,
- high=5180,
- medium=4770,
- low=4360,
- ),
- MODULE_TYPE_RAIN: BatteryData(
- full=5500,
- high=5000,
- medium=4500,
- low=4000,
- ),
- MODULE_TYPE_INDOOR: BatteryData(
- full=5500,
- high=5280,
- medium=4920,
- low=4560,
- ),
- MODULE_TYPE_OUTDOOR: BatteryData(
- full=5500,
- high=5000,
- medium=4500,
- low=4000,
- ),
-}
-
-PUBLIC = "public"
+BATTERY_SENSOR_DESCRIPTION = NetatmoSensorEntityDescription(
+ key="battery",
+ name="Battery Percent",
+ netatmo_name="battery",
+ entity_category=EntityCategory.DIAGNOSTIC,
+ native_unit_of_measurement=PERCENTAGE,
+ state_class=SensorStateClass.MEASUREMENT,
+ device_class=SensorDeviceClass.BATTERY,
+)
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
- """Set up the Netatmo weather and homecoach platform."""
- data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER]
- platform_not_ready = True
+ """Set up the Netatmo sensor platform."""
- async def find_entities(data_class_name: str) -> list:
- """Find all entities."""
- all_module_infos = {}
- data = data_handler.data
+ @callback
+ def _create_battery_entity(netatmo_device: NetatmoDevice) -> None:
+ if not hasattr(netatmo_device.device, "battery"):
+ return
+ entity = NetatmoClimateBatterySensor(netatmo_device)
+ async_add_entities([entity])
- if data_class_name not in data:
- return []
+ entry.async_on_unload(
+ async_dispatcher_connect(hass, NETATMO_CREATE_BATTERY, _create_battery_entity)
+ )
- if data[data_class_name] is None:
- return []
+ @callback
+ def _create_weather_sensor_entity(netatmo_device: NetatmoDevice) -> None:
+ async_add_entities(
+ NetatmoWeatherSensor(netatmo_device, description)
+ for description in SENSOR_TYPES
+ if description.netatmo_name in netatmo_device.device.features
+ )
- data_class = data[data_class_name]
+ entry.async_on_unload(
+ async_dispatcher_connect(
+ hass, NETATMO_CREATE_WEATHER_SENSOR, _create_weather_sensor_entity
+ )
+ )
- for station_id in data_class.stations:
- for module_id in data_class.get_modules(station_id):
- all_module_infos[module_id] = data_class.get_module(module_id)
-
- all_module_infos[station_id] = data_class.get_station(station_id)
-
- entities = []
- for module in all_module_infos.values():
- if "_id" not in module:
- _LOGGER.debug("Skipping module %s", module.get("module_name"))
- continue
-
- conditions = [
- c.lower()
- for c in data_class.get_monitored_conditions(module_id=module["_id"])
- if c.lower() in SENSOR_TYPES_KEYS
+ @callback
+ def _create_sensor_entity(netatmo_device: NetatmoDevice) -> None:
+ _LOGGER.debug(
+ "Adding %s sensor %s",
+ netatmo_device.device.device_category,
+ netatmo_device.device.name,
+ )
+ async_add_entities(
+ [
+ NetatmoSensor(netatmo_device, description)
+ for description in SENSOR_TYPES
+ if description.key in netatmo_device.device.features
]
- for condition in conditions:
- if f"{condition}_value" in SENSOR_TYPES_KEYS:
- conditions.append(f"{condition}_value")
- elif f"{condition}_lvl" in SENSOR_TYPES_KEYS:
- conditions.append(f"{condition}_lvl")
+ )
- entities.extend(
- [
- NetatmoSensor(data_handler, data_class_name, module, description)
- for description in SENSOR_TYPES
- if description.key in conditions
- ]
- )
+ entry.async_on_unload(
+ async_dispatcher_connect(hass, NETATMO_CREATE_SENSOR, _create_sensor_entity)
+ )
- _LOGGER.debug("Adding weather sensors %s", entities)
- return entities
+ @callback
+ def _create_room_sensor_entity(netatmo_device: NetatmoRoom) -> None:
+ async_add_entities(
+ NetatmoRoomSensor(netatmo_device, description)
+ for description in SENSOR_TYPES
+ if description.key in netatmo_device.room.features
+ )
- for data_class_name in (
- WEATHERSTATION_DATA_CLASS_NAME,
- HOMECOACH_DATA_CLASS_NAME,
- ):
- data_class = data_handler.data.get(data_class_name)
-
- if data_class and data_class.raw_data:
- platform_not_ready = False
-
- async_add_entities(await find_entities(data_class_name), True)
+ entry.async_on_unload(
+ async_dispatcher_connect(
+ hass, NETATMO_CREATE_ROOM_SENSOR, _create_room_sensor_entity
+ )
+ )
device_registry = dr.async_get(hass)
+ data_handler = hass.data[DOMAIN][entry.entry_id][DATA_HANDLER]
async def add_public_entities(update: bool = True) -> None:
"""Retrieve Netatmo public weather entities."""
entities = {
device.name: device.id
- for device in dr.async_entries_for_config_entry(
+ for device in async_entries_for_config_entry(
device_registry, entry.entry_id
)
- if device.model == "Public Weather stations"
+ if device.model == "Public Weather station"
}
new_entities = []
for area in [
NetatmoArea(**i) for i in entry.options.get(CONF_WEATHER_AREAS, {}).values()
]:
- signal_name = f"{PUBLICDATA_DATA_CLASS_NAME}-{area.uuid}"
+ signal_name = f"{PUBLIC}-{area.uuid}"
if area.area_name in entities:
entities.pop(area.area_name)
@@ -422,25 +376,21 @@ async def async_setup_entry(
continue
await data_handler.subscribe(
- PUBLICDATA_DATA_CLASS_NAME,
+ PUBLIC,
signal_name,
None,
lat_ne=area.lat_ne,
lon_ne=area.lon_ne,
lat_sw=area.lat_sw,
lon_sw=area.lon_sw,
+ area_id=str(area.uuid),
)
- data_class = data_handler.data.get(signal_name)
-
- if data_class and data_class.raw_data:
- nonlocal platform_not_ready
- platform_not_ready = False
new_entities.extend(
[
NetatmoPublicSensor(data_handler, area, description)
for description in SENSOR_TYPES
- if description.key in SUPPORTED_PUBLIC_SENSOR_TYPES
+ if description.netatmo_name in SUPPORTED_PUBLIC_SENSOR_TYPES
]
)
@@ -454,68 +404,56 @@ async def async_setup_entry(
hass, f"signal-{DOMAIN}-public-update-{entry.entry_id}", add_public_entities
)
- @callback
- def _create_entity(netatmo_device: NetatmoDevice) -> None:
- entity = NetatmoClimateBatterySensor(netatmo_device)
- _LOGGER.debug("Adding climate battery sensor %s", entity)
- async_add_entities([entity])
-
- entry.async_on_unload(
- async_dispatcher_connect(hass, NETATMO_CREATE_BATTERY, _create_entity)
- )
-
await add_public_entities(False)
- if platform_not_ready:
- raise PlatformNotReady
+class NetatmoWeatherSensor(NetatmoBase, SensorEntity):
+ """Implementation of a Netatmo weather/home coach sensor."""
-class NetatmoSensor(NetatmoBase, SensorEntity):
- """Implementation of a Netatmo sensor."""
-
+ _attr_has_entity_name = True
entity_description: NetatmoSensorEntityDescription
def __init__(
self,
- data_handler: NetatmoDataHandler,
- data_class_name: str,
- module_info: dict,
+ netatmo_device: NetatmoDevice,
description: NetatmoSensorEntityDescription,
) -> None:
"""Initialize the sensor."""
- super().__init__(data_handler)
+ super().__init__(netatmo_device.data_handler)
self.entity_description = description
- self._publishers.append({"name": data_class_name, SIGNAL_NAME: data_class_name})
+ self._module = netatmo_device.device
+ self._id = self._module.entity_id
+ self._station_id = (
+ self._module.bridge if self._module.bridge is not None else self._id
+ )
+ self._device_name = self._module.name
+ category = getattr(self._module.device_category, "name")
+ self._publishers.extend(
+ [
+ {
+ "name": category,
+ SIGNAL_NAME: category,
+ },
+ ]
+ )
- self._id = module_info["_id"]
- self._station_id = module_info.get("main_device", self._id)
-
- station = self._data.get_station(self._station_id)
- if not (device := self._data.get_module(self._id)):
- # Assume it's a station if module can't be found
- device = station
-
- if device["type"] in ("NHC", "NAMain"):
- self._device_name = module_info["station_name"]
- else:
- self._device_name = (
- f"{station['station_name']} "
- f"{module_info.get('module_name', device['type'])}"
- )
-
- self._attr_name = f"{self._device_name} {description.name}"
- self._model = device["type"]
- self._netatmo_type = TYPE_WEATHER
+ self._attr_name = f"{description.name}"
+ self._model = self._module.device_type
+ self._config_url = CONF_URL_WEATHER
self._attr_unique_id = f"{self._id}-{description.key}"
- @property
- def _data(self) -> pyatmo.AsyncWeatherStationData:
- """Return data for this entity."""
- return cast(
- pyatmo.AsyncWeatherStationData,
- self.data_handler.data[self._publishers[0]["name"]],
- )
+ if hasattr(self._module, "place"):
+ place = cast(
+ pyatmo.modules.base_class.Place, getattr(self._module, "place")
+ )
+ if hasattr(place, "location") and place.location is not None:
+ self._attr_extra_state_attributes.update(
+ {
+ ATTR_LATITUDE: place.location.latitude,
+ ATTR_LONGITUDE: place.location.longitude,
+ }
+ )
@property
def available(self) -> bool:
@@ -525,46 +463,25 @@ class NetatmoSensor(NetatmoBase, SensorEntity):
@callback
def async_update_callback(self) -> None:
"""Update the entity's state."""
- data = self._data.get_last_data(station_id=self._station_id, exclude=3600).get(
- self._id
- )
-
- if data is None:
- if self.state:
- _LOGGER.debug(
- "No data found for %s - %s (%s)",
- self.name,
- self._device_name,
- self._id,
- )
- self._attr_native_value = None
+ if (
+ state := getattr(self._module, self.entity_description.netatmo_name)
+ ) is None:
return
- try:
- state = data[self.entity_description.netatmo_name]
- if self.entity_description.key in {"temperature", "pressure", "sum_rain_1"}:
- self._attr_native_value = round(state, 1)
- elif self.entity_description.key in {"windangle_value", "gustangle_value"}:
- self._attr_native_value = fix_angle(state)
- elif self.entity_description.key in {"windangle", "gustangle"}:
- self._attr_native_value = process_angle(fix_angle(state))
- elif self.entity_description.key == "rf_status":
- self._attr_native_value = process_rf(state)
- elif self.entity_description.key == "wifi_status":
- self._attr_native_value = process_wifi(state)
- elif self.entity_description.key == "health_idx":
- self._attr_native_value = process_health(state)
- else:
- self._attr_native_value = state
- except KeyError:
- if self.state:
- _LOGGER.debug(
- "No %s data found for %s",
- self.entity_description.key,
- self._device_name,
- )
- self._attr_native_value = None
- return
+ if self.entity_description.netatmo_name in {
+ "temperature",
+ "pressure",
+ "sum_rain_1",
+ }:
+ self._attr_native_value = round(state, 1)
+ elif self.entity_description.netatmo_name == "rf_strength":
+ self._attr_native_value = process_rf(state)
+ elif self.entity_description.netatmo_name == "wifi_strength":
+ self._attr_native_value = process_wifi(state)
+ elif self.entity_description.netatmo_name == "health_idx":
+ self._attr_native_value = process_health(state)
+ else:
+ self._attr_native_value = state
self.async_write_ha_state()
@@ -580,24 +497,25 @@ class NetatmoClimateBatterySensor(NetatmoBase, SensorEntity):
) -> None:
"""Initialize the sensor."""
super().__init__(netatmo_device.data_handler)
- self.entity_description = NetatmoSensorEntityDescription(
- key="battery_percent",
- name="Battery Percent",
- netatmo_name="battery_percent",
- entity_registry_enabled_default=True,
- entity_category=EntityCategory.DIAGNOSTIC,
- native_unit_of_measurement=PERCENTAGE,
- state_class=SensorStateClass.MEASUREMENT,
- device_class=SensorDeviceClass.BATTERY,
+ self.entity_description = BATTERY_SENSOR_DESCRIPTION
+
+ self._module = cast(pyatmo.modules.NRV, netatmo_device.device)
+ self._id = netatmo_device.parent_id
+
+ self._publishers.extend(
+ [
+ {
+ "name": HOME,
+ "home_id": netatmo_device.device.home.entity_id,
+ SIGNAL_NAME: netatmo_device.signal_name,
+ },
+ ]
)
- self._module = netatmo_device.device
- self._id = netatmo_device.parent_id
self._attr_name = f"{self._module.name} {self.entity_description.name}"
-
- self._signal_name = netatmo_device.signal_name
self._room_id = self._module.room_id
self._model = getattr(self._module.device_type, "value")
+ self._config_url = CONF_URL_ENERGY
self._attr_unique_id = (
f"{self._id}-{self._module.entity_id}-{self.entity_description.key}"
@@ -613,70 +531,54 @@ class NetatmoClimateBatterySensor(NetatmoBase, SensorEntity):
return
self._attr_available = True
- self._attr_native_value = self._process_battery_state()
-
- def _process_battery_state(self) -> int | None:
- """Construct room status."""
- if battery_state := self._module.battery_state:
- return process_battery_percentage(battery_state)
-
- return None
+ self._attr_native_value = self._module.battery
-def process_battery_percentage(data: str) -> int:
- """Process battery data and return percent (int) for display."""
- mapping = {
- "max": 100,
- "full": 90,
- "high": 75,
- "medium": 50,
- "low": 25,
- "very low": 10,
- }
- return mapping[data]
+class NetatmoSensor(NetatmoBase, SensorEntity):
+ """Implementation of a Netatmo sensor."""
+ entity_description: NetatmoSensorEntityDescription
-def fix_angle(angle: int) -> int:
- """Fix angle when value is negative."""
- if angle < 0:
- return 360 + angle
- return angle
+ def __init__(
+ self,
+ netatmo_device: NetatmoDevice,
+ description: NetatmoSensorEntityDescription,
+ ) -> None:
+ """Initialize the sensor."""
+ super().__init__(netatmo_device.data_handler)
+ self.entity_description = description
+ self._module = netatmo_device.device
+ self._id = self._module.entity_id
-def process_angle(angle: int) -> str:
- """Process angle and return string for display."""
- if angle >= 330:
- return "N"
- if angle >= 300:
- return "NW"
- if angle >= 240:
- return "W"
- if angle >= 210:
- return "SW"
- if angle >= 150:
- return "S"
- if angle >= 120:
- return "SE"
- if angle >= 60:
- return "E"
- if angle >= 30:
- return "NE"
- return "N"
+ self._publishers.extend(
+ [
+ {
+ "name": HOME,
+ "home_id": netatmo_device.device.home.entity_id,
+ SIGNAL_NAME: netatmo_device.signal_name,
+ },
+ ]
+ )
+ self._attr_name = f"{self._module.name} {self.entity_description.name}"
+ self._room_id = self._module.room_id
+ self._model = getattr(self._module.device_type, "value")
+ self._config_url = CONF_URL_ENERGY
-def process_battery(data: int, model: str) -> str:
- """Process battery data and return string for display."""
- battery_data = BATTERY_VALUES[model]
+ self._attr_unique_id = (
+ f"{self._id}-{self._module.entity_id}-{self.entity_description.key}"
+ )
- if data >= battery_data.full:
- return "Full"
- if data >= battery_data.high:
- return "High"
- if data >= battery_data.medium:
- return "Medium"
- if data >= battery_data.low:
- return "Low"
- return "Very Low"
+ @callback
+ def async_update_callback(self) -> None:
+ """Update the entity's state."""
+ if (state := getattr(self._module, self.entity_description.key)) is None:
+ return
+
+ self._attr_native_value = state
+
+ self.async_write_ha_state()
def process_health(health: int) -> str:
@@ -714,9 +616,57 @@ def process_wifi(strength: int) -> str:
return "Full"
+class NetatmoRoomSensor(NetatmoBase, SensorEntity):
+ """Implementation of a Netatmo room sensor."""
+
+ entity_description: NetatmoSensorEntityDescription
+
+ def __init__(
+ self,
+ netatmo_room: NetatmoRoom,
+ description: NetatmoSensorEntityDescription,
+ ) -> None:
+ """Initialize the sensor."""
+ super().__init__(netatmo_room.data_handler)
+ self.entity_description = description
+
+ self._room = netatmo_room.room
+ self._id = self._room.entity_id
+
+ self._publishers.extend(
+ [
+ {
+ "name": HOME,
+ "home_id": netatmo_room.room.home.entity_id,
+ SIGNAL_NAME: netatmo_room.signal_name,
+ },
+ ]
+ )
+
+ self._attr_name = f"{self._room.name} {self.entity_description.name}"
+ self._room_id = self._room.entity_id
+ self._model = f"{self._room.climate_type}"
+ self._config_url = CONF_URL_ENERGY
+
+ self._attr_unique_id = (
+ f"{self._id}-{self._room.entity_id}-{self.entity_description.key}"
+ )
+
+ @callback
+ def async_update_callback(self) -> None:
+ """Update the entity's state."""
+ if (state := getattr(self._room, self.entity_description.key)) is None:
+ return
+
+ self._attr_native_value = state
+
+ self.async_write_ha_state()
+
+
class NetatmoPublicSensor(NetatmoBase, SensorEntity):
"""Represent a single sensor in a Netatmo."""
+ _attr_has_entity_name = True
entity_description: NetatmoSensorEntityDescription
def __init__(
@@ -729,11 +679,10 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity):
super().__init__(data_handler)
self.entity_description = description
- self._signal_name = f"{PUBLICDATA_DATA_CLASS_NAME}-{area.uuid}"
-
+ self._signal_name = f"{PUBLIC}-{area.uuid}"
self._publishers.append(
{
- "name": PUBLICDATA_DATA_CLASS_NAME,
+ "name": PUBLIC,
"lat_ne": area.lat_ne,
"lon_ne": area.lon_ne,
"lat_sw": area.lat_sw,
@@ -743,12 +692,14 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity):
}
)
+ self._station = data_handler.account.public_weather_areas[str(area.uuid)]
+
self.area = area
self._mode = area.mode
self._area_name = area.area_name
self._id = self._area_name
self._device_name = f"{self._area_name}"
- self._attr_name = f"{self._device_name} {description.name}"
+ self._attr_name = f"{description.name}"
self._show_on_map = area.show_on_map
self._attr_unique_id = (
f"{self._device_name.replace(' ', '-')}-{description.key}"
@@ -762,17 +713,12 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity):
}
)
- @property
- def _data(self) -> pyatmo.AsyncPublicData:
- """Return data for this entity."""
- return cast(pyatmo.AsyncPublicData, self.data_handler.data[self._signal_name])
-
async def async_added_to_hass(self) -> None:
"""Entity created."""
await super().async_added_to_hass()
assert self.device_info and "name" in self.device_info
- self.data_handler.config_entry.async_on_unload(
+ self.async_on_remove(
async_dispatcher_connect(
self.hass,
f"netatmo-config-{self.device_info['name']}",
@@ -790,22 +736,11 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity):
)
self.area = area
- self._signal_name = f"{PUBLICDATA_DATA_CLASS_NAME}-{area.uuid}"
- self._publishers = [
- {
- "name": PUBLICDATA_DATA_CLASS_NAME,
- "lat_ne": area.lat_ne,
- "lon_ne": area.lon_ne,
- "lat_sw": area.lat_sw,
- "lon_sw": area.lon_sw,
- "area_name": area.area_name,
- SIGNAL_NAME: self._signal_name,
- }
- ]
+ self._signal_name = f"{PUBLIC}-{area.uuid}"
self._mode = area.mode
self._show_on_map = area.show_on_map
await self.data_handler.subscribe(
- PUBLICDATA_DATA_CLASS_NAME,
+ PUBLIC,
self._signal_name,
self.async_update_callback,
lat_ne=area.lat_ne,
@@ -819,22 +754,26 @@ class NetatmoPublicSensor(NetatmoBase, SensorEntity):
"""Update the entity's state."""
data = None
- if self.entity_description.key == "temperature":
- data = self._data.get_latest_temperatures()
- elif self.entity_description.key == "pressure":
- data = self._data.get_latest_pressures()
- elif self.entity_description.key == "humidity":
- data = self._data.get_latest_humidities()
- elif self.entity_description.key == "rain":
- data = self._data.get_latest_rain()
- elif self.entity_description.key == "sum_rain_1":
- data = self._data.get_60_min_rain()
- elif self.entity_description.key == "sum_rain_24":
- data = self._data.get_24_h_rain()
- elif self.entity_description.key == "windstrength":
- data = self._data.get_latest_wind_strengths()
- elif self.entity_description.key == "guststrength":
- data = self._data.get_latest_gust_strengths()
+ if self.entity_description.netatmo_name == "temperature":
+ data = self._station.get_latest_temperatures()
+ elif self.entity_description.netatmo_name == "pressure":
+ data = self._station.get_latest_pressures()
+ elif self.entity_description.netatmo_name == "humidity":
+ data = self._station.get_latest_humidities()
+ elif self.entity_description.netatmo_name == "rain":
+ data = self._station.get_latest_rain()
+ elif self.entity_description.netatmo_name == "sum_rain_1":
+ data = self._station.get_60_min_rain()
+ elif self.entity_description.netatmo_name == "sum_rain_24":
+ data = self._station.get_24_h_rain()
+ elif self.entity_description.netatmo_name == "wind_strength":
+ data = self._station.get_latest_wind_strengths()
+ elif self.entity_description.netatmo_name == "gust_strength":
+ data = self._station.get_latest_gust_strengths()
+ elif self.entity_description.netatmo_name == "wind_angle":
+ data = self._station.get_latest_wind_angles()
+ elif self.entity_description.netatmo_name == "gust_angle":
+ data = self._station.get_latest_gust_angles()
if not data:
if self.available:
diff --git a/homeassistant/components/netatmo/switch.py b/homeassistant/components/netatmo/switch.py
new file mode 100644
index 00000000000..338d073c205
--- /dev/null
+++ b/homeassistant/components/netatmo/switch.py
@@ -0,0 +1,83 @@
+"""Support for Netatmo/BTicino/Legrande switches."""
+from __future__ import annotations
+
+import logging
+from typing import Any, cast
+
+from pyatmo import modules as NaModules
+
+from homeassistant.components.switch import SwitchEntity
+from homeassistant.config_entries import ConfigEntry
+from homeassistant.core import HomeAssistant, callback
+from homeassistant.helpers.dispatcher import async_dispatcher_connect
+from homeassistant.helpers.entity_platform import AddEntitiesCallback
+
+from .const import CONF_URL_CONTROL, NETATMO_CREATE_SWITCH
+from .data_handler import HOME, SIGNAL_NAME, NetatmoDevice
+from .netatmo_entity_base import NetatmoBase
+
+_LOGGER = logging.getLogger(__name__)
+
+
+async def async_setup_entry(
+ hass: HomeAssistant,
+ entry: ConfigEntry,
+ async_add_entities: AddEntitiesCallback,
+) -> None:
+ """Set up the Netatmo switch platform."""
+
+ @callback
+ def _create_entity(netatmo_device: NetatmoDevice) -> None:
+ entity = NetatmoSwitch(netatmo_device)
+ _LOGGER.debug("Adding switch %s", entity)
+ async_add_entities([entity])
+
+ entry.async_on_unload(
+ async_dispatcher_connect(hass, NETATMO_CREATE_SWITCH, _create_entity)
+ )
+
+
+class NetatmoSwitch(NetatmoBase, SwitchEntity):
+ """Representation of a Netatmo switch device."""
+
+ def __init__(
+ self,
+ netatmo_device: NetatmoDevice,
+ ) -> None:
+ """Initialize the Netatmo device."""
+ super().__init__(netatmo_device.data_handler)
+
+ self._switch = cast(NaModules.Switch, netatmo_device.device)
+
+ self._id = self._switch.entity_id
+ self._attr_name = self._device_name = self._switch.name
+ self._model = self._switch.device_type
+ self._config_url = CONF_URL_CONTROL
+
+ self._home_id = self._switch.home.entity_id
+
+ self._signal_name = f"{HOME}-{self._home_id}"
+ self._publishers.extend(
+ [
+ {
+ "name": HOME,
+ "home_id": self._home_id,
+ SIGNAL_NAME: self._signal_name,
+ },
+ ]
+ )
+ self._attr_unique_id = f"{self._id}-{self._model}"
+ self._attr_is_on = self._switch.on
+
+ @callback
+ def async_update_callback(self) -> None:
+ """Update the entity's state."""
+ self._attr_is_on = self._switch.on
+
+ async def async_turn_on(self, **kwargs: Any) -> None:
+ """Turn the zone on."""
+ await self._switch.async_on()
+
+ async def async_turn_off(self, **kwargs: Any) -> None:
+ """Turn the zone off."""
+ await self._switch.async_off()
diff --git a/requirements_all.txt b/requirements_all.txt
index 8805209e6f4..2f811e4d286 100644
--- a/requirements_all.txt
+++ b/requirements_all.txt
@@ -1436,7 +1436,7 @@ pyalmond==0.0.2
pyatag==0.3.5.3
# homeassistant.components.netatmo
-pyatmo==6.2.4
+pyatmo==7.0.1
# homeassistant.components.atome
pyatome==0.1.1
diff --git a/requirements_test_all.txt b/requirements_test_all.txt
index 1ce12de4a86..6a2182faf87 100644
--- a/requirements_test_all.txt
+++ b/requirements_test_all.txt
@@ -1018,7 +1018,7 @@ pyalmond==0.0.2
pyatag==0.3.5.3
# homeassistant.components.netatmo
-pyatmo==6.2.4
+pyatmo==7.0.1
# homeassistant.components.apple_tv
pyatv==0.10.3
diff --git a/tests/components/netatmo/common.py b/tests/components/netatmo/common.py
index 784b428e8d0..375dce4e723 100644
--- a/tests/components/netatmo/common.py
+++ b/tests/components/netatmo/common.py
@@ -11,19 +11,6 @@ from tests.test_util.aiohttp import AiohttpClientMockResponse
CLIENT_ID = "1234"
CLIENT_SECRET = "5678"
-ALL_SCOPES = [
- "read_station",
- "read_camera",
- "access_camera",
- "write_camera",
- "read_presence",
- "access_presence",
- "write_presence",
- "read_homecoach",
- "read_smokedetector",
- "read_thermostat",
- "write_thermostat",
-]
COMMON_RESPONSE = {
"user_id": "91763b24c43d3e344f424e8d",
@@ -43,10 +30,10 @@ DEFAULT_PLATFORMS = ["camera", "climate", "light", "sensor"]
async def fake_post_request(*args, **kwargs):
"""Return fake data."""
- if "url" not in kwargs:
+ if "endpoint" not in kwargs:
return "{}"
- endpoint = kwargs["url"].split("/")[-1]
+ endpoint = kwargs["endpoint"].split("/")[-1]
if endpoint in "snapshot_720.jpg":
return b"test stream image bytes"
@@ -59,7 +46,7 @@ async def fake_post_request(*args, **kwargs):
"setthermmode",
"switchhomeschedule",
]:
- payload = f'{{"{endpoint}": true}}'
+ payload = {f"{endpoint}": True, "status": "ok"}
elif endpoint == "homestatus":
home_id = kwargs.get("params", {}).get("home_id")
@@ -70,17 +57,17 @@ async def fake_post_request(*args, **kwargs):
return AiohttpClientMockResponse(
method="POST",
- url=kwargs["url"],
+ url=kwargs["endpoint"],
json=payload,
)
async def fake_get_image(*args, **kwargs):
"""Return fake data."""
- if "url" not in kwargs:
+ if "endpoint" not in kwargs:
return "{}"
- endpoint = kwargs["url"].split("/")[-1]
+ endpoint = kwargs["endpoint"].split("/")[-1]
if endpoint in "snapshot_720.jpg":
return b"test stream image bytes"
diff --git a/tests/components/netatmo/conftest.py b/tests/components/netatmo/conftest.py
index 808e477e053..a10030fab08 100644
--- a/tests/components/netatmo/conftest.py
+++ b/tests/components/netatmo/conftest.py
@@ -2,9 +2,10 @@
from time import time
from unittest.mock import AsyncMock, patch
+from pyatmo.const import ALL_SCOPES
import pytest
-from .common import ALL_SCOPES, fake_get_image, fake_post_request
+from .common import fake_get_image, fake_post_request
from tests.common import MockConfigEntry
@@ -60,6 +61,7 @@ def netatmo_auth():
"homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth"
) as mock_auth:
mock_auth.return_value.async_post_request.side_effect = fake_post_request
+ mock_auth.return_value.async_post_api_request.side_effect = fake_post_request
mock_auth.return_value.async_get_image.side_effect = fake_get_image
mock_auth.return_value.async_addwebhook.side_effect = AsyncMock()
mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock()
diff --git a/tests/components/netatmo/fixtures/events.txt b/tests/components/netatmo/fixtures/events.txt
index f2bc29f782c..112abb7115a 100644
--- a/tests/components/netatmo/fixtures/events.txt
+++ b/tests/components/netatmo/fixtures/events.txt
@@ -1,61 +1,50 @@
{
- "12:34:56:78:90:ab": {
- 1599152672: {
- "id": "12345",
- "type": "person",
- "time": 1599152672,
- "camera_id": "12:34:56:78:90:ab",
- "snapshot": {
- "url": "https://netatmocameraimage",
- },
- "video_id": "98765",
- "video_status": "available",
- "message": "Paulus seen",
- "media_url": "http:///files/high/index.m3u8",
- },
- 1599152673: {
- "id": "12346",
- "type": "person",
- "time": 1599152673,
- "camera_id": "12:34:56:78:90:ab",
- "snapshot": {
- "url": "https://netatmocameraimage",
- },
- "message": "Tobias seen",
- },
- 1599152674: {
- "id": "12347",
- "type": "outdoor",
- "time": 1599152674,
- "camera_id": "12:34:56:78:90:ac",
- "snapshot": {
- "url": "https://netatmocameraimage",
- },
- "video_id": "98766",
- "video_status": "available",
- "event_list": [
- {
- "type": "vehicle",
- "time": 1599152674,
- "id": "12347-0",
- "offset": 0,
- "message": "Vehicle detected",
- "snapshot": {
- "url": "https://netatmocameraimage",
- },
- },
- {
- "type": "human",
- "time": 1599152674,
- "id": "12347-1",
- "offset": 8,
- "message": "Person detected",
- "snapshot": {
- "url": "https://netatmocameraimage",
- },
- },
- ],
- "media_url": "http:///files/high/index.m3u8",
- },
+ "12:34:56:78:90:ab": {
+ 1654191519: {
+ "home_id": "91763b24c43d3e344f424e8b",
+ "entity_id": "000001",
+ "event_type": "human",
+ "event_time": 1654191519,
+ "module_id": "12:34:56:78:90:ab",
+ "snapshot": {
+ "url": "https://netatmocameraimage"
+ },
+ "vignette": {
+ "url": "https://netatmocameraimage"
+ },
+ "video_id": "0011",
+ "video_status": "available",
+ "message": "Bewegung erkannt",
+ "subevents": [],
+ "media_url": "http:///files/high/index.m3u8"
+ },
+ 1654189491: {
+ "home_id": "91763b24c43d3e344f424e8b",
+ "entity_id": "000002",
+ "event_type": "person",
+ "event_time": 1654189491,
+ "module_id": "12:34:56:78:90:ab",
+ "snapshot": {
+ "url": "https://netatmocameraimage"
+ },
+ "video_id": "0012",
+ "video_status": "available",
+ "message": "Jane gesehen",
+ "person_id": "1111",
+ "out_of_sight": False,
+ "subevents": [],
+ "media_url": "http:///files/high/index.m3u8"
+ },
+ 1654289891: {
+ "home_id": "91763b24c43d3e344f424e8b",
+ "entity_id": "000002",
+ "event_type": "person",
+ "event_time": 1654289891,
+ "module_id": "12:34:56:78:90:ab",
+ "message": "Jane gesehen",
+ "person_id": "1111",
+ "out_of_sight": False,
+ "subevents": []
}
-}
\ No newline at end of file
+ }
+}
diff --git a/tests/components/netatmo/fixtures/getevents.json b/tests/components/netatmo/fixtures/getevents.json
new file mode 100644
index 00000000000..7db4dbe5e9b
--- /dev/null
+++ b/tests/components/netatmo/fixtures/getevents.json
@@ -0,0 +1,151 @@
+{
+ "body": {
+ "home": {
+ "id": "91763b24c43d3e344f424e8b",
+ "events": [
+ {
+ "id": "11111111111111111f7763a6d",
+ "type": "outdoor",
+ "time": 1645794709,
+ "module_id": "12:34:56:00:a5:a4",
+ "video_id": "11111111-2222-3333-4444-b42f0fc4cfad",
+ "video_status": "available",
+ "subevents": [
+ {
+ "id": "11111111-2222-3333-4444-013560107fce",
+ "type": "human",
+ "time": 1645794709,
+ "verified": true,
+ "offset": 0,
+ "snapshot": {
+ "url": "https://netatmocameraimage.blob.core.windows.net/production/000000a722374"
+ },
+ "vignette": {
+ "url": "https://netatmocameraimage.blob.core.windows.net/production/0000009625c0f"
+ },
+ "message": "Person erfasst"
+ },
+ {
+ "id": "11111111-2222-3333-4444-0b0bc962df43",
+ "type": "vehicle",
+ "time": 1645794716,
+ "verified": true,
+ "offset": 15,
+ "snapshot": {
+ "url": "https://netatmocameraimage.blob.core.windows.net/production/00000033f9f96"
+ },
+ "vignette": {
+ "url": "https://netatmocameraimage.blob.core.windows.net/production/000000cba08af"
+ },
+ "message": "Fahrzeug erfasst"
+ },
+ {
+ "id": "11111111-2222-3333-4444-129e72195968",
+ "type": "human",
+ "time": 1645794716,
+ "verified": true,
+ "offset": 15,
+ "snapshot": {
+ "filename": "vod/11111/events/22222/snapshot_129e72195968.jpg"
+ },
+ "vignette": {
+ "filename": "vod/11111/events/22222/vignette_129e72195968.jpg"
+ },
+ "message": "Person erfasst"
+ },
+ {
+ "id": "11111111-2222-3333-4444-dae4d7e4f24e",
+ "type": "human",
+ "time": 1645794718,
+ "verified": true,
+ "offset": 17,
+ "snapshot": {
+ "filename": "vod/11111/events/22222/snapshot_dae4d7e4f24e.jpg"
+ },
+ "vignette": {
+ "filename": "vod/11111/events/22222/vignette_dae4d7e4f24e.jpg"
+ },
+ "message": "Person erfasst"
+ }
+ ]
+ },
+ {
+ "id": "1111111111111111e7e40c353",
+ "type": "connection",
+ "time": 1645784799,
+ "module_id": "12:34:56:00:a5:a4",
+ "message": "Front verbunden"
+ },
+ {
+ "id": "11111111111111144e3115860",
+ "type": "boot",
+ "time": 1645784775,
+ "module_id": "12:34:56:00:a5:a4",
+ "message": "Front gestartet"
+ },
+ {
+ "id": "11111111111111169804049ca",
+ "type": "disconnection",
+ "time": 1645773806,
+ "module_id": "12:34:56:00:a5:a4",
+ "message": "Front getrennt"
+ },
+ {
+ "id": "1111111111111117cb8147ffd",
+ "type": "outdoor",
+ "time": 1645712826,
+ "module_id": "12:34:56:00:a5:a4",
+ "video_id": "11111111-2222-3333-4444-5091e1903f8d",
+ "video_status": "available",
+ "subevents": [
+ {
+ "id": "11111111-2222-3333-4444-b7d28e3ccc38",
+ "type": "human",
+ "time": 1645712826,
+ "verified": true,
+ "offset": 0,
+ "snapshot": {
+ "url": "https://netatmocameraimage.blob.core.windows.net/production/000000a0ca642"
+ },
+ "vignette": {
+ "url": "https://netatmocameraimage.blob.core.windows.net/production/00000031b0ed4"
+ },
+ "message": "Person erfasst"
+ }
+ ]
+ },
+ {
+ "id": "1111111111111119df3d2de6",
+ "type": "person_home",
+ "time": 1645902000,
+ "module_id": "12:34:56:00:f1:62",
+ "message": "Home Assistant Cloud definiert John Doe und Jane Doe als \"Zu Hause\""
+ },
+ {
+ "id": "1111111111111112c91b3628",
+ "type": "person",
+ "time": 1645901266,
+ "module_id": "12:34:56:00:f1:62",
+ "snapshot": {
+ "url": "https://netatmocameraimage.blob.core.windows.net/production/0000081d4f42875d9"
+ },
+ "video_id": "11111111-2222-3333-4444-314d161525db",
+ "video_status": "available",
+ "message": "John Doe gesehen",
+ "person_id": "91827374-7e04-5298-83ad-a0cb8372dff1",
+ "out_of_sight": false
+ },
+ {
+ "id": "1111111111111115166b1283",
+ "type": "tag_open",
+ "time": 1645897638,
+ "module_id": "12:34:56:00:86:99",
+ "message": "Window Hall: immer noch offen"
+ }
+ ]
+ }
+ },
+ "status": "ok",
+ "time_exec": 0.24369096755981445,
+ "time_server": 1645897231
+}
diff --git a/tests/components/netatmo/fixtures/homesdata.json b/tests/components/netatmo/fixtures/homesdata.json
index a56ccb236b5..93c04388f4c 100644
--- a/tests/components/netatmo/fixtures/homesdata.json
+++ b/tests/components/netatmo/fixtures/homesdata.json
@@ -19,7 +19,13 @@
"id": "3688132631",
"name": "Hall",
"type": "custom",
- "module_ids": ["12:34:56:00:f1:62"]
+ "module_ids": [
+ "12:34:56:00:f1:62",
+ "12:34:56:10:f1:66",
+ "12:34:56:00:e3:9b",
+ "12:34:56:00:86:99",
+ "0009999992"
+ ]
},
{
"id": "2833524037",
@@ -32,6 +38,44 @@
"name": "Cocina",
"type": "kitchen",
"module_ids": ["12:34:56:03:a0:ac"]
+ },
+ {
+ "id": "2940411588",
+ "name": "Child",
+ "type": "custom",
+ "module_ids": ["12:34:56:26:cc:01"]
+ },
+ {
+ "id": "222452125",
+ "name": "Bureau",
+ "type": "electrical_cabinet",
+ "module_ids": ["12:34:56:20:f5:44", "12:34:56:20:f5:8c"],
+ "modules": ["12:34:56:20:f5:44", "12:34:56:20:f5:8c"],
+ "therm_relay": "12:34:56:20:f5:44",
+ "true_temperature_available": true
+ },
+ {
+ "id": "100007519",
+ "name": "Cabinet",
+ "type": "electrical_cabinet",
+ "module_ids": [
+ "12:34:56:00:16:0e",
+ "12:34:56:00:16:0e#0",
+ "12:34:56:00:16:0e#1",
+ "12:34:56:00:16:0e#2",
+ "12:34:56:00:16:0e#3",
+ "12:34:56:00:16:0e#4",
+ "12:34:56:00:16:0e#5",
+ "12:34:56:00:16:0e#6",
+ "12:34:56:00:16:0e#7",
+ "12:34:56:00:16:0e#8"
+ ]
+ },
+ {
+ "id": "1002003001",
+ "name": "Corridor",
+ "type": "corridor",
+ "module_ids": ["10:20:30:bd:b8:1e"]
}
],
"modules": [
@@ -75,7 +119,334 @@
"type": "NACamera",
"name": "Hall",
"setup_date": 1544828430,
- "room_id": "3688132631"
+ "room_id": "3688132631",
+ "reachable": true,
+ "modules_bridged": ["12:34:56:00:86:99", "12:34:56:00:e3:9b"]
+ },
+ {
+ "id": "12:34:56:00:a5:a4",
+ "type": "NOC",
+ "name": "Garden",
+ "setup_date": 1544828430,
+ "reachable": true
+ },
+ {
+ "id": "12:34:56:20:f5:44",
+ "type": "OTH",
+ "name": "Modulating Relay",
+ "setup_date": 1607443936,
+ "room_id": "222452125",
+ "reachable": true,
+ "modules_bridged": ["12:34:56:20:f5:8c"],
+ "hk_device_id": "12:34:56:20:d0:c5",
+ "capabilities": [
+ {
+ "name": "automatism",
+ "available": true
+ }
+ ],
+ "max_modules_nb": 21
+ },
+ {
+ "id": "12:34:56:20:f5:8c",
+ "type": "OTM",
+ "name": "Bureau Modulate",
+ "setup_date": 1607443939,
+ "room_id": "222452125",
+ "bridge": "12:34:56:20:f5:44"
+ },
+ {
+ "id": "12:34:56:10:f1:66",
+ "type": "NDB",
+ "name": "Netatmo-Doorbell",
+ "setup_date": 1602691361,
+ "room_id": "3688132631",
+ "reachable": true,
+ "hk_device_id": "123456007df1",
+ "customer_id": "1000010",
+ "network_lock": false,
+ "quick_display_zone": 62
+ },
+ {
+ "id": "12:34:56:00:e3:9b",
+ "type": "NIS",
+ "setup_date": 1620479901,
+ "bridge": "12:34:56:00:f1:62",
+ "name": "Sirene in hall"
+ },
+ {
+ "id": "12:34:56:00:86:99",
+ "type": "NACamDoorTag",
+ "name": "Window Hall",
+ "setup_date": 1581177375,
+ "bridge": "12:34:56:00:f1:62",
+ "category": "window"
+ },
+ {
+ "id": "12:34:56:30:d5:d4",
+ "type": "NBG",
+ "name": "module iDiamant",
+ "setup_date": 1562262465,
+ "room_id": "222452125",
+ "modules_bridged": ["0009999992"]
+ },
+ {
+ "id": "0009999992",
+ "type": "NBR",
+ "name": "Entrance Blinds",
+ "setup_date": 1578551339,
+ "room_id": "3688132631",
+ "bridge": "12:34:56:30:d5:d4"
+ },
+ {
+ "id": "12:34:56:37:11:ca",
+ "type": "NAMain",
+ "name": "NetatmoIndoor",
+ "setup_date": 1419453350,
+ "reachable": true,
+ "modules_bridged": [
+ "12:34:56:07:bb:3e",
+ "12:34:56:03:1b:e4",
+ "12:34:56:36:fc:de",
+ "12:34:56:05:51:20"
+ ],
+ "customer_id": "C00016",
+ "hardware_version": 251,
+ "public_ext_data": false,
+ "public_ext_counter": 0,
+ "alarm_config": {
+ "default_alarm": [
+ {
+ "db_alarm_number": 0
+ },
+ {
+ "db_alarm_number": 1
+ },
+ {
+ "db_alarm_number": 2
+ },
+ {
+ "db_alarm_number": 6
+ },
+ {
+ "db_alarm_number": 4
+ },
+ {
+ "db_alarm_number": 5
+ },
+ {
+ "db_alarm_number": 7
+ },
+ {
+ "db_alarm_number": 22
+ }
+ ],
+ "personnalized": [
+ {
+ "threshold": 20,
+ "data_type": 1,
+ "direction": 0,
+ "db_alarm_number": 8
+ },
+ {
+ "threshold": 17,
+ "data_type": 1,
+ "direction": 1,
+ "db_alarm_number": 9
+ },
+ {
+ "threshold": 65,
+ "data_type": 4,
+ "direction": 0,
+ "db_alarm_number": 16
+ },
+ {
+ "threshold": 19,
+ "data_type": 8,
+ "direction": 0,
+ "db_alarm_number": 22
+ }
+ ]
+ },
+ "module_offset": {
+ "12:34:56:80:bb:26": {
+ "a": 0.1
+ }
+ }
+ },
+ {
+ "id": "12:34:56:36:fc:de",
+ "type": "NAModule1",
+ "name": "Outdoor",
+ "setup_date": 1448565785,
+ "bridge": "12:34:56:37:11:ca"
+ },
+ {
+ "id": "12:34:56:03:1b:e4",
+ "type": "NAModule2",
+ "name": "Garden",
+ "setup_date": 1543579864,
+ "bridge": "12:34:56:37:11:ca"
+ },
+ {
+ "id": "12:34:56:05:51:20",
+ "type": "NAModule3",
+ "name": "Rain",
+ "setup_date": 1591770206,
+ "bridge": "12:34:56:37:11:ca"
+ },
+ {
+ "id": "12:34:56:07:bb:3e",
+ "type": "NAModule4",
+ "name": "Bedroom",
+ "setup_date": 1484997703,
+ "bridge": "12:34:56:37:11:ca"
+ },
+ {
+ "id": "12:34:56:26:68:92",
+ "type": "NHC",
+ "name": "Indoor",
+ "setup_date": 1571342643
+ },
+ {
+ "id": "12:34:56:26:cc:01",
+ "type": "BNS",
+ "name": "Child",
+ "setup_date": 1571634243
+ },
+ {
+ "id": "12:34:56:80:60:40",
+ "type": "NLG",
+ "name": "Prise Control",
+ "setup_date": 1641841257,
+ "room_id": "1310352496",
+ "modules_bridged": [
+ "12:34:56:80:00:12:ac:f2",
+ "12:34:56:80:00:c3:69:3c",
+ "12:34:56:00:00:a1:4c:da",
+ "12:34:56:00:01:01:01:a1"
+ ]
+ },
+ {
+ "id": "12:34:56:80:00:12:ac:f2",
+ "type": "NLP",
+ "name": "Prise",
+ "setup_date": 1641841262,
+ "room_id": "1310352496",
+ "bridge": "12:34:56:80:60:40"
+ },
+ {
+ "id": "12:34:56:80:00:c3:69:3c",
+ "type": "NLT",
+ "name": "Commande sans fil",
+ "setup_date": 1641841262,
+ "bridge": "12:34:56:80:60:40"
+ },
+ {
+ "id": "12:34:56:00:16:0e",
+ "type": "NLE",
+ "name": "Écocompteur",
+ "setup_date": 1644496884,
+ "room_id": "100007519",
+ "modules_bridged": [
+ "12:34:56:00:16:0e#0",
+ "12:34:56:00:16:0e#1",
+ "12:34:56:00:16:0e#2",
+ "12:34:56:00:16:0e#3",
+ "12:34:56:00:16:0e#4",
+ "12:34:56:00:16:0e#5",
+ "12:34:56:00:16:0e#6",
+ "12:34:56:00:16:0e#7",
+ "12:34:56:00:16:0e#8"
+ ]
+ },
+ {
+ "id": "12:34:56:00:16:0e#0",
+ "type": "NLE",
+ "name": "Line 1",
+ "setup_date": 1644496886,
+ "room_id": "100007519",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#1",
+ "type": "NLE",
+ "name": "Line 2",
+ "setup_date": 1644496886,
+ "room_id": "100007519",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#2",
+ "type": "NLE",
+ "name": "Line 3",
+ "setup_date": 1644496886,
+ "room_id": "100007519",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#3",
+ "type": "NLE",
+ "name": "Line 4",
+ "setup_date": 1644496886,
+ "room_id": "100007519",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#4",
+ "type": "NLE",
+ "name": "Line 5",
+ "setup_date": 1644496886,
+ "room_id": "100007519",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#5",
+ "type": "NLE",
+ "name": "Total",
+ "setup_date": 1644496886,
+ "room_id": "100007519",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#6",
+ "type": "NLE",
+ "name": "Gas",
+ "setup_date": 1644496886,
+ "room_id": "100007519",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#7",
+ "type": "NLE",
+ "name": "Hot water",
+ "setup_date": 1644496886,
+ "room_id": "100007519",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#8",
+ "type": "NLE",
+ "name": "Cold water",
+ "setup_date": 1644496886,
+ "room_id": "100007519",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:00:a1:4c:da",
+ "type": "NLPC",
+ "name": "Consumption meter",
+ "setup_date": 1638376602,
+ "room_id": "100008999",
+ "bridge": "12:34:56:80:60:40"
+ },
+ {
+ "id": "12:34:56:00:01:01:01:a1",
+ "type": "NLFN",
+ "name": "Bathroom light",
+ "setup_date": 1598367404,
+ "room_id": "1002003001",
+ "bridge": "12:34:56:80:60:40"
}
],
"schedules": [
diff --git a/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8b.json b/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8b.json
index 12c7044aaaa..4cd5dceec3b 100644
--- a/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8b.json
+++ b/tests/components/netatmo/fixtures/homestatus_91763b24c43d3e344f424e8b.json
@@ -14,6 +14,25 @@
"vpn_url": "https://prodvpn-eu-2.netatmo.net/restricted/10.255.123.45/609e27de5699fb18147ab47d06846631/MTRPn_BeWCav5RBq4U1OMDruTW4dkQ0NuMwNDAw11g,,",
"is_local": true
},
+ {
+ "type": "NOC",
+ "firmware_revision": 3002000,
+ "monitoring": "on",
+ "sd_status": 4,
+ "connection": "wifi",
+ "homekit_status": "upgradable",
+ "floodlight": "auto",
+ "timelapse_available": true,
+ "id": "12:34:56:00:a5:a4",
+ "vpn_url": "https://prodvpn-eu-6.netatmo.net/10.20.30.41/333333333333/444444444444,,",
+ "is_local": false,
+ "network_lock": false,
+ "firmware_name": "3.2.0",
+ "wifi_strength": 62,
+ "alim_status": 2,
+ "locked": false,
+ "wifi_state": "high"
+ },
{
"id": "12:34:56:00:fa:d0",
"type": "NAPlug",
@@ -50,6 +69,805 @@
"rf_strength": 59,
"bridge": "12:34:56:00:fa:d0",
"battery_state": "full"
+ },
+ {
+ "id": "12:34:56:26:cc:01",
+ "type": "BNS",
+ "firmware_revision": 32,
+ "wifi_strength": 50,
+ "boiler_valve_comfort_boost": false,
+ "boiler_status": true,
+ "cooler_status": false
+ },
+ {
+ "type": "NDB",
+ "last_ftp_event": {
+ "type": 3,
+ "time": 1631444443,
+ "id": 3
+ },
+ "id": "12:34:56:10:f1:66",
+ "websocket_connected": true,
+ "vpn_url": "https://prodvpn-eu-6.netatmo.net/10.20.30.40/1111111111111/2222222222222,,",
+ "is_local": false,
+ "alim_status": 2,
+ "connection": "wifi",
+ "firmware_name": "2.18.0",
+ "firmware_revision": 2018000,
+ "homekit_status": "configured",
+ "max_peers_reached": false,
+ "sd_status": 4,
+ "wifi_strength": 66,
+ "wifi_state": "medium"
+ },
+ {
+ "boiler_control": "onoff",
+ "dhw_control": "none",
+ "firmware_revision": 22,
+ "hardware_version": 222,
+ "id": "12:34:56:20:f5:44",
+ "outdoor_temperature": 8.2,
+ "sequence_id": 19764,
+ "type": "OTH",
+ "wifi_strength": 57
+ },
+ {
+ "battery_level": 4176,
+ "boiler_status": false,
+ "boiler_valve_comfort_boost": false,
+ "firmware_revision": 6,
+ "id": "12:34:56:20:f5:8c",
+ "last_message": 1637684297,
+ "last_seen": 1637684297,
+ "radio_id": 2,
+ "reachable": true,
+ "rf_strength": 64,
+ "type": "OTM",
+ "bridge": "12:34:56:20:f5:44",
+ "battery_state": "full"
+ },
+ {
+ "id": "12:34:56:30:d5:d4",
+ "type": "NBG",
+ "firmware_revision": 39,
+ "wifi_strength": 65,
+ "reachable": true
+ },
+ {
+ "id": "0009999992",
+ "type": "NBR",
+ "current_position": 0,
+ "target_position": 0,
+ "target_position_step": 100,
+ "firmware_revision": 16,
+ "rf_strength": 0,
+ "last_seen": 1638353156,
+ "reachable": true,
+ "bridge": "12:34:56:30:d5:d4"
+ },
+ {
+ "id": "12:34:56:00:86:99",
+ "type": "NACamDoorTag",
+ "battery_state": "high",
+ "battery_level": 5240,
+ "firmware_revision": 58,
+ "rf_state": "full",
+ "rf_strength": 58,
+ "last_seen": 1642698124,
+ "last_activity": 1627757310,
+ "reachable": false,
+ "bridge": "12:34:56:00:f1:62",
+ "status": "no_news"
+ },
+ {
+ "id": "12:34:56:00:e3:9b",
+ "type": "NIS",
+ "battery_state": "low",
+ "battery_level": 5438,
+ "firmware_revision": 209,
+ "rf_state": "medium",
+ "rf_strength": 62,
+ "last_seen": 1644569790,
+ "reachable": true,
+ "bridge": "12:34:56:00:f1:62",
+ "status": "no_sound",
+ "monitoring": "off"
+ },
+ {
+ "id": "12:34:56:80:60:40",
+ "type": "NLG",
+ "offload": false,
+ "firmware_revision": 211,
+ "last_seen": 1644567372,
+ "wifi_strength": 51,
+ "reachable": true
+ },
+ {
+ "id": "12:34:56:80:00:12:ac:f2",
+ "type": "NLP",
+ "on": true,
+ "offload": false,
+ "firmware_revision": 62,
+ "last_seen": 1644569425,
+ "power": 0,
+ "reachable": true,
+ "bridge": "12:34:56:80:60:40"
+ },
+ {
+ "id": "12:34:56:80:00:c3:69:3c",
+ "type": "NLT",
+ "battery_state": "full",
+ "battery_level": 3300,
+ "firmware_revision": 42,
+ "last_seen": 0,
+ "reachable": false,
+ "bridge": "12:34:56:80:60:40"
+ },
+ {
+ "id": "12:34:56:00:16:0e",
+ "type": "NLE",
+ "firmware_revision": 14,
+ "wifi_strength": 38
+ },
+ {
+ "id": "12:34:56:00:16:0e#0",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#1",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#2",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#3",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#4",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#5",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#6",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#7",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#8",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:00:a1:4c:da",
+ "type": "NLPC",
+ "firmware_revision": 62,
+ "last_seen": 1646511241,
+ "power": 476,
+ "reachable": true,
+ "bridge": "12:34:56:80:60:40"
+ },
+ {
+ "id": "12:34:56:00:01:01:01:a1",
+ "brightness": 100,
+ "firmware_revision": 52,
+ "last_seen": 1604940167,
+ "on": false,
+ "power": 0,
+ "reachable": true,
+ "type": "NLFN",
+ "bridge": "12:34:56:80:60:40"
+ },
+ {
+ "type": "NDB",
+ "last_ftp_event": {
+ "type": 3,
+ "time": 1631444443,
+ "id": 3
+ },
+ "id": "12:34:56:10:f1:66",
+ "websocket_connected": true,
+ "vpn_url": "https://prodvpn-eu-6.netatmo.net/10.20.30.40/1111111111111/2222222222222,,",
+ "is_local": false,
+ "alim_status": 2,
+ "connection": "wifi",
+ "firmware_name": "2.18.0",
+ "firmware_revision": 2018000,
+ "homekit_status": "configured",
+ "max_peers_reached": false,
+ "sd_status": 4,
+ "wifi_strength": 66,
+ "wifi_state": "medium"
+ },
+ {
+ "boiler_control": "onoff",
+ "dhw_control": "none",
+ "firmware_revision": 22,
+ "hardware_version": 222,
+ "id": "12:34:56:20:f5:44",
+ "outdoor_temperature": 8.2,
+ "sequence_id": 19764,
+ "type": "OTH",
+ "wifi_strength": 57
+ },
+ {
+ "battery_level": 4176,
+ "boiler_status": false,
+ "boiler_valve_comfort_boost": false,
+ "firmware_revision": 6,
+ "id": "12:34:56:20:f5:8c",
+ "last_message": 1637684297,
+ "last_seen": 1637684297,
+ "radio_id": 2,
+ "reachable": true,
+ "rf_strength": 64,
+ "type": "OTM",
+ "bridge": "12:34:56:20:f5:44",
+ "battery_state": "full"
+ },
+ {
+ "id": "12:34:56:30:d5:d4",
+ "type": "NBG",
+ "firmware_revision": 39,
+ "wifi_strength": 65,
+ "reachable": true
+ },
+ {
+ "id": "0009999992",
+ "type": "NBR",
+ "current_position": 0,
+ "target_position": 0,
+ "target_position_step": 100,
+ "firmware_revision": 16,
+ "rf_strength": 0,
+ "last_seen": 1638353156,
+ "reachable": true,
+ "therm_measured_temperature": 5,
+ "heating_power_request": 1,
+ "therm_setpoint_temperature": 7,
+ "therm_setpoint_mode": "away",
+ "therm_setpoint_start_time": 0,
+ "therm_setpoint_end_time": 0,
+ "anticipating": false,
+ "open_window": false
+ },
+ {
+ "id": "12:34:56:00:86:99",
+ "type": "NACamDoorTag",
+ "battery_state": "high",
+ "battery_level": 5240,
+ "firmware_revision": 58,
+ "rf_state": "full",
+ "rf_strength": 58,
+ "last_seen": 1642698124,
+ "last_activity": 1627757310,
+ "reachable": false,
+ "bridge": "12:34:56:00:f1:62",
+ "status": "no_news"
+ },
+ {
+ "id": "12:34:56:00:e3:9b",
+ "type": "NIS",
+ "battery_state": "low",
+ "battery_level": 5438,
+ "firmware_revision": 209,
+ "rf_state": "medium",
+ "rf_strength": 62,
+ "last_seen": 1644569790,
+ "reachable": true,
+ "bridge": "12:34:56:00:f1:62",
+ "status": "no_sound",
+ "monitoring": "off"
+ },
+ {
+ "id": "12:34:56:80:60:40",
+ "type": "NLG",
+ "offload": false,
+ "firmware_revision": 211,
+ "last_seen": 1644567372,
+ "wifi_strength": 51,
+ "reachable": true
+ },
+ {
+ "id": "12:34:56:80:00:12:ac:f2",
+ "type": "NLP",
+ "on": true,
+ "offload": false,
+ "firmware_revision": 62,
+ "last_seen": 1644569425,
+ "power": 0,
+ "reachable": true,
+ "bridge": "12:34:56:80:60:40"
+ },
+ {
+ "id": "12:34:56:80:00:c3:69:3c",
+ "type": "NLT",
+ "battery_state": "full",
+ "battery_level": 3300,
+ "firmware_revision": 42,
+ "last_seen": 0,
+ "reachable": false,
+ "bridge": "12:34:56:80:60:40"
+ },
+ {
+ "id": "12:34:56:00:16:0e",
+ "type": "NLE",
+ "firmware_revision": 14,
+ "wifi_strength": 38
+ },
+ {
+ "id": "12:34:56:00:16:0e#0",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#1",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#2",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#3",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#4",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#5",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#6",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#7",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#8",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:00:a1:4c:da",
+ "type": "NLPC",
+ "firmware_revision": 62,
+ "last_seen": 1646511241,
+ "power": 476,
+ "reachable": true,
+ "bridge": "12:34:56:80:60:40"
+ },
+ {
+ "id": "12:34:56:00:01:01:01:a1",
+ "brightness": 100,
+ "firmware_revision": 52,
+ "last_seen": 1604940167,
+ "on": false,
+ "power": 0,
+ "reachable": true,
+ "type": "NLFN",
+ "bridge": "12:34:56:80:60:40"
+ },
+ {
+ "type": "NDB",
+ "last_ftp_event": {
+ "type": 3,
+ "time": 1631444443,
+ "id": 3
+ },
+ "id": "12:34:56:10:f1:66",
+ "websocket_connected": true,
+ "vpn_url": "https://prodvpn-eu-6.netatmo.net/10.20.30.40/1111111111111/2222222222222,,",
+ "is_local": false,
+ "alim_status": 2,
+ "connection": "wifi",
+ "firmware_name": "2.18.0",
+ "firmware_revision": 2018000,
+ "homekit_status": "configured",
+ "max_peers_reached": false,
+ "sd_status": 4,
+ "wifi_strength": 66,
+ "wifi_state": "medium"
+ },
+ {
+ "boiler_control": "onoff",
+ "dhw_control": "none",
+ "firmware_revision": 22,
+ "hardware_version": 222,
+ "id": "12:34:56:20:f5:44",
+ "outdoor_temperature": 8.2,
+ "sequence_id": 19764,
+ "type": "OTH",
+ "wifi_strength": 57
+ },
+ {
+ "battery_level": 4176,
+ "boiler_status": false,
+ "boiler_valve_comfort_boost": false,
+ "firmware_revision": 6,
+ "id": "12:34:56:20:f5:8c",
+ "last_message": 1637684297,
+ "last_seen": 1637684297,
+ "radio_id": 2,
+ "reachable": true,
+ "rf_strength": 64,
+ "type": "OTM",
+ "bridge": "12:34:56:20:f5:44",
+ "battery_state": "full"
+ },
+ {
+ "id": "12:34:56:30:d5:d4",
+ "type": "NBG",
+ "firmware_revision": 39,
+ "wifi_strength": 65,
+ "reachable": true
+ },
+ {
+ "id": "0009999992",
+ "type": "NBR",
+ "current_position": 0,
+ "target_position": 0,
+ "target_position_step": 100,
+ "firmware_revision": 16,
+ "rf_strength": 0,
+ "last_seen": 1638353156,
+ "reachable": true,
+ "therm_measured_temperature": 5,
+ "heating_power_request": 1,
+ "therm_setpoint_temperature": 7,
+ "therm_setpoint_mode": "away",
+ "therm_setpoint_start_time": 0,
+ "therm_setpoint_end_time": 0,
+ "anticipating": false,
+ "open_window": false
+ },
+ {
+ "id": "12:34:56:00:86:99",
+ "type": "NACamDoorTag",
+ "battery_state": "high",
+ "battery_level": 5240,
+ "firmware_revision": 58,
+ "rf_state": "full",
+ "rf_strength": 58,
+ "last_seen": 1642698124,
+ "last_activity": 1627757310,
+ "reachable": false,
+ "bridge": "12:34:56:00:f1:62",
+ "status": "no_news"
+ },
+ {
+ "id": "12:34:56:00:e3:9b",
+ "type": "NIS",
+ "battery_state": "low",
+ "battery_level": 5438,
+ "firmware_revision": 209,
+ "rf_state": "medium",
+ "rf_strength": 62,
+ "last_seen": 1644569790,
+ "reachable": true,
+ "bridge": "12:34:56:00:f1:62",
+ "status": "no_sound",
+ "monitoring": "off"
+ },
+ {
+ "id": "12:34:56:80:60:40",
+ "type": "NLG",
+ "offload": false,
+ "firmware_revision": 211,
+ "last_seen": 1644567372,
+ "wifi_strength": 51,
+ "reachable": true
+ },
+ {
+ "id": "12:34:56:80:00:12:ac:f2",
+ "type": "NLP",
+ "on": true,
+ "offload": false,
+ "firmware_revision": 62,
+ "last_seen": 1644569425,
+ "power": 0,
+ "reachable": true,
+ "bridge": "12:34:56:80:60:40"
+ },
+ {
+ "id": "12:34:56:80:00:c3:69:3c",
+ "type": "NLT",
+ "battery_state": "full",
+ "battery_level": 3300,
+ "firmware_revision": 42,
+ "last_seen": 0,
+ "reachable": false,
+ "bridge": "12:34:56:80:60:40"
+ },
+ {
+ "id": "12:34:56:00:16:0e",
+ "type": "NLE",
+ "firmware_revision": 14,
+ "wifi_strength": 38
+ },
+ {
+ "id": "12:34:56:00:16:0e#0",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#1",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#2",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#3",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#4",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#5",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#6",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#7",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#8",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:00:a1:4c:da",
+ "type": "NLPC",
+ "firmware_revision": 62,
+ "last_seen": 1646511241,
+ "power": 476,
+ "reachable": true,
+ "bridge": "12:34:56:80:60:40"
+ },
+ {
+ "id": "12:34:56:00:01:01:01:a1",
+ "brightness": 100,
+ "firmware_revision": 52,
+ "last_seen": 1604940167,
+ "on": false,
+ "power": 0,
+ "reachable": true,
+ "type": "NLFN",
+ "bridge": "12:34:56:80:60:40"
+ },
+ {
+ "type": "NDB",
+ "last_ftp_event": {
+ "type": 3,
+ "time": 1631444443,
+ "id": 3
+ },
+ "id": "12:34:56:10:f1:66",
+ "websocket_connected": true,
+ "vpn_url": "https://prodvpn-eu-6.netatmo.net/10.20.30.40/1111111111111/2222222222222,,",
+ "is_local": false,
+ "alim_status": 2,
+ "connection": "wifi",
+ "firmware_name": "2.18.0",
+ "firmware_revision": 2018000,
+ "homekit_status": "configured",
+ "max_peers_reached": false,
+ "sd_status": 4,
+ "wifi_strength": 66,
+ "wifi_state": "medium"
+ },
+ {
+ "boiler_control": "onoff",
+ "dhw_control": "none",
+ "firmware_revision": 22,
+ "hardware_version": 222,
+ "id": "12:34:56:20:f5:44",
+ "outdoor_temperature": 8.2,
+ "sequence_id": 19764,
+ "type": "OTH",
+ "wifi_strength": 57
+ },
+ {
+ "battery_level": 4176,
+ "boiler_status": false,
+ "boiler_valve_comfort_boost": false,
+ "firmware_revision": 6,
+ "id": "12:34:56:20:f5:8c",
+ "last_message": 1637684297,
+ "last_seen": 1637684297,
+ "radio_id": 2,
+ "reachable": true,
+ "rf_strength": 64,
+ "type": "OTM",
+ "bridge": "12:34:56:20:f5:44",
+ "battery_state": "full"
+ },
+ {
+ "id": "12:34:56:30:d5:d4",
+ "type": "NBG",
+ "firmware_revision": 39,
+ "wifi_strength": 65,
+ "reachable": true
+ },
+ {
+ "id": "0009999992",
+ "type": "NBR",
+ "current_position": 0,
+ "target_position": 0,
+ "target_position_step": 100,
+ "firmware_revision": 16,
+ "rf_strength": 0,
+ "last_seen": 1638353156,
+ "reachable": true,
+ "therm_measured_temperature": 5,
+ "heating_power_request": 1,
+ "therm_setpoint_temperature": 7,
+ "therm_setpoint_mode": "away",
+ "therm_setpoint_start_time": 0,
+ "therm_setpoint_end_time": 0,
+ "anticipating": false,
+ "open_window": false
+ },
+ {
+ "id": "12:34:56:00:86:99",
+ "type": "NACamDoorTag",
+ "battery_state": "high",
+ "battery_level": 5240,
+ "firmware_revision": 58,
+ "rf_state": "full",
+ "rf_strength": 58,
+ "last_seen": 1642698124,
+ "last_activity": 1627757310,
+ "reachable": false,
+ "bridge": "12:34:56:00:f1:62",
+ "status": "no_news"
+ },
+ {
+ "id": "12:34:56:00:e3:9b",
+ "type": "NIS",
+ "battery_state": "low",
+ "battery_level": 5438,
+ "firmware_revision": 209,
+ "rf_state": "medium",
+ "rf_strength": 62,
+ "last_seen": 1644569790,
+ "reachable": true,
+ "bridge": "12:34:56:00:f1:62",
+ "status": "no_sound",
+ "monitoring": "off"
+ },
+ {
+ "id": "12:34:56:80:60:40",
+ "type": "NLG",
+ "offload": false,
+ "firmware_revision": 211,
+ "last_seen": 1644567372,
+ "wifi_strength": 51,
+ "reachable": true
+ },
+ {
+ "id": "12:34:56:80:00:12:ac:f2",
+ "type": "NLP",
+ "on": true,
+ "offload": false,
+ "firmware_revision": 62,
+ "last_seen": 1644569425,
+ "power": 0,
+ "reachable": true,
+ "bridge": "12:34:56:80:60:40"
+ },
+ {
+ "id": "12:34:56:80:00:c3:69:3c",
+ "type": "NLT",
+ "battery_state": "full",
+ "battery_level": 3300,
+ "firmware_revision": 42,
+ "last_seen": 0,
+ "reachable": false,
+ "bridge": "12:34:56:80:60:40"
+ },
+ {
+ "id": "12:34:56:00:16:0e",
+ "type": "NLE",
+ "firmware_revision": 14,
+ "wifi_strength": 38
+ },
+ {
+ "id": "12:34:56:00:16:0e#0",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#1",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#2",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#3",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#4",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#5",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#6",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#7",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:16:0e#8",
+ "type": "NLE",
+ "bridge": "12:34:56:00:16:0e"
+ },
+ {
+ "id": "12:34:56:00:00:a1:4c:da",
+ "type": "NLPC",
+ "firmware_revision": 62,
+ "last_seen": 1646511241,
+ "power": 476,
+ "reachable": true,
+ "bridge": "12:34:56:80:60:40"
}
],
"rooms": [
@@ -85,6 +903,19 @@
"therm_setpoint_end_time": 0,
"anticipating": false,
"open_window": false
+ },
+ {
+ "id": "2940411588",
+ "reachable": true,
+ "anticipating": false,
+ "heating_power_request": 0,
+ "open_window": false,
+ "humidity": 68,
+ "therm_measured_temperature": 19.9,
+ "therm_setpoint_temperature": 21.5,
+ "therm_setpoint_start_time": 1647793285,
+ "therm_setpoint_end_time": null,
+ "therm_setpoint_mode": "home"
}
],
"id": "91763b24c43d3e344f424e8b",
diff --git a/tests/components/netatmo/test_camera.py b/tests/components/netatmo/test_camera.py
index 5b01668925f..ea39497ce58 100644
--- a/tests/components/netatmo/test_camera.py
+++ b/tests/components/netatmo/test_camera.py
@@ -93,26 +93,36 @@ async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth):
assert hass.states.get(camera_entity_indoor).state == "streaming"
assert hass.states.get(camera_entity_outdoor).attributes["light_state"] == "auto"
- with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state:
+ with patch("pyatmo.home.Home.async_set_state") as mock_set_state:
await hass.services.async_call(
"camera", "turn_off", service_data={"entity_id": "camera.hall"}
)
await hass.async_block_till_done()
mock_set_state.assert_called_once_with(
- home_id="91763b24c43d3e344f424e8b",
- camera_id="12:34:56:00:f1:62",
- monitoring="off",
+ {
+ "modules": [
+ {
+ "id": "12:34:56:00:f1:62",
+ "monitoring": "off",
+ }
+ ]
+ }
)
- with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state:
+ with patch("pyatmo.home.Home.async_set_state") as mock_set_state:
await hass.services.async_call(
"camera", "turn_on", service_data={"entity_id": "camera.hall"}
)
await hass.async_block_till_done()
mock_set_state.assert_called_once_with(
- home_id="91763b24c43d3e344f424e8b",
- camera_id="12:34:56:00:f1:62",
- monitoring="on",
+ {
+ "modules": [
+ {
+ "id": "12:34:56:00:f1:62",
+ "monitoring": "on",
+ }
+ ]
+ }
)
@@ -135,15 +145,13 @@ async def test_camera_image_local(hass, config_entry, requests_mock, netatmo_aut
assert cam is not None
assert cam.state == STATE_STREAMING
+ assert cam.name == "Hall"
stream_source = await camera.async_get_stream_source(hass, camera_entity_indoor)
assert stream_source == stream_uri
- requests_mock.get(
- uri + "/live/snapshot_720.jpg",
- content=IMAGE_BYTES_FROM_STREAM,
- )
image = await camera.async_get_image(hass, camera_entity_indoor)
+
assert image.content == IMAGE_BYTES_FROM_STREAM
@@ -156,10 +164,7 @@ async def test_camera_image_vpn(hass, config_entry, requests_mock, netatmo_auth)
await hass.async_block_till_done()
- uri = (
- "https://prodvpn-eu-2.netatmo.net/restricted/10.255.248.91/"
- "6d278460699e56180d47ab47169efb31/MpEylTU2MDYzNjRVD-LJxUnIndumKzLboeAwMDqTTw,,"
- )
+ uri = "https://prodvpn-eu-6.netatmo.net/10.20.30.41/333333333333/444444444444,,"
stream_uri = uri + "/live/files/high/index.m3u8"
camera_entity_indoor = "camera.garden"
cam = hass.states.get(camera_entity_indoor)
@@ -170,10 +175,6 @@ async def test_camera_image_vpn(hass, config_entry, requests_mock, netatmo_auth)
stream_source = await camera.async_get_stream_source(hass, camera_entity_indoor)
assert stream_source == stream_uri
- requests_mock.get(
- uri + "/live/snapshot_720.jpg",
- content=IMAGE_BYTES_FROM_STREAM,
- )
image = await camera.async_get_image(hass, camera_entity_indoor)
assert image.content == IMAGE_BYTES_FROM_STREAM
@@ -192,32 +193,26 @@ async def test_service_set_person_away(hass, config_entry, netatmo_auth):
"person": "Richard Doe",
}
- with patch(
- "pyatmo.camera.AsyncCameraData.async_set_persons_away"
- ) as mock_set_persons_away:
+ with patch("pyatmo.home.Home.async_set_persons_away") as mock_set_persons_away:
await hass.services.async_call(
"netatmo", SERVICE_SET_PERSON_AWAY, service_data=data
)
await hass.async_block_till_done()
mock_set_persons_away.assert_called_once_with(
person_id="91827376-7e04-5298-83af-a0cb8372dff3",
- home_id="91763b24c43d3e344f424e8b",
)
data = {
"entity_id": "camera.hall",
}
- with patch(
- "pyatmo.camera.AsyncCameraData.async_set_persons_away"
- ) as mock_set_persons_away:
+ with patch("pyatmo.home.Home.async_set_persons_away") as mock_set_persons_away:
await hass.services.async_call(
"netatmo", SERVICE_SET_PERSON_AWAY, service_data=data
)
await hass.async_block_till_done()
mock_set_persons_away.assert_called_once_with(
person_id=None,
- home_id="91763b24c43d3e344f424e8b",
)
@@ -289,16 +284,13 @@ async def test_service_set_persons_home(hass, config_entry, netatmo_auth):
"persons": "John Doe",
}
- with patch(
- "pyatmo.camera.AsyncCameraData.async_set_persons_home"
- ) as mock_set_persons_home:
+ with patch("pyatmo.home.Home.async_set_persons_home") as mock_set_persons_home:
await hass.services.async_call(
"netatmo", SERVICE_SET_PERSONS_HOME, service_data=data
)
await hass.async_block_till_done()
mock_set_persons_home.assert_called_once_with(
person_ids=["91827374-7e04-5298-83ad-a0cb8372dff1"],
- home_id="91763b24c43d3e344f424e8b",
)
@@ -316,16 +308,49 @@ async def test_service_set_camera_light(hass, config_entry, netatmo_auth):
"camera_light_mode": "on",
}
- with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state:
+ expected_data = {
+ "modules": [
+ {
+ "id": "12:34:56:00:a5:a4",
+ "floodlight": "on",
+ },
+ ],
+ }
+ with patch("pyatmo.home.Home.async_set_state") as mock_set_state:
await hass.services.async_call(
"netatmo", SERVICE_SET_CAMERA_LIGHT, service_data=data
)
await hass.async_block_till_done()
- mock_set_state.assert_called_once_with(
- home_id="91763b24c43d3e344f424e8b",
- camera_id="12:34:56:00:a5:a4",
- floodlight="on",
+ mock_set_state.assert_called_once_with(expected_data)
+
+
+async def test_service_set_camera_light_invalid_type(hass, config_entry, netatmo_auth):
+ """Test service to set the indoor camera light mode."""
+ with selected_platforms(["camera"]):
+ await hass.config_entries.async_setup(config_entry.entry_id)
+
+ await hass.async_block_till_done()
+
+ await hass.async_block_till_done()
+
+ data = {
+ "entity_id": "camera.hall",
+ "camera_light_mode": "on",
+ }
+
+ with patch("pyatmo.home.Home.async_set_state") as mock_set_state, pytest.raises(
+ HomeAssistantError
+ ) as excinfo:
+ await hass.services.async_call(
+ "netatmo",
+ SERVICE_SET_CAMERA_LIGHT,
+ service_data=data,
+ blocking=True,
)
+ await hass.async_block_till_done()
+
+ mock_set_state.assert_not_called()
+ assert excinfo.value.args == ("NACamera does not have a floodlight",)
@pytest.mark.skip
@@ -342,13 +367,13 @@ async def test_camera_reconnect_webhook(hass, config_entry):
with patch(
"homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth"
) as mock_auth, patch(
- "homeassistant.components.netatmo.PLATFORMS", ["camera"]
+ "homeassistant.components.netatmo.data_handler.PLATFORMS", ["camera"]
), patch(
"homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
), patch(
"homeassistant.components.netatmo.webhook_generate_url"
) as mock_webhook:
- mock_auth.return_value.async_post_request.side_effect = fake_post
+ mock_auth.return_value.async_post_api_request.side_effect = fake_post
mock_auth.return_value.async_addwebhook.side_effect = AsyncMock()
mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock()
mock_webhook.return_value = "https://example.com"
@@ -429,44 +454,6 @@ async def test_setup_component_no_devices(hass, config_entry):
"""Fake error during requesting backend data."""
nonlocal fake_post_hits
fake_post_hits += 1
- return "{}"
-
- with patch(
- "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth"
- ) as mock_auth, patch(
- "homeassistant.components.netatmo.PLATFORMS", ["camera"]
- ), patch(
- "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
- ), patch(
- "homeassistant.components.netatmo.webhook_generate_url"
- ):
- mock_auth.return_value.async_post_request.side_effect = fake_post_no_data
- mock_auth.return_value.async_addwebhook.side_effect = AsyncMock()
- mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock()
-
- await hass.config_entries.async_setup(config_entry.entry_id)
- await hass.async_block_till_done()
-
- assert fake_post_hits == 4
-
-
-async def test_camera_image_raises_exception(hass, config_entry, requests_mock):
- """Test setup with no devices."""
- fake_post_hits = 0
-
- async def fake_post(*args, **kwargs):
- """Return fake data."""
- nonlocal fake_post_hits
- fake_post_hits += 1
-
- if "url" not in kwargs:
- return "{}"
-
- endpoint = kwargs["url"].split("/")[-1]
-
- if "snapshot_720.jpg" in endpoint:
- raise pyatmo.exceptions.ApiError()
-
return await fake_post_request(*args, **kwargs)
with patch(
@@ -478,7 +465,45 @@ async def test_camera_image_raises_exception(hass, config_entry, requests_mock):
), patch(
"homeassistant.components.netatmo.webhook_generate_url"
):
- mock_auth.return_value.async_post_request.side_effect = fake_post
+ mock_auth.return_value.async_post_api_request.side_effect = fake_post_no_data
+ mock_auth.return_value.async_addwebhook.side_effect = AsyncMock()
+ mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock()
+
+ await hass.config_entries.async_setup(config_entry.entry_id)
+ await hass.async_block_till_done()
+
+ assert fake_post_hits == 9
+
+
+async def test_camera_image_raises_exception(hass, config_entry, requests_mock):
+ """Test setup with no devices."""
+ fake_post_hits = 0
+
+ async def fake_post(*args, **kwargs):
+ """Return fake data."""
+ nonlocal fake_post_hits
+ fake_post_hits += 1
+
+ if "endpoint" not in kwargs:
+ return "{}"
+
+ endpoint = kwargs["endpoint"].split("/")[-1]
+
+ if "snapshot_720.jpg" in endpoint:
+ raise pyatmo.ApiError()
+
+ return await fake_post_request(*args, **kwargs)
+
+ with patch(
+ "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth"
+ ) as mock_auth, patch(
+ "homeassistant.components.netatmo.data_handler.PLATFORMS", ["camera"]
+ ), patch(
+ "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
+ ), patch(
+ "homeassistant.components.netatmo.webhook_generate_url"
+ ):
+ mock_auth.return_value.async_post_api_request.side_effect = fake_post
mock_auth.return_value.async_get_image.side_effect = fake_post
mock_auth.return_value.async_addwebhook.side_effect = AsyncMock()
mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock()
diff --git a/tests/components/netatmo/test_climate.py b/tests/components/netatmo/test_climate.py
index 0d51a53ec71..cc23dc887bd 100644
--- a/tests/components/netatmo/test_climate.py
+++ b/tests/components/netatmo/test_climate.py
@@ -413,9 +413,7 @@ async def test_service_schedule_thermostats(hass, config_entry, caplog, netatmo_
climate_entity_livingroom = "climate.livingroom"
# Test setting a valid schedule
- with patch(
- "pyatmo.climate.AsyncClimate.async_switch_home_schedule"
- ) as mock_switch_home_schedule:
+ with patch("pyatmo.home.Home.async_switch_schedule") as mock_switch_schedule:
await hass.services.async_call(
"netatmo",
SERVICE_SET_SCHEDULE,
@@ -423,7 +421,7 @@ async def test_service_schedule_thermostats(hass, config_entry, caplog, netatmo_
blocking=True,
)
await hass.async_block_till_done()
- mock_switch_home_schedule.assert_called_once_with(
+ mock_switch_schedule.assert_called_once_with(
schedule_id="b1b54a2f45795764f59d50d8"
)
@@ -442,9 +440,7 @@ async def test_service_schedule_thermostats(hass, config_entry, caplog, netatmo_
)
# Test setting an invalid schedule
- with patch(
- "pyatmo.climate.AsyncClimate.async_switch_home_schedule"
- ) as mock_switch_home_schedule:
+ with patch("pyatmo.home.Home.async_switch_schedule") as mock_switch_home_schedule:
await hass.services.async_call(
"netatmo",
SERVICE_SET_SCHEDULE,
diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py
index ae13268eda3..964c7696e64 100644
--- a/tests/components/netatmo/test_config_flow.py
+++ b/tests/components/netatmo/test_config_flow.py
@@ -1,6 +1,8 @@
"""Test the Netatmo config flow."""
from unittest.mock import patch
+from pyatmo.const import ALL_SCOPES
+
from homeassistant import config_entries, data_entry_flow, setup
from homeassistant.components import zeroconf
from homeassistant.components.netatmo import config_flow
@@ -14,8 +16,6 @@ from homeassistant.components.netatmo.const import (
from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET
from homeassistant.helpers import config_entry_oauth2_flow
-from .common import ALL_SCOPES
-
from tests.common import MockConfigEntry
CLIENT_ID = "1234"
diff --git a/tests/components/netatmo/test_cover.py b/tests/components/netatmo/test_cover.py
new file mode 100644
index 00000000000..c0f34f25b24
--- /dev/null
+++ b/tests/components/netatmo/test_cover.py
@@ -0,0 +1,110 @@
+"""The tests for Netatmo cover."""
+from unittest.mock import patch
+
+from homeassistant.components.cover import (
+ ATTR_POSITION,
+ DOMAIN as COVER_DOMAIN,
+ SERVICE_CLOSE_COVER,
+ SERVICE_OPEN_COVER,
+ SERVICE_SET_COVER_POSITION,
+ SERVICE_STOP_COVER,
+)
+from homeassistant.const import ATTR_ENTITY_ID
+
+from .common import selected_platforms
+
+
+async def test_cover_setup_and_services(hass, config_entry, netatmo_auth):
+ """Test setup and services."""
+ with selected_platforms(["cover"]):
+ await hass.config_entries.async_setup(config_entry.entry_id)
+
+ await hass.async_block_till_done()
+
+ switch_entity = "cover.entrance_blinds"
+
+ assert hass.states.get(switch_entity).state == "closed"
+
+ # Test cover open
+ with patch("pyatmo.home.Home.async_set_state") as mock_set_state:
+ await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_OPEN_COVER,
+ {ATTR_ENTITY_ID: switch_entity},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ mock_set_state.assert_called_once_with(
+ {
+ "modules": [
+ {
+ "id": "0009999992",
+ "target_position": 100,
+ "bridge": "12:34:56:30:d5:d4",
+ }
+ ]
+ }
+ )
+
+ # Test cover close
+ with patch("pyatmo.home.Home.async_set_state") as mock_set_state:
+ await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_CLOSE_COVER,
+ {ATTR_ENTITY_ID: switch_entity},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ mock_set_state.assert_called_once_with(
+ {
+ "modules": [
+ {
+ "id": "0009999992",
+ "target_position": 0,
+ "bridge": "12:34:56:30:d5:d4",
+ }
+ ]
+ }
+ )
+
+ # Test stop cover
+ with patch("pyatmo.home.Home.async_set_state") as mock_set_state:
+ await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_STOP_COVER,
+ {ATTR_ENTITY_ID: switch_entity},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ mock_set_state.assert_called_once_with(
+ {
+ "modules": [
+ {
+ "id": "0009999992",
+ "target_position": -1,
+ "bridge": "12:34:56:30:d5:d4",
+ }
+ ]
+ }
+ )
+
+ # Test set cover position
+ with patch("pyatmo.home.Home.async_set_state") as mock_set_state:
+ await hass.services.async_call(
+ COVER_DOMAIN,
+ SERVICE_SET_COVER_POSITION,
+ {ATTR_ENTITY_ID: switch_entity, ATTR_POSITION: 50},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ mock_set_state.assert_called_once_with(
+ {
+ "modules": [
+ {
+ "id": "0009999992",
+ "target_position": 50,
+ "bridge": "12:34:56:30:d5:d4",
+ }
+ ]
+ }
+ )
diff --git a/tests/components/netatmo/test_device_trigger.py b/tests/components/netatmo/test_device_trigger.py
index 88e455b9f67..25b86f8410e 100644
--- a/tests/components/netatmo/test_device_trigger.py
+++ b/tests/components/netatmo/test_device_trigger.py
@@ -7,11 +7,6 @@ from homeassistant.components.netatmo import DOMAIN as NETATMO_DOMAIN
from homeassistant.components.netatmo.const import (
CLIMATE_TRIGGERS,
INDOOR_CAMERA_TRIGGERS,
- MODEL_NACAMERA,
- MODEL_NAPLUG,
- MODEL_NATHERM1,
- MODEL_NOC,
- MODEL_NRV,
NETATMO_EVENT,
OUTDOOR_CAMERA_TRIGGERS,
)
@@ -52,10 +47,10 @@ def calls(hass):
@pytest.mark.parametrize(
"platform,device_type,event_types",
[
- ("camera", MODEL_NOC, OUTDOOR_CAMERA_TRIGGERS),
- ("camera", MODEL_NACAMERA, INDOOR_CAMERA_TRIGGERS),
- ("climate", MODEL_NRV, CLIMATE_TRIGGERS),
- ("climate", MODEL_NATHERM1, CLIMATE_TRIGGERS),
+ ("camera", "NOC", OUTDOOR_CAMERA_TRIGGERS),
+ ("camera", "NACamera", INDOOR_CAMERA_TRIGGERS),
+ ("climate", "NRV", CLIMATE_TRIGGERS),
+ ("climate", "NATherm1", CLIMATE_TRIGGERS),
],
)
async def test_get_triggers(
@@ -110,15 +105,15 @@ async def test_get_triggers(
@pytest.mark.parametrize(
"platform,camera_type,event_type",
- [("camera", MODEL_NOC, trigger) for trigger in OUTDOOR_CAMERA_TRIGGERS]
- + [("camera", MODEL_NACAMERA, trigger) for trigger in INDOOR_CAMERA_TRIGGERS]
+ [("camera", "NOC", trigger) for trigger in OUTDOOR_CAMERA_TRIGGERS]
+ + [("camera", "NACamera", trigger) for trigger in INDOOR_CAMERA_TRIGGERS]
+ [
- ("climate", MODEL_NRV, trigger)
+ ("climate", "NRV", trigger)
for trigger in CLIMATE_TRIGGERS
if trigger not in SUBTYPES
]
+ [
- ("climate", MODEL_NATHERM1, trigger)
+ ("climate", "NATherm1", trigger)
for trigger in CLIMATE_TRIGGERS
if trigger not in SUBTYPES
],
@@ -188,12 +183,12 @@ async def test_if_fires_on_event(
@pytest.mark.parametrize(
"platform,camera_type,event_type,sub_type",
[
- ("climate", MODEL_NRV, trigger, subtype)
+ ("climate", "NRV", trigger, subtype)
for trigger in SUBTYPES
for subtype in SUBTYPES[trigger]
]
+ [
- ("climate", MODEL_NATHERM1, trigger, subtype)
+ ("climate", "NATherm1", trigger, subtype)
for trigger in SUBTYPES
for subtype in SUBTYPES[trigger]
],
@@ -267,7 +262,7 @@ async def test_if_fires_on_event_with_subtype(
@pytest.mark.parametrize(
"platform,device_type,event_type",
- [("climate", MODEL_NAPLUG, trigger) for trigger in CLIMATE_TRIGGERS],
+ [("climate", "NAPLUG", trigger) for trigger in CLIMATE_TRIGGERS],
)
async def test_if_invalid_device(
hass, device_reg, entity_reg, platform, device_type, event_type
diff --git a/tests/components/netatmo/test_diagnostics.py b/tests/components/netatmo/test_diagnostics.py
index 3c4a2090f1b..cf9a76e38a3 100644
--- a/tests/components/netatmo/test_diagnostics.py
+++ b/tests/components/netatmo/test_diagnostics.py
@@ -18,7 +18,7 @@ async def test_entry_diagnostics(hass, hass_client, config_entry):
), patch(
"homeassistant.components.netatmo.webhook_generate_url"
):
- mock_auth.return_value.async_post_request.side_effect = fake_post_request
+ mock_auth.return_value.async_post_api_request.side_effect = fake_post_request
mock_auth.return_value.async_addwebhook.side_effect = AsyncMock()
mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock()
assert await async_setup_component(hass, "netatmo", {})
@@ -39,16 +39,27 @@ async def test_entry_diagnostics(hass, hass_client, config_entry):
"expires_in": 60,
"refresh_token": REDACTED,
"scope": [
- "read_station",
- "read_camera",
"access_camera",
- "write_camera",
- "read_presence",
+ "access_doorbell",
"access_presence",
- "write_presence",
+ "read_bubendorff",
+ "read_camera",
+ "read_carbonmonoxidedetector",
+ "read_doorbell",
"read_homecoach",
+ "read_magellan",
+ "read_mx",
+ "read_presence",
+ "read_smarther",
"read_smokedetector",
+ "read_station",
"read_thermostat",
+ "write_bubendorff",
+ "write_camera",
+ "write_magellan",
+ "write_mx",
+ "write_presence",
+ "write_smarther",
"write_thermostat",
],
"type": "Bearer",
@@ -88,5 +99,5 @@ async def test_entry_diagnostics(hass, hass_client, config_entry):
"webhook_registered": False,
}
- for home in result["data"]["AsyncClimateTopology"]["homes"]:
+ for home in result["data"]["account"]["homes"]:
assert home["coordinates"] == REDACTED
diff --git a/tests/components/netatmo/test_init.py b/tests/components/netatmo/test_init.py
index 911fd6c309a..373d7e19765 100644
--- a/tests/components/netatmo/test_init.py
+++ b/tests/components/netatmo/test_init.py
@@ -1,11 +1,10 @@
"""The tests for Netatmo component."""
-import asyncio
from datetime import timedelta
from time import time
from unittest.mock import AsyncMock, patch
import aiohttp
-import pyatmo
+from pyatmo.const import ALL_SCOPES
from homeassistant import config_entries
from homeassistant.components.netatmo import DOMAIN
@@ -15,7 +14,6 @@ from homeassistant.setup import async_setup_component
from homeassistant.util import dt
from .common import (
- ALL_SCOPES,
FAKE_WEBHOOK_ACTIVATION,
fake_post_request,
selected_platforms,
@@ -60,7 +58,7 @@ async def test_setup_component(hass, config_entry):
) as mock_impl, patch(
"homeassistant.components.netatmo.webhook_generate_url"
) as mock_webhook:
- mock_auth.return_value.async_post_request.side_effect = fake_post_request
+ mock_auth.return_value.async_post_api_request.side_effect = fake_post_request
mock_auth.return_value.async_addwebhook.side_effect = AsyncMock()
mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock()
assert await async_setup_component(hass, "netatmo", {})
@@ -100,9 +98,9 @@ async def test_setup_component_with_config(hass, config_entry):
) as mock_webhook, patch(
"homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth",
) as mock_auth, patch(
- "homeassistant.components.netatmo.PLATFORMS", ["sensor"]
+ "homeassistant.components.netatmo.data_handler.PLATFORMS", ["sensor"]
):
- mock_auth.return_value.async_post_request.side_effect = fake_post
+ mock_auth.return_value.async_post_api_request.side_effect = fake_post
mock_auth.return_value.async_addwebhook.side_effect = AsyncMock()
mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock()
@@ -112,7 +110,7 @@ async def test_setup_component_with_config(hass, config_entry):
await hass.async_block_till_done()
- assert fake_post_hits == 9
+ assert fake_post_hits == 8
mock_impl.assert_called_once()
mock_webhook.assert_called_once()
@@ -162,7 +160,7 @@ async def test_setup_without_https(hass, config_entry, caplog):
), patch(
"homeassistant.components.netatmo.webhook_generate_url"
) as mock_async_generate_url:
- mock_auth.return_value.async_post_request.side_effect = fake_post_request
+ mock_auth.return_value.async_post_api_request.side_effect = fake_post_request
mock_async_generate_url.return_value = "http://example.com"
assert await async_setup_component(
hass, "netatmo", {"netatmo": {"client_id": "123", "client_secret": "abc"}}
@@ -200,7 +198,7 @@ async def test_setup_with_cloud(hass, config_entry):
), patch(
"homeassistant.components.netatmo.webhook_generate_url"
):
- mock_auth.return_value.async_post_request.side_effect = fake_post_request
+ mock_auth.return_value.async_post_api_request.side_effect = fake_post_request
assert await async_setup_component(
hass, "netatmo", {"netatmo": {"client_id": "123", "client_secret": "abc"}}
)
@@ -266,7 +264,7 @@ async def test_setup_with_cloudhook(hass):
), patch(
"homeassistant.components.netatmo.webhook_generate_url"
):
- mock_auth.return_value.async_post_request.side_effect = fake_post_request
+ mock_auth.return_value.async_post_api_request.side_effect = fake_post_request
mock_auth.return_value.async_addwebhook.side_effect = AsyncMock()
mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock()
assert await async_setup_component(hass, "netatmo", {})
@@ -289,52 +287,6 @@ async def test_setup_with_cloudhook(hass):
assert not hass.config_entries.async_entries(DOMAIN)
-async def test_setup_component_api_error(hass, config_entry):
- """Test error on setup of the netatmo component."""
- with patch(
- "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth",
- ) as mock_auth, patch(
- "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
- ) as mock_impl, patch(
- "homeassistant.components.netatmo.webhook_generate_url"
- ):
- mock_auth.return_value.async_post_request.side_effect = (
- pyatmo.exceptions.ApiError()
- )
-
- mock_auth.return_value.async_addwebhook.side_effect = AsyncMock()
- mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock()
- assert await async_setup_component(hass, "netatmo", {})
-
- await hass.async_block_till_done()
-
- mock_auth.assert_called_once()
- mock_impl.assert_called_once()
-
-
-async def test_setup_component_api_timeout(hass, config_entry):
- """Test timeout on setup of the netatmo component."""
- with patch(
- "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth",
- ) as mock_auth, patch(
- "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation",
- ) as mock_impl, patch(
- "homeassistant.components.netatmo.webhook_generate_url"
- ):
- mock_auth.return_value.async_post_request.side_effect = (
- asyncio.exceptions.TimeoutError()
- )
-
- mock_auth.return_value.async_addwebhook.side_effect = AsyncMock()
- mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock()
- assert await async_setup_component(hass, "netatmo", {})
-
- await hass.async_block_till_done()
-
- mock_auth.assert_called_once()
- mock_impl.assert_called_once()
-
-
async def test_setup_component_with_delay(hass, config_entry):
"""Test setup of the netatmo component with delayed startup."""
hass.state = CoreState.not_running
@@ -348,9 +300,9 @@ async def test_setup_component_with_delay(hass, config_entry):
) as mock_impl, patch(
"homeassistant.components.netatmo.webhook_generate_url"
) as mock_webhook, patch(
- "pyatmo.AbstractAsyncAuth.async_post_request", side_effect=fake_post_request
- ) as mock_post_request, patch(
- "homeassistant.components.netatmo.PLATFORMS", ["light"]
+ "pyatmo.AbstractAsyncAuth.async_post_api_request", side_effect=fake_post_request
+ ) as mock_post_api_request, patch(
+ "homeassistant.components.netatmo.data_handler.PLATFORMS", ["light"]
):
assert await async_setup_component(
@@ -359,7 +311,7 @@ async def test_setup_component_with_delay(hass, config_entry):
await hass.async_block_till_done()
- assert mock_post_request.call_count == 8
+ assert mock_post_api_request.call_count == 7
mock_impl.assert_called_once()
mock_webhook.assert_not_called()
@@ -422,7 +374,7 @@ async def test_setup_component_invalid_token_scope(hass):
) as mock_impl, patch(
"homeassistant.components.netatmo.webhook_generate_url"
) as mock_webhook:
- mock_auth.return_value.async_post_request.side_effect = fake_post_request
+ mock_auth.return_value.async_post_api_request.side_effect = fake_post_request
mock_auth.return_value.async_addwebhook.side_effect = AsyncMock()
mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock()
assert await async_setup_component(hass, "netatmo", {})
@@ -465,7 +417,7 @@ async def test_setup_component_invalid_token(hass, config_entry):
) as mock_webhook, patch(
"homeassistant.helpers.config_entry_oauth2_flow.OAuth2Session"
) as mock_session:
- mock_auth.return_value.async_post_request.side_effect = fake_post_request
+ mock_auth.return_value.async_post_api_request.side_effect = fake_post_request
mock_auth.return_value.async_addwebhook.side_effect = AsyncMock()
mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock()
mock_session.return_value.async_ensure_token_valid.side_effect = (
diff --git a/tests/components/netatmo/test_light.py b/tests/components/netatmo/test_light.py
index 433841f3878..ced24c738e3 100644
--- a/tests/components/netatmo/test_light.py
+++ b/tests/components/netatmo/test_light.py
@@ -14,8 +14,8 @@ from .common import FAKE_WEBHOOK_ACTIVATION, selected_platforms, simulate_webhoo
from tests.test_util.aiohttp import AiohttpClientMockResponse
-async def test_light_setup_and_services(hass, config_entry, netatmo_auth):
- """Test setup and services."""
+async def test_camera_light_setup_and_services(hass, config_entry, netatmo_auth):
+ """Test camera ligiht setup and services."""
with selected_platforms(["light"]):
await hass.config_entries.async_setup(config_entry.entry_id)
@@ -53,7 +53,7 @@ async def test_light_setup_and_services(hass, config_entry, netatmo_auth):
assert hass.states.get(light_entity).state == "on"
# Test turning light off
- with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state:
+ with patch("pyatmo.home.Home.async_set_state") as mock_set_state:
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_OFF,
@@ -62,13 +62,11 @@ async def test_light_setup_and_services(hass, config_entry, netatmo_auth):
)
await hass.async_block_till_done()
mock_set_state.assert_called_once_with(
- home_id="91763b24c43d3e344f424e8b",
- camera_id="12:34:56:00:a5:a4",
- floodlight="auto",
+ {"modules": [{"id": "12:34:56:00:a5:a4", "floodlight": "auto"}]}
)
# Test turning light on
- with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state:
+ with patch("pyatmo.home.Home.async_set_state") as mock_set_state:
await hass.services.async_call(
LIGHT_DOMAIN,
SERVICE_TURN_ON,
@@ -77,9 +75,7 @@ async def test_light_setup_and_services(hass, config_entry, netatmo_auth):
)
await hass.async_block_till_done()
mock_set_state.assert_called_once_with(
- home_id="91763b24c43d3e344f424e8b",
- camera_id="12:34:56:00:a5:a4",
- floodlight="on",
+ {"modules": [{"id": "12:34:56:00:a5:a4", "floodlight": "on"}]}
)
@@ -93,7 +89,7 @@ async def test_setup_component_no_devices(hass, config_entry):
fake_post_hits += 1
return AiohttpClientMockResponse(
method="POST",
- url=kwargs["url"],
+ url=kwargs["endpoint"],
json={},
)
@@ -106,7 +102,7 @@ async def test_setup_component_no_devices(hass, config_entry):
), patch(
"homeassistant.components.netatmo.webhook_generate_url"
):
- mock_auth.return_value.async_post_request.side_effect = (
+ mock_auth.return_value.async_post_api_request.side_effect = (
fake_post_request_no_data
)
mock_auth.return_value.async_addwebhook.side_effect = AsyncMock()
@@ -125,3 +121,57 @@ async def test_setup_component_no_devices(hass, config_entry):
assert hass.config_entries.async_entries(DOMAIN)
assert len(hass.states.async_all()) == 0
+
+
+async def test_light_setup_and_services(hass, config_entry, netatmo_auth):
+ """Test setup and services."""
+ with selected_platforms(["light"]):
+ await hass.config_entries.async_setup(config_entry.entry_id)
+
+ await hass.async_block_till_done()
+
+ light_entity = "light.bathroom_light"
+
+ assert hass.states.get(light_entity).state == "off"
+
+ # Test turning light off
+ with patch("pyatmo.home.Home.async_set_state") as mock_set_state:
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: light_entity},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ mock_set_state.assert_called_once_with(
+ {
+ "modules": [
+ {
+ "id": "12:34:56:00:01:01:01:a1",
+ "on": False,
+ "bridge": "12:34:56:80:60:40",
+ }
+ ]
+ }
+ )
+
+ # Test turning light on
+ with patch("pyatmo.home.Home.async_set_state") as mock_set_state:
+ await hass.services.async_call(
+ LIGHT_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: light_entity},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ mock_set_state.assert_called_once_with(
+ {
+ "modules": [
+ {
+ "id": "12:34:56:00:01:01:01:a1",
+ "on": True,
+ "bridge": "12:34:56:80:60:40",
+ }
+ ]
+ }
+ )
diff --git a/tests/components/netatmo/test_media_source.py b/tests/components/netatmo/test_media_source.py
index 01422dfd118..96566af6832 100644
--- a/tests/components/netatmo/test_media_source.py
+++ b/tests/components/netatmo/test_media_source.py
@@ -70,13 +70,13 @@ async def test_async_browse_media(hass):
# Test successful event listing
media = await async_browse_media(
- hass, f"{URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/1599152672"
+ hass, f"{URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/1654191519"
)
assert media
# Test successful event resolve
media = await async_resolve_media(
- hass, f"{URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/1599152672", None
+ hass, f"{URI_SCHEME}{DOMAIN}/events/12:34:56:78:90:ab/1654191519", None
)
assert media == PlayMedia(
url="http:///files/high/index.m3u8", mime_type="application/x-mpegURL"
diff --git a/tests/components/netatmo/test_select.py b/tests/components/netatmo/test_select.py
index 12168a03ad8..ea8e88ce8de 100644
--- a/tests/components/netatmo/test_select.py
+++ b/tests/components/netatmo/test_select.py
@@ -19,7 +19,7 @@ async def test_select_schedule_thermostats(hass, config_entry, caplog, netatmo_a
await hass.async_block_till_done()
webhook_id = config_entry.data[CONF_WEBHOOK_ID]
- select_entity = "select.netatmo_myhome"
+ select_entity = "select.myhome"
assert hass.states.get(select_entity).state == "Default"
@@ -40,9 +40,7 @@ async def test_select_schedule_thermostats(hass, config_entry, caplog, netatmo_a
]
# Test setting a different schedule
- with patch(
- "pyatmo.climate.AsyncClimate.async_switch_home_schedule"
- ) as mock_switch_home_schedule:
+ with patch("pyatmo.home.Home.async_switch_schedule") as mock_switch_home_schedule:
await hass.services.async_call(
SELECT_DOMAIN,
SERVICE_SELECT_OPTION,
diff --git a/tests/components/netatmo/test_sensor.py b/tests/components/netatmo/test_sensor.py
index 9adc7423bd6..99e76389c13 100644
--- a/tests/components/netatmo/test_sensor.py
+++ b/tests/components/netatmo/test_sensor.py
@@ -4,7 +4,6 @@ from unittest.mock import patch
import pytest
from homeassistant.components.netatmo import sensor
-from homeassistant.components.netatmo.sensor import MODULE_TYPE_WIND
from homeassistant.helpers import entity_registry as er
from .common import TEST_TIME, selected_platforms
@@ -17,7 +16,7 @@ async def test_weather_sensor(hass, config_entry, netatmo_auth):
await hass.async_block_till_done()
- prefix = "sensor.mystation_"
+ prefix = "sensor.netatmoindoor_"
assert hass.states.get(f"{prefix}temperature").state == "24.6"
assert hass.states.get(f"{prefix}humidity").state == "36"
@@ -102,77 +101,19 @@ async def test_process_health(health, expected):
assert sensor.process_health(health) == expected
-@pytest.mark.parametrize(
- "model, data, expected",
- [
- (MODULE_TYPE_WIND, 5591, "Full"),
- (MODULE_TYPE_WIND, 5181, "High"),
- (MODULE_TYPE_WIND, 4771, "Medium"),
- (MODULE_TYPE_WIND, 4361, "Low"),
- (MODULE_TYPE_WIND, 4300, "Very Low"),
- ],
-)
-async def test_process_battery(model, data, expected):
- """Test battery level translation."""
- assert sensor.process_battery(data, model) == expected
-
-
-@pytest.mark.parametrize(
- "angle, expected",
- [
- (0, "N"),
- (40, "NE"),
- (70, "E"),
- (130, "SE"),
- (160, "S"),
- (220, "SW"),
- (250, "W"),
- (310, "NW"),
- (340, "N"),
- ],
-)
-async def test_process_angle(angle, expected):
- """Test wind direction translation."""
- assert sensor.process_angle(angle) == expected
-
-
-@pytest.mark.parametrize(
- "angle, expected",
- [(-1, 359), (-40, 320)],
-)
-async def test_fix_angle(angle, expected):
- """Test wind angle fix."""
- assert sensor.fix_angle(angle) == expected
-
-
@pytest.mark.parametrize(
"uid, name, expected",
[
- ("12:34:56:37:11:ca-reachable", "netatmo_mystation_reachable", "True"),
- ("12:34:56:03:1b:e4-rf_status", "netatmo_mystation_yard_radio", "Full"),
- (
- "12:34:56:05:25:6e-rf_status",
- "netatmo_valley_road_rain_gauge_radio",
- "Medium",
- ),
- (
- "12:34:56:36:fc:de-rf_status_lvl",
- "netatmo_mystation_netatmooutdoor_radio_level",
- "65",
- ),
- (
- "12:34:56:37:11:ca-wifi_status_lvl",
- "netatmo_mystation_wifi_level",
- "45",
- ),
+ ("12:34:56:37:11:ca-reachable", "mystation_reachable", "True"),
+ ("12:34:56:03:1b:e4-rf_status", "mystation_yard_radio", "Full"),
(
"12:34:56:37:11:ca-wifi_status",
- "netatmo_mystation_wifi_status",
+ "mystation_wifi_strength",
"Full",
),
(
"12:34:56:37:11:ca-temp_trend",
- "netatmo_mystation_temperature_trend",
+ "mystation_temperature_trend",
"stable",
),
(
@@ -182,33 +123,47 @@ async def test_fix_angle(angle, expected):
),
("12:34:56:05:51:20-sum_rain_1", "netatmo_mystation_yard_rain_last_hour", "0"),
("12:34:56:05:51:20-sum_rain_24", "netatmo_mystation_yard_rain_today", "0"),
- ("12:34:56:03:1b:e4-windangle", "netatmo_mystation_garden_direction", "SW"),
+ ("12:34:56:03:1b:e4-windangle", "netatmoindoor_garden_direction", "SW"),
(
"12:34:56:03:1b:e4-windangle_value",
- "netatmo_mystation_garden_angle",
+ "netatmoindoor_garden_angle",
"217",
),
("12:34:56:03:1b:e4-gustangle", "mystation_garden_gust_direction", "S"),
(
"12:34:56:03:1b:e4-gustangle",
- "netatmo_mystation_garden_gust_direction",
+ "netatmoindoor_garden_gust_direction",
"S",
),
(
"12:34:56:03:1b:e4-gustangle_value",
- "netatmo_mystation_garden_gust_angle_value",
+ "netatmoindoor_garden_gust_angle",
"206",
),
(
"12:34:56:03:1b:e4-guststrength",
- "netatmo_mystation_garden_gust_strength",
+ "netatmoindoor_garden_gust_strength",
"9",
),
+ (
+ "12:34:56:03:1b:e4-rf_status",
+ "netatmoindoor_garden_rf_strength",
+ "Full",
+ ),
(
"12:34:56:26:68:92-health_idx",
- "netatmo_baby_bedroom_health",
+ "baby_bedroom_health",
"Fine",
),
+ (
+ "12:34:56:26:68:92-wifi_status",
+ "baby_bedroom_wifi",
+ "High",
+ ),
+ ("Home-max-windangle_value", "home_max_wind_angle", "17"),
+ ("Home-max-gustangle_value", "home_max_gust_angle", "217"),
+ ("Home-max-guststrength", "home_max_gust_strength", "31"),
+ ("Home-max-sum_rain_1", "home_max_sum_rain_1", "0.2"),
],
)
async def test_weather_sensor_enabling(
diff --git a/tests/components/netatmo/test_switch.py b/tests/components/netatmo/test_switch.py
new file mode 100644
index 00000000000..dc11ac22746
--- /dev/null
+++ b/tests/components/netatmo/test_switch.py
@@ -0,0 +1,65 @@
+"""The tests for Netatmo switch."""
+from unittest.mock import patch
+
+from homeassistant.components.switch import (
+ DOMAIN as SWITCH_DOMAIN,
+ SERVICE_TURN_OFF,
+ SERVICE_TURN_ON,
+)
+from homeassistant.const import ATTR_ENTITY_ID
+
+from .common import selected_platforms
+
+
+async def test_switch_setup_and_services(hass, config_entry, netatmo_auth):
+ """Test setup and services."""
+ with selected_platforms(["switch"]):
+ await hass.config_entries.async_setup(config_entry.entry_id)
+
+ await hass.async_block_till_done()
+
+ switch_entity = "switch.prise"
+
+ assert hass.states.get(switch_entity).state == "on"
+
+ # Test turning switch off
+ with patch("pyatmo.home.Home.async_set_state") as mock_set_state:
+ await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_OFF,
+ {ATTR_ENTITY_ID: switch_entity},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ mock_set_state.assert_called_once_with(
+ {
+ "modules": [
+ {
+ "id": "12:34:56:80:00:12:ac:f2",
+ "on": False,
+ "bridge": "12:34:56:80:60:40",
+ }
+ ]
+ }
+ )
+
+ # Test turning switch on
+ with patch("pyatmo.home.Home.async_set_state") as mock_set_state:
+ await hass.services.async_call(
+ SWITCH_DOMAIN,
+ SERVICE_TURN_ON,
+ {ATTR_ENTITY_ID: switch_entity},
+ blocking=True,
+ )
+ await hass.async_block_till_done()
+ mock_set_state.assert_called_once_with(
+ {
+ "modules": [
+ {
+ "id": "12:34:56:80:00:12:ac:f2",
+ "on": True,
+ "bridge": "12:34:56:80:60:40",
+ }
+ ]
+ }
+ )