Netatmo refactor to use pyatmo 7.0.1 (#73482) (#78523)

Co-authored-by: Robert Svensson <Kane610@users.noreply.github.com>
This commit is contained in:
Tobias Sauerwein 2022-09-26 03:55:58 +02:00 committed by GitHub
parent 1b17c83095
commit 81abeac83e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 2915 additions and 1279 deletions

View file

@ -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)