Home Assistant Unofficial Reference 2024.12.1
camera.py
Go to the documentation of this file.
1 """Camera platform that receives images through HTTP POST."""
2 
3 from __future__ import annotations
4 
5 import asyncio
6 from collections import deque
7 from datetime import timedelta
8 import logging
9 from typing import cast
10 
11 from aiohttp import web
12 import voluptuous as vol
13 
14 from homeassistant.components import webhook
16  DOMAIN as CAMERA_DOMAIN,
17  PLATFORM_SCHEMA as CAMERA_PLATFORM_SCHEMA,
18  Camera,
19  CameraState,
20 )
21 from homeassistant.const import CONF_NAME, CONF_TIMEOUT, CONF_WEBHOOK_ID
22 from homeassistant.core import HomeAssistant, callback
23 from homeassistant.helpers import config_validation as cv
24 from homeassistant.helpers.entity_platform import AddEntitiesCallback
25 from homeassistant.helpers.event import async_track_point_in_utc_time
26 from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
27 import homeassistant.util.dt as dt_util
28 
29 _LOGGER = logging.getLogger(__name__)
30 
31 CONF_BUFFER_SIZE = "buffer"
32 CONF_IMAGE_FIELD = "field"
33 
34 DEFAULT_NAME = "Push Camera"
35 
36 ATTR_FILENAME = "filename"
37 ATTR_LAST_TRIP = "last_trip"
38 
39 PUSH_CAMERA_DATA = "push_camera"
40 
41 PLATFORM_SCHEMA = CAMERA_PLATFORM_SCHEMA.extend(
42  {
43  vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
44  vol.Optional(CONF_BUFFER_SIZE, default=1): cv.positive_int,
45  vol.Optional(CONF_TIMEOUT, default=timedelta(seconds=5)): vol.All(
46  cv.time_period, cv.positive_timedelta
47  ),
48  vol.Optional(CONF_IMAGE_FIELD, default="image"): cv.string,
49  vol.Required(CONF_WEBHOOK_ID): cv.string,
50  }
51 )
52 
53 
55  hass: HomeAssistant,
56  config: ConfigType,
57  async_add_entities: AddEntitiesCallback,
58  discovery_info: DiscoveryInfoType | None = None,
59 ) -> None:
60  """Set up the Push Camera platform."""
61  if PUSH_CAMERA_DATA not in hass.data:
62  hass.data[PUSH_CAMERA_DATA] = {}
63 
64  webhook_id = config.get(CONF_WEBHOOK_ID)
65 
66  cameras = [
67  PushCamera(
68  hass,
69  config[CONF_NAME],
70  config[CONF_BUFFER_SIZE],
71  config[CONF_TIMEOUT],
72  config[CONF_IMAGE_FIELD],
73  webhook_id,
74  )
75  ]
76 
77  async_add_entities(cameras)
78 
79 
80 async def handle_webhook(
81  hass: HomeAssistant, webhook_id: str, request: web.Request
82 ) -> None:
83  """Handle incoming webhook POST with image files."""
84  try:
85  async with asyncio.timeout(5):
86  data = dict(await request.post())
87  except (TimeoutError, web.HTTPException) as error:
88  _LOGGER.error("Could not get information from POST <%s>", error)
89  return
90 
91  camera = hass.data[PUSH_CAMERA_DATA][webhook_id]
92 
93  if camera.image_field not in data:
94  _LOGGER.warning("Webhook call without POST parameter <%s>", camera.image_field)
95  return
96 
97  image_data = cast(web.FileField, data[camera.image_field])
98  await camera.update_image(image_data.file.read(), image_data.filename)
99 
100 
102  """The representation of a Push camera."""
103 
104  def __init__(self, hass, name, buffer_size, timeout, image_field, webhook_id):
105  """Initialize push camera component."""
106  super().__init__()
107  self._name_name = name
108  self._last_trip_last_trip = None
109  self._filename_filename = None
110  self._expired_listener_expired_listener = None
111  self._timeout_timeout = timeout
112  self.queuequeue = deque([], buffer_size)
113  self._current_image_current_image = None
114  self._image_field_image_field = image_field
115  self.webhook_idwebhook_id = webhook_id
116  self.webhook_urlwebhook_url = webhook.async_generate_url(hass, webhook_id)
117 
118  async def async_added_to_hass(self) -> None:
119  """Call when entity is added to hass."""
120  self.hasshass.data[PUSH_CAMERA_DATA][self.webhook_idwebhook_id] = self
121 
122  try:
123  webhook.async_register(
124  self.hasshass, CAMERA_DOMAIN, self.namenamename, self.webhook_idwebhook_id, handle_webhook
125  )
126  except ValueError:
127  _LOGGER.error(
128  "In <%s>, webhook_id <%s> already used", self.namenamename, self.webhook_idwebhook_id
129  )
130 
131  @property
132  def image_field(self):
133  """HTTP field containing the image file."""
134  return self._image_field_image_field
135 
136  async def update_image(self, image, filename):
137  """Update the camera image."""
138  if self.statestatestatestate == CameraState.IDLE:
139  self._attr_is_recording_attr_is_recording = True
140  self._last_trip_last_trip = dt_util.utcnow()
141  self.queuequeue.clear()
142 
143  self._filename_filename = filename
144  self.queuequeue.appendleft(image)
145 
146  @callback
147  def reset_state(now):
148  """Set state to idle after no new images for a period of time."""
149  self._attr_is_recording_attr_is_recording = False
150  self._expired_listener_expired_listener = None
151  _LOGGER.debug("Reset state")
152  self.async_write_ha_stateasync_write_ha_stateasync_write_ha_state()
153 
154  if self._expired_listener_expired_listener:
155  self._expired_listener_expired_listener()
156 
157  self._expired_listener_expired_listener = async_track_point_in_utc_time(
158  self.hasshass, reset_state, dt_util.utcnow() + self._timeout_timeout
159  )
160 
161  self.async_write_ha_stateasync_write_ha_stateasync_write_ha_state()
162 
164  self, width: int | None = None, height: int | None = None
165  ) -> bytes | None:
166  """Return a still image response."""
167  if self.queuequeue:
168  if self.statestatestatestate == CameraState.IDLE:
169  self.queuequeue.rotate(1)
170  self._current_image_current_image = self.queuequeue[0]
171 
172  return self._current_image_current_image
173 
174  @property
175  def name(self):
176  """Return the name of this camera."""
177  return self._name_name
178 
179  @property
181  """Camera Motion Detection Status."""
182  return False
183 
184  @property
186  """Return the state attributes."""
187  return {
188  name: value
189  for name, value in (
190  (ATTR_LAST_TRIP, self._last_trip_last_trip),
191  (ATTR_FILENAME, self._filename_filename),
192  )
193  if value is not None
194  }
def __init__(self, hass, name, buffer_size, timeout, image_field, webhook_id)
Definition: camera.py:104
bytes|None async_camera_image(self, int|None width=None, int|None height=None)
Definition: camera.py:165
def update_image(self, image, filename)
Definition: camera.py:136
str|UndefinedType|None name(self)
Definition: entity.py:738
None async_setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback async_add_entities, DiscoveryInfoType|None discovery_info=None)
Definition: camera.py:59
None handle_webhook(HomeAssistant hass, str webhook_id, web.Request request)
Definition: camera.py:82
CALLBACK_TYPE async_track_point_in_utc_time(HomeAssistant hass, HassJob[[datetime], Coroutine[Any, Any, None]|None]|Callable[[datetime], Coroutine[Any, Any, None]|None] action, datetime point_in_time)
Definition: event.py:1542