Home Assistant Unofficial Reference 2024.12.1
camera.py
Go to the documentation of this file.
1 """Support for the Netatmo cameras."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any, cast
7 
8 import aiohttp
9 from pyatmo import ApiError as NetatmoApiError, modules as NaModules
10 from pyatmo.event import Event as NaEvent
11 import voluptuous as vol
12 
13 from homeassistant.components.camera import Camera, CameraEntityFeature
14 from homeassistant.config_entries import ConfigEntry
15 from homeassistant.core import HomeAssistant, callback
16 from homeassistant.exceptions import HomeAssistantError
17 from homeassistant.helpers import config_validation as cv, entity_platform
18 from homeassistant.helpers.dispatcher import async_dispatcher_connect
19 from homeassistant.helpers.entity_platform import AddEntitiesCallback
20 
21 from .const import (
22  ATTR_CAMERA_LIGHT_MODE,
23  ATTR_PERSON,
24  ATTR_PERSONS,
25  CAMERA_LIGHT_MODES,
26  CONF_URL_SECURITY,
27  DATA_CAMERAS,
28  DATA_EVENTS,
29  DOMAIN,
30  EVENT_TYPE_LIGHT_MODE,
31  EVENT_TYPE_OFF,
32  EVENT_TYPE_ON,
33  MANUFACTURER,
34  NETATMO_CREATE_CAMERA,
35  SERVICE_SET_CAMERA_LIGHT,
36  SERVICE_SET_PERSON_AWAY,
37  SERVICE_SET_PERSONS_HOME,
38  WEBHOOK_LIGHT_MODE,
39  WEBHOOK_NACAMERA_CONNECTION,
40  WEBHOOK_PUSH_TYPE,
41 )
42 from .data_handler import EVENT, HOME, SIGNAL_NAME, NetatmoDevice
43 from .entity import NetatmoModuleEntity
44 
45 _LOGGER = logging.getLogger(__name__)
46 
47 DEFAULT_QUALITY = "high"
48 
49 
51  hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
52 ) -> None:
53  """Set up the Netatmo camera platform."""
54 
55  @callback
56  def _create_entity(netatmo_device: NetatmoDevice) -> None:
57  entity = NetatmoCamera(netatmo_device)
58  async_add_entities([entity])
59 
60  entry.async_on_unload(
61  async_dispatcher_connect(hass, NETATMO_CREATE_CAMERA, _create_entity)
62  )
63 
64  platform = entity_platform.async_get_current_platform()
65 
66  platform.async_register_entity_service(
67  SERVICE_SET_PERSONS_HOME,
68  {vol.Required(ATTR_PERSONS): vol.All(cv.ensure_list, [cv.string])},
69  "_service_set_persons_home",
70  )
71  platform.async_register_entity_service(
72  SERVICE_SET_PERSON_AWAY,
73  {vol.Optional(ATTR_PERSON): cv.string},
74  "_service_set_person_away",
75  )
76  platform.async_register_entity_service(
77  SERVICE_SET_CAMERA_LIGHT,
78  {vol.Required(ATTR_CAMERA_LIGHT_MODE): vol.In(CAMERA_LIGHT_MODES)},
79  "_service_set_camera_light",
80  )
81 
82 
84  """Representation of a Netatmo camera."""
85 
86  _attr_brand = MANUFACTURER
87  _attr_supported_features = CameraEntityFeature.STREAM
88  _attr_configuration_url = CONF_URL_SECURITY
89  device: NaModules.Camera
90  _quality = DEFAULT_QUALITY
91  _monitoring: bool | None = None
92  _attr_name = None
93 
94  def __init__(
95  self,
96  netatmo_device: NetatmoDevice,
97  ) -> None:
98  """Set up for access to the Netatmo camera images."""
99  Camera.__init__(self)
100  super().__init__(netatmo_device)
101 
102  self._attr_unique_id_attr_unique_id = f"{netatmo_device.device.entity_id}-{self.device_type}"
103  self._light_state_light_state = None
104 
105  self._publishers.extend(
106  [
107  {
108  "name": HOME,
109  "home_id": self.homehome.entity_id,
110  SIGNAL_NAME: f"{HOME}-{self.home.entity_id}",
111  },
112  {
113  "name": EVENT,
114  "home_id": self.homehome.entity_id,
115  SIGNAL_NAME: f"{EVENT}-{self.home.entity_id}",
116  },
117  ]
118  )
119 
120  async def async_added_to_hass(self) -> None:
121  """Entity created."""
122  await super().async_added_to_hass()
123 
124  for event_type in (EVENT_TYPE_LIGHT_MODE, EVENT_TYPE_OFF, EVENT_TYPE_ON):
125  self.async_on_removeasync_on_remove(
127  self.hasshass,
128  f"signal-{DOMAIN}-webhook-{event_type}",
129  self.handle_eventhandle_event,
130  )
131  )
132 
133  self.hasshass.data[DOMAIN][DATA_CAMERAS][self.devicedevice.entity_id] = self.devicedevice.name
134 
135  @callback
136  def handle_event(self, event: dict) -> None:
137  """Handle webhook events."""
138  data = event["data"]
139 
140  if not data.get("camera_id"):
141  return
142 
143  if (
144  data["home_id"] == self.homehome.entity_id
145  and data["camera_id"] == self.devicedevice.entity_id
146  ):
147  if data[WEBHOOK_PUSH_TYPE] in ("NACamera-off", "NACamera-disconnection"):
148  self._attr_is_streaming_attr_is_streaming = False
149  self._monitoring_monitoring = False
150  elif data[WEBHOOK_PUSH_TYPE] in (
151  "NACamera-on",
152  WEBHOOK_NACAMERA_CONNECTION,
153  ):
154  self._attr_is_streaming_attr_is_streaming = True
155  self._monitoring_monitoring = True
156  elif data[WEBHOOK_PUSH_TYPE] == WEBHOOK_LIGHT_MODE:
157  self._light_state_light_state = data["sub_type"]
158  self._attr_extra_state_attributes_attr_extra_state_attributes.update(
159  {"light_state": self._light_state_light_state}
160  )
161 
162  self.async_write_ha_stateasync_write_ha_stateasync_write_ha_state()
163  return
164 
166  self, width: int | None = None, height: int | None = None
167  ) -> bytes | None:
168  """Return a still image response from the camera."""
169  try:
170  return cast(bytes, await self.devicedevice.async_get_live_snapshot())
171  except (
172  aiohttp.ClientPayloadError,
173  aiohttp.ContentTypeError,
174  aiohttp.ServerDisconnectedError,
175  aiohttp.ClientConnectorError,
176  NetatmoApiError,
177  ) as err:
178  _LOGGER.debug("Could not fetch live camera image (%s)", err)
179  return None
180 
181  @property
182  def supported_features(self) -> CameraEntityFeature:
183  """Return supported features."""
184  supported_features = CameraEntityFeature.ON_OFF
185  if self.device_typedevice_typedevice_type != "NDB":
186  supported_features |= CameraEntityFeature.STREAM
187  return supported_features
188 
189  async def async_turn_off(self) -> None:
190  """Turn off camera."""
191  await self.devicedevice.async_monitoring_off()
192 
193  async def async_turn_on(self) -> None:
194  """Turn on camera."""
195  await self.devicedevice.async_monitoring_on()
196 
197  async def stream_source(self) -> str:
198  """Return the stream source."""
199  if self.devicedevice.is_local:
200  await self.devicedevice.async_update_camera_urls()
201 
202  if self.devicedevice.local_url:
203  return f"{self.device.local_url}/live/files/{self._quality}/index.m3u8"
204  return f"{self.device.vpn_url}/live/files/{self._quality}/index.m3u8"
205 
206  @callback
207  def async_update_callback(self) -> None:
208  """Update the entity's state."""
209  self._attr_is_on_attr_is_on = self.devicedevice.alim_status is not None
210  self._attr_available_attr_available = self.devicedevice.alim_status is not None
211 
212  if self.devicedevice.monitoring is not None:
213  self._attr_is_streaming_attr_is_streaming = self.devicedevice.monitoring
214  self._attr_motion_detection_enabled_attr_motion_detection_enabled = self.devicedevice.monitoring
215 
216  self.hasshass.data[DOMAIN][DATA_EVENTS][self.devicedevice.entity_id] = (
217  self.process_eventsprocess_events(self.devicedevice.events)
218  )
219 
220  self._attr_extra_state_attributes_attr_extra_state_attributes.update(
221  {
222  "id": self.devicedevice.entity_id,
223  "monitoring": self._monitoring_monitoring,
224  "sd_status": self.devicedevice.sd_status,
225  "alim_status": self.devicedevice.alim_status,
226  "is_local": self.devicedevice.is_local,
227  "vpn_url": self.devicedevice.vpn_url,
228  "local_url": self.devicedevice.local_url,
229  "light_state": self._light_state_light_state,
230  }
231  )
232 
233  def process_events(self, event_list: list[NaEvent]) -> dict:
234  """Add meta data to events."""
235  events = {}
236  for event in event_list:
237  if not (video_id := event.video_id):
238  continue
239  event_data = event.__dict__
240  event_data["subevents"] = [
241  event.__dict__
242  for event in event_data.get("subevents", [])
243  if not isinstance(event, dict)
244  ]
245  event_data["media_url"] = self.get_video_urlget_video_url(video_id)
246  events[event.event_time] = event_data
247  return events
248 
249  def get_video_url(self, video_id: str) -> str:
250  """Get video url."""
251  if self.devicedevice.is_local:
252  return f"{self.device.local_url}/vod/{video_id}/files/{self._quality}/index.m3u8"
253  return f"{self.device.vpn_url}/vod/{video_id}/files/{self._quality}/index.m3u8"
254 
255  def fetch_person_ids(self, persons: list[str | None]) -> list[str]:
256  """Fetch matching person ids for given list of persons."""
257  person_ids = []
258  person_id_errors = []
259 
260  for person in persons:
261  person_id = None
262  for pid, data in self.homehome.persons.items():
263  if data.pseudo == person:
264  person_ids.append(pid)
265  person_id = pid
266  break
267 
268  if person_id is None:
269  person_id_errors.append(person)
270 
271  if person_id_errors:
272  raise HomeAssistantError(f"Person(s) not registered {person_id_errors}")
273 
274  return person_ids
275 
276  async def _service_set_persons_home(self, **kwargs: Any) -> None:
277  """Service to change current home schedule."""
278  persons = kwargs.get(ATTR_PERSONS, [])
279  person_ids = self.fetch_person_idsfetch_person_ids(persons)
280 
281  await self.homehome.async_set_persons_home(person_ids=person_ids)
282  _LOGGER.debug("Set %s as at home", persons)
283 
284  async def _service_set_person_away(self, **kwargs: Any) -> None:
285  """Service to mark a person as away or set the home as empty."""
286  person = kwargs.get(ATTR_PERSON)
287  person_ids = self.fetch_person_idsfetch_person_ids([person] if person else [])
288  person_id = next(iter(person_ids), None)
289 
290  await self.homehome.async_set_persons_away(
291  person_id=person_id,
292  )
293 
294  if person_id:
295  _LOGGER.debug("Set %s as away %s", person, person_id)
296  else:
297  _LOGGER.debug("Set home as empty")
298 
299  async def _service_set_camera_light(self, **kwargs: Any) -> None:
300  """Service to set light mode."""
301  if not isinstance(self.devicedevice, NaModules.netatmo.NOC):
302  raise HomeAssistantError(
303  f"{self.device_type} <{self.device.name}> does not have a floodlight"
304  )
305 
306  mode = str(kwargs.get(ATTR_CAMERA_LIGHT_MODE))
307  _LOGGER.debug("Turn %s camera light for '%s'", mode, self._attr_name_attr_name)
308  await self.devicedevice.async_set_floodlight_state(mode)
dict process_events(self, list[NaEvent] event_list)
Definition: camera.py:233
None _service_set_persons_home(self, **Any kwargs)
Definition: camera.py:276
None _service_set_person_away(self, **Any kwargs)
Definition: camera.py:284
list[str] fetch_person_ids(self, list[str|None] persons)
Definition: camera.py:255
None __init__(self, NetatmoDevice netatmo_device)
Definition: camera.py:97
bytes|None async_camera_image(self, int|None width=None, int|None height=None)
Definition: camera.py:167
None _service_set_camera_light(self, **Any kwargs)
Definition: camera.py:299
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
None async_setup_entry(HomeAssistant hass, ConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: camera.py:52
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103