* Introducing the Netatmo component As Netatmo is providing several type of device (sensor, camera), a new Netatmo component needs to be created in order to centralize the Netatmo login data. Currently this change only impacts the Netatmo Weather station * Add new Netatmo library This new API will provide access to the Welcome Camera * Basic support for Netatmo Welcome camera This change introduces support for Netatmo Welcome camera. Currently, it will add all detected camera to Home Assistant, camera filtering (similar to the one used for weather station modules) will be added later * Remove useless REQUIREMENTS * Fixes for Netatmo Welcome support * Allow to filter Welcome cameras by name and/or home * Update requirements for Netatmo components * Fix multi-camera support for Welcome * Fix pep8 error/warning * This commit also adds improved logging for bad credentials * Add Throttle decorator for Welcome update function As the update function updates the data for all cameras, we should prevent this function to be called several time during an interval
201 lines
5.2 KiB
Python
201 lines
5.2 KiB
Python
# pylint: disable=too-many-lines
|
|
"""
|
|
Component to interface with cameras.
|
|
|
|
For more details about this component, please refer to the documentation at
|
|
https://home-assistant.io/components/camera/
|
|
"""
|
|
import logging
|
|
|
|
from homeassistant.helpers.entity import Entity
|
|
from homeassistant.helpers.entity_component import EntityComponent
|
|
from homeassistant.components import bloomsky, netatmo
|
|
from homeassistant.helpers.config_validation import PLATFORM_SCHEMA # noqa
|
|
from homeassistant.components.http import HomeAssistantView
|
|
|
|
DOMAIN = 'camera'
|
|
DEPENDENCIES = ['http']
|
|
SCAN_INTERVAL = 30
|
|
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
|
|
|
# Maps discovered services to their platforms
|
|
DISCOVERY_PLATFORMS = {
|
|
bloomsky.DISCOVER_CAMERAS: 'bloomsky',
|
|
netatmo.DISCOVER_CAMERAS: 'netatmo',
|
|
}
|
|
|
|
STATE_RECORDING = 'recording'
|
|
STATE_STREAMING = 'streaming'
|
|
STATE_IDLE = 'idle'
|
|
|
|
ENTITY_IMAGE_URL = '/api/camera_proxy/{0}?token={1}'
|
|
|
|
|
|
# pylint: disable=too-many-branches
|
|
def setup(hass, config):
|
|
"""Setup the camera component."""
|
|
component = EntityComponent(
|
|
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
|
|
DISCOVERY_PLATFORMS)
|
|
|
|
hass.wsgi.register_view(CameraImageView(hass, component.entities))
|
|
hass.wsgi.register_view(CameraMjpegStream(hass, component.entities))
|
|
|
|
component.setup(config)
|
|
|
|
return True
|
|
|
|
|
|
class Camera(Entity):
|
|
"""The base class for camera entities."""
|
|
|
|
def __init__(self):
|
|
"""Initialize a camera."""
|
|
self.is_streaming = False
|
|
|
|
@property
|
|
def access_token(self):
|
|
"""Access token for this camera."""
|
|
return str(id(self))
|
|
|
|
@property
|
|
def should_poll(self):
|
|
"""No need to poll cameras."""
|
|
return False
|
|
|
|
@property
|
|
def entity_picture(self):
|
|
"""Return a link to the camera feed as entity picture."""
|
|
return ENTITY_IMAGE_URL.format(self.entity_id, self.access_token)
|
|
|
|
@property
|
|
def is_recording(self):
|
|
"""Return true if the device is recording."""
|
|
return False
|
|
|
|
@property
|
|
def brand(self):
|
|
"""Camera brand."""
|
|
return None
|
|
|
|
@property
|
|
def model(self):
|
|
"""Camera model."""
|
|
return None
|
|
|
|
def camera_image(self):
|
|
"""Return bytes of camera image."""
|
|
raise NotImplementedError()
|
|
|
|
def mjpeg_stream(self, response):
|
|
"""Generate an HTTP MJPEG stream from camera images."""
|
|
import eventlet
|
|
response.content_type = ('multipart/x-mixed-replace; '
|
|
'boundary=--jpegboundary')
|
|
|
|
def stream():
|
|
"""Stream images as mjpeg stream."""
|
|
try:
|
|
last_image = None
|
|
while True:
|
|
img_bytes = self.camera_image()
|
|
|
|
if img_bytes is not None and img_bytes != last_image:
|
|
yield bytes(
|
|
'--jpegboundary\r\n'
|
|
'Content-Type: image/jpeg\r\n'
|
|
'Content-Length: {}\r\n\r\n'.format(
|
|
len(img_bytes)), 'utf-8') + img_bytes + b'\r\n'
|
|
|
|
last_image = img_bytes
|
|
|
|
eventlet.sleep(0.5)
|
|
except GeneratorExit:
|
|
pass
|
|
|
|
response.response = stream()
|
|
|
|
return response
|
|
|
|
@property
|
|
def state(self):
|
|
"""Camera state."""
|
|
if self.is_recording:
|
|
return STATE_RECORDING
|
|
elif self.is_streaming:
|
|
return STATE_STREAMING
|
|
else:
|
|
return STATE_IDLE
|
|
|
|
@property
|
|
def state_attributes(self):
|
|
"""Camera state attributes."""
|
|
attr = {
|
|
'access_token': self.access_token,
|
|
}
|
|
|
|
if self.model:
|
|
attr['model_name'] = self.model
|
|
|
|
if self.brand:
|
|
attr['brand'] = self.brand
|
|
|
|
return attr
|
|
|
|
|
|
class CameraView(HomeAssistantView):
|
|
"""Base CameraView."""
|
|
|
|
requires_auth = False
|
|
|
|
def __init__(self, hass, entities):
|
|
"""Initialize a basic camera view."""
|
|
super().__init__(hass)
|
|
self.entities = entities
|
|
|
|
def get(self, request, entity_id):
|
|
"""Start a get request."""
|
|
camera = self.entities.get(entity_id)
|
|
|
|
if camera is None:
|
|
return self.Response(status=404)
|
|
|
|
authenticated = (request.authenticated or
|
|
request.args.get('token') == camera.access_token)
|
|
|
|
if not authenticated:
|
|
return self.Response(status=401)
|
|
|
|
return self.handle(camera)
|
|
|
|
def handle(self, camera):
|
|
"""Hanlde the camera request."""
|
|
raise NotImplementedError()
|
|
|
|
|
|
class CameraImageView(CameraView):
|
|
"""Camera view to serve an image."""
|
|
|
|
url = "/api/camera_proxy/<entity(domain=camera):entity_id>"
|
|
name = "api:camera:image"
|
|
|
|
def handle(self, camera):
|
|
"""Serve camera image."""
|
|
response = camera.camera_image()
|
|
|
|
if response is None:
|
|
return self.Response(status=500)
|
|
|
|
return self.Response(response)
|
|
|
|
|
|
class CameraMjpegStream(CameraView):
|
|
"""Camera View to serve an MJPEG stream."""
|
|
|
|
url = "/api/camera_proxy_stream/<entity(domain=camera):entity_id>"
|
|
name = "api:camera:stream"
|
|
|
|
def handle(self, camera):
|
|
"""Serve camera image."""
|
|
return camera.mjpeg_stream(self.Response())
|