Very simple IP Camera support
This commit is contained in:
parent
378d3798fd
commit
aaf0ca2105
8 changed files with 1985 additions and 1258 deletions
263
homeassistant/components/camera/__init__.py
Normal file
263
homeassistant/components/camera/__init__.py
Normal file
|
@ -0,0 +1,263 @@
|
||||||
|
# pylint: disable=too-many-lines
|
||||||
|
"""
|
||||||
|
homeassistant.components.camera
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
Component to interface with various cameras.
|
||||||
|
|
||||||
|
The following features are supported:
|
||||||
|
-Recording
|
||||||
|
-Snapshot
|
||||||
|
-Motion Detection Recording(for supported cameras)
|
||||||
|
-Automatic Configuration(for supported cameras)
|
||||||
|
-Creation of child entities for supported functions
|
||||||
|
-Collating motion event images passed via FTP into time based events
|
||||||
|
-Returning recorded camera images and streams
|
||||||
|
-Proxying image requests via HA for external access
|
||||||
|
-Converting a still image url into a live video stream
|
||||||
|
-A service for calling camera functions
|
||||||
|
|
||||||
|
Upcoming features
|
||||||
|
-Camera movement(panning)
|
||||||
|
-Zoom
|
||||||
|
-Light/Nightvision toggling
|
||||||
|
-Support for more devices
|
||||||
|
-A demo entity
|
||||||
|
-Expanded documentation
|
||||||
|
"""
|
||||||
|
import requests
|
||||||
|
from requests.auth import HTTPBasicAuth
|
||||||
|
import logging
|
||||||
|
import time
|
||||||
|
import re
|
||||||
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.const import (
|
||||||
|
ATTR_ENTITY_PICTURE,
|
||||||
|
HTTP_NOT_FOUND,
|
||||||
|
ATTR_ENTITY_ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
from homeassistant.helpers.entity_component import EntityComponent
|
||||||
|
|
||||||
|
|
||||||
|
DOMAIN = 'camera'
|
||||||
|
DEPENDENCIES = ['http']
|
||||||
|
GROUP_NAME_ALL_CAMERAS = 'all_cameras'
|
||||||
|
SCAN_INTERVAL = 30
|
||||||
|
ENTITY_ID_FORMAT = DOMAIN + '.{}'
|
||||||
|
|
||||||
|
SWITCH_ACTION_RECORD = 'record'
|
||||||
|
SWITCH_ACTION_SNAPSHOT = 'snapshot'
|
||||||
|
|
||||||
|
SERVICE_CAMERA = 'camera_service'
|
||||||
|
|
||||||
|
STATE_RECORDING = 'recording'
|
||||||
|
|
||||||
|
DEFAULT_RECORDING_SECONDS = 30
|
||||||
|
|
||||||
|
# Maps discovered services to their platforms
|
||||||
|
DISCOVERY_PLATFORMS = {}
|
||||||
|
|
||||||
|
FILE_DATETIME_FORMAT = '%Y-%m-%d_%H-%M-%S-%f'
|
||||||
|
DIR_DATETIME_FORMAT = '%Y-%m-%d_%H-%M-%S'
|
||||||
|
|
||||||
|
REC_DIR_PREFIX = 'recording-'
|
||||||
|
REC_IMG_PREFIX = 'recording_image-'
|
||||||
|
|
||||||
|
STATE_STREAMING = 'streaming'
|
||||||
|
STATE_IDLE = 'idle'
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-many-branches
|
||||||
|
def setup(hass, config):
|
||||||
|
""" Track states and offer events for sensors. """
|
||||||
|
|
||||||
|
component = EntityComponent(
|
||||||
|
logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL,
|
||||||
|
DISCOVERY_PLATFORMS)
|
||||||
|
|
||||||
|
component.setup(config)
|
||||||
|
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# CAMERA COMPONENT ENDPOINTS
|
||||||
|
# -------------------------------------------------------------------------
|
||||||
|
# The following defines the endpoints for serving images from the camera
|
||||||
|
# via the HA http server. This is means that you can access images from
|
||||||
|
# your camera outside of your LAN without the need for port forwards etc.
|
||||||
|
|
||||||
|
# Because the authentication header can't be added in image requests these
|
||||||
|
# endpoints are secured with session based security.
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def _proxy_camera_image(handler, path_match, data):
|
||||||
|
""" Proxies the camera image via the HA server. """
|
||||||
|
entity_id = path_match.group(ATTR_ENTITY_ID)
|
||||||
|
|
||||||
|
camera = None
|
||||||
|
if entity_id in component.entities.keys():
|
||||||
|
camera = component.entities[entity_id]
|
||||||
|
|
||||||
|
if camera:
|
||||||
|
response = camera.get_camera_image()
|
||||||
|
handler.wfile.write(response)
|
||||||
|
else:
|
||||||
|
handler.send_response(HTTP_NOT_FOUND)
|
||||||
|
|
||||||
|
hass.http.register_path(
|
||||||
|
'GET',
|
||||||
|
re.compile(r'/api/camera_proxy/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||||
|
_proxy_camera_image,
|
||||||
|
require_auth=True)
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def _proxy_camera_mjpeg_stream(handler, path_match, data):
|
||||||
|
""" Proxies the camera image as an mjpeg stream via the HA server.
|
||||||
|
This function takes still images from the IP camera and turns them
|
||||||
|
into an MJPEG stream. This means that HA can return a live video
|
||||||
|
stream even with only a still image URL available.
|
||||||
|
"""
|
||||||
|
entity_id = path_match.group(ATTR_ENTITY_ID)
|
||||||
|
|
||||||
|
camera = None
|
||||||
|
if entity_id in component.entities.keys():
|
||||||
|
camera = component.entities[entity_id]
|
||||||
|
|
||||||
|
if camera:
|
||||||
|
|
||||||
|
try:
|
||||||
|
camera.is_streaming = True
|
||||||
|
camera.update_ha_state()
|
||||||
|
|
||||||
|
handler.request.sendall(bytes('HTTP/1.1 200 OK\r\n', 'utf-8'))
|
||||||
|
handler.request.sendall(bytes(
|
||||||
|
'Content-type: multipart/x-mixed-replace; \
|
||||||
|
boundary=--jpgboundary\r\n\r\n', 'utf-8'))
|
||||||
|
|
||||||
|
handler.request.sendall(bytes('--jpgboundary\r\n', 'utf-8'))
|
||||||
|
|
||||||
|
while True:
|
||||||
|
|
||||||
|
if camera.username and camera.password:
|
||||||
|
response = requests.get(
|
||||||
|
camera.still_image_url,
|
||||||
|
auth=HTTPBasicAuth(
|
||||||
|
camera.username,
|
||||||
|
camera.password))
|
||||||
|
else:
|
||||||
|
response = requests.get(camera.still_image_url)
|
||||||
|
|
||||||
|
headers_str = '\r\n'.join((
|
||||||
|
'Content-length: {}'.format(len(response.content)),
|
||||||
|
'Content-type: image/jpeg',
|
||||||
|
)) + '\r\n\r\n'
|
||||||
|
|
||||||
|
handler.request.sendall(
|
||||||
|
bytes(headers_str, 'utf-8') +
|
||||||
|
response.content +
|
||||||
|
bytes('\r\n', 'utf-8'))
|
||||||
|
|
||||||
|
handler.request.sendall(
|
||||||
|
bytes('--jpgboundary\r\n', 'utf-8'))
|
||||||
|
|
||||||
|
except (requests.RequestException, IOError):
|
||||||
|
camera.is_streaming = False
|
||||||
|
camera.update_ha_state()
|
||||||
|
|
||||||
|
else:
|
||||||
|
handler.send_response(HTTP_NOT_FOUND)
|
||||||
|
|
||||||
|
camera.is_streaming = False
|
||||||
|
|
||||||
|
hass.http.register_path(
|
||||||
|
'GET',
|
||||||
|
re.compile(
|
||||||
|
r'/api/camera_proxy_stream/(?P<entity_id>[a-zA-Z\._0-9]+)'),
|
||||||
|
_proxy_camera_mjpeg_stream,
|
||||||
|
require_auth=True)
|
||||||
|
|
||||||
|
|
||||||
|
class Camera(Entity):
|
||||||
|
""" The base class for camera components """
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_recording(self):
|
||||||
|
""" Returns true if the device is recording """
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_streaming(self):
|
||||||
|
""" Returns true if the device is streaming """
|
||||||
|
return False
|
||||||
|
|
||||||
|
@is_streaming.setter
|
||||||
|
def is_streaming(self, value):
|
||||||
|
""" Set this to true when streaming begins """
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def brand(self):
|
||||||
|
""" Should return a string of the camera brand """
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def model(self):
|
||||||
|
""" Returns string of camera model """
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def base_url(self):
|
||||||
|
""" Return the configured base URL for the camera """
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def image_url(self):
|
||||||
|
""" Return the still image segment of the URL """
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
""" Get the configuration object """
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def username(self):
|
||||||
|
""" Get the configured username """
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def password(self):
|
||||||
|
""" Get the configured password """
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def still_image_url(self):
|
||||||
|
""" Get the URL of a camera still image """
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_camera_image(self):
|
||||||
|
""" Return bytes of camera image """
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
""" Returns the state of the entity. """
|
||||||
|
if self.is_recording:
|
||||||
|
return STATE_RECORDING
|
||||||
|
elif self.is_streaming:
|
||||||
|
return STATE_STREAMING
|
||||||
|
else:
|
||||||
|
return STATE_IDLE
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state_attributes(self):
|
||||||
|
""" Returns optional state attributes. """
|
||||||
|
attr = super().state_attributes
|
||||||
|
attr['model_name'] = self.device_info.get('model', 'generic')
|
||||||
|
attr['brand'] = self.device_info.get('brand', 'generic')
|
||||||
|
attr['still_image_url'] = '/api/camera_proxy/' + self.entity_id
|
||||||
|
attr[ATTR_ENTITY_PICTURE] = (
|
||||||
|
'/api/camera_proxy/' +
|
||||||
|
self.entity_id + '?time=' +
|
||||||
|
str(time.time()))
|
||||||
|
attr['stream_url'] = '/api/camera_proxy_stream/' + self.entity_id
|
||||||
|
|
||||||
|
return attr
|
174
homeassistant/components/camera/generic.py
Normal file
174
homeassistant/components/camera/generic.py
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
"""
|
||||||
|
Support for IP Cameras.
|
||||||
|
|
||||||
|
This component provides basic support for IP camera models that do not have
|
||||||
|
a speicifc HA component.
|
||||||
|
|
||||||
|
As part of the basic support the following features will be provided:
|
||||||
|
-MJPEG video streaming
|
||||||
|
-Saving a snapshot
|
||||||
|
-Recording(JPEG frame capture)
|
||||||
|
|
||||||
|
NOTE: for the basic support to work you camera must support accessing a JPEG
|
||||||
|
snapshot via a URL and you will need to specify the "still_image_url" parameter
|
||||||
|
which should be the location of the JPEG snapshot relative to you specified
|
||||||
|
base_url. For example "snapshot.cgi" or "image.jpg".
|
||||||
|
|
||||||
|
To use this component you will need to add something like the following to your
|
||||||
|
config/configuration.yaml
|
||||||
|
|
||||||
|
camera:
|
||||||
|
platform: generic
|
||||||
|
base_url: http://YOUR_CAMERA_IP_AND_PORT/
|
||||||
|
name: Door Camera
|
||||||
|
brand: dlink
|
||||||
|
family: DCS
|
||||||
|
model: DCS-930L
|
||||||
|
username: YOUR_USERNAME
|
||||||
|
password: YOUR_PASSWORD
|
||||||
|
still_image_url: image.jpg
|
||||||
|
|
||||||
|
|
||||||
|
VARIABLES:
|
||||||
|
|
||||||
|
These are the variables for the device_data array:
|
||||||
|
|
||||||
|
base_url
|
||||||
|
*Required
|
||||||
|
The base URL for accessing you camera
|
||||||
|
Example: http://192.168.1.21:2112/
|
||||||
|
|
||||||
|
name
|
||||||
|
*Optional
|
||||||
|
This parameter allows you to override the name of your camera in homeassistant
|
||||||
|
|
||||||
|
|
||||||
|
brand
|
||||||
|
*Optional
|
||||||
|
The manufacturer of your device, used to help load the specific camera
|
||||||
|
functionality.
|
||||||
|
|
||||||
|
family
|
||||||
|
*Optional
|
||||||
|
The family of devices by the specified brand, useful when many models
|
||||||
|
support the same settings. This used when attempting load up specific
|
||||||
|
device functionality.
|
||||||
|
|
||||||
|
model
|
||||||
|
*Optional
|
||||||
|
The specific model number of your device.
|
||||||
|
|
||||||
|
still_image_url
|
||||||
|
*Optional
|
||||||
|
Useful if using an unsupported camera model. This should point to the location
|
||||||
|
of the still image on your particular camera and should be relative to your
|
||||||
|
specified base_url.
|
||||||
|
Example: cam/image.jpg
|
||||||
|
|
||||||
|
username
|
||||||
|
*Required
|
||||||
|
THe username for acessing your camera
|
||||||
|
|
||||||
|
password
|
||||||
|
*Required
|
||||||
|
the password for accessing your camera
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
|
||||||
|
from homeassistant.helpers import validate_config
|
||||||
|
from homeassistant.components.camera import DOMAIN
|
||||||
|
from homeassistant.components.camera import Camera
|
||||||
|
import requests
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
def setup_platform(hass, config, add_devices_callback, discovery_info=None):
|
||||||
|
""" Find and return Vera lights. """
|
||||||
|
if not validate_config(
|
||||||
|
{DOMAIN: config},
|
||||||
|
{DOMAIN: ['base_url', CONF_USERNAME, CONF_PASSWORD]},
|
||||||
|
_LOGGER):
|
||||||
|
return None
|
||||||
|
|
||||||
|
camera = GenericCamera(hass, config)
|
||||||
|
cameras = [camera]
|
||||||
|
|
||||||
|
add_devices_callback(cameras)
|
||||||
|
|
||||||
|
|
||||||
|
# pylint: disable=too-many-instance-attributes
|
||||||
|
class GenericCamera(Camera):
|
||||||
|
"""
|
||||||
|
Base class for cameras.
|
||||||
|
This is quite a large class but the camera component encompasses a lot of
|
||||||
|
functionality. It should take care of most of the heavy lifting and
|
||||||
|
plumbing associated with adding support for additional models of camera.
|
||||||
|
If you are adding support for a new camera your entity class should inherit
|
||||||
|
from this.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, hass, device_info):
|
||||||
|
self.hass = hass
|
||||||
|
self._device_info = device_info
|
||||||
|
self._base_url = device_info.get('base_url')
|
||||||
|
if not self._base_url.endswith('/'):
|
||||||
|
self._base_url = self._base_url + '/'
|
||||||
|
self._username = device_info.get('username')
|
||||||
|
self._password = device_info.get('password')
|
||||||
|
self._is_streaming = False
|
||||||
|
self._still_image_url = device_info.get('still_image_url', 'image.jpg')
|
||||||
|
self._logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def get_camera_image(self):
|
||||||
|
""" Return a still image reponse from the camera """
|
||||||
|
response = requests.get(
|
||||||
|
self.still_image_url,
|
||||||
|
auth=(self._username, self._password))
|
||||||
|
|
||||||
|
return response.content
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
""" Return the config data for this device """
|
||||||
|
return self._device_info
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
""" Return the name of this device """
|
||||||
|
return self._device_info.get('name') or super().name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state_attributes(self):
|
||||||
|
""" Returns optional state attributes. """
|
||||||
|
attr = super().state_attributes
|
||||||
|
|
||||||
|
return attr
|
||||||
|
|
||||||
|
@property
|
||||||
|
def base_url(self):
|
||||||
|
return self._base_url
|
||||||
|
|
||||||
|
@property
|
||||||
|
def username(self):
|
||||||
|
return self._username
|
||||||
|
|
||||||
|
@property
|
||||||
|
def password(self):
|
||||||
|
return self._password
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_streaming(self):
|
||||||
|
return self._is_streaming
|
||||||
|
|
||||||
|
@is_streaming.setter
|
||||||
|
def is_streaming(self, value):
|
||||||
|
self._is_streaming = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def still_image_url(self):
|
||||||
|
""" This should be implemented by different camera models. """
|
||||||
|
return self.base_url + self._still_image_url
|
|
@ -1,2 +1,2 @@
|
||||||
""" DO NOT MODIFY. Auto-generated by build_frontend script """
|
""" DO NOT MODIFY. Auto-generated by build_frontend script """
|
||||||
VERSION = "ed339673ca129a1a51dcc3975d0a492d"
|
VERSION = "24f15feebc48785ce908064dccbdb204"
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -34,7 +34,7 @@
|
||||||
<paper-dialog id="dialog" with-backdrop>
|
<paper-dialog id="dialog" with-backdrop>
|
||||||
<h2><state-card-content state-obj="[[stateObj]]"></state-card-content></h2>
|
<h2><state-card-content state-obj="[[stateObj]]"></state-card-content></h2>
|
||||||
<div>
|
<div>
|
||||||
<template is='dom-if' if="[[hasHistoryComponent]]">
|
<template is='dom-if' if="[[showHistoryComponent]]">
|
||||||
<state-history-charts state-history="[[stateHistory]]"
|
<state-history-charts state-history="[[stateHistory]]"
|
||||||
is-loading-data="[[isLoadingHistoryData]]"></state-history-charts>
|
is-loading-data="[[isLoadingHistoryData]]"></state-history-charts>
|
||||||
</template>
|
</template>
|
||||||
|
@ -53,6 +53,9 @@
|
||||||
var stateHistoryStore = window.hass.stateHistoryStore;
|
var stateHistoryStore = window.hass.stateHistoryStore;
|
||||||
var stateHistoryActions = window.hass.stateHistoryActions;
|
var stateHistoryActions = window.hass.stateHistoryActions;
|
||||||
|
|
||||||
|
// if you don't want the history component to show add the domain to this array
|
||||||
|
var DOMAINS_WITH_NO_HISTORY = ['camera'];
|
||||||
|
|
||||||
Polymer({
|
Polymer({
|
||||||
is: 'more-info-dialog',
|
is: 'more-info-dialog',
|
||||||
|
|
||||||
|
@ -81,6 +84,11 @@
|
||||||
value: false,
|
value: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
showHistoryComponent: {
|
||||||
|
type: Boolean,
|
||||||
|
value: false,
|
||||||
|
},
|
||||||
|
|
||||||
dialogOpen: {
|
dialogOpen: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
value: false,
|
value: false,
|
||||||
|
@ -100,7 +108,15 @@
|
||||||
var newState = this.entityId ? stateStore.get(this.entityId) : null;
|
var newState = this.entityId ? stateStore.get(this.entityId) : null;
|
||||||
|
|
||||||
if (newState !== this.stateObj) {
|
if (newState !== this.stateObj) {
|
||||||
this.stateObj = newState;
|
this.stateObj = newState;
|
||||||
|
}
|
||||||
|
if(this.stateObj) {
|
||||||
|
if(DOMAINS_WITH_NO_HISTORY.indexOf(this.stateObj.domain) !== -1) {
|
||||||
|
this.showHistoryComponent = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.showHistoryComponent = this.hasHistoryComponent;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -120,11 +136,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onIronOverlayOpened: function() {
|
onIronOverlayOpened: function() {
|
||||||
this.dialogOpen = true;
|
this.dialogOpen = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
onIronOverlayClosed: function() {
|
onIronOverlayClosed: function() {
|
||||||
this.dialogOpen = false;
|
this.dialogOpen = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -144,7 +160,7 @@
|
||||||
this.changeEntityId(entityId);
|
this.changeEntityId(entityId);
|
||||||
|
|
||||||
this.debounce('showDialogAfterRender', function() {
|
this.debounce('showDialogAfterRender', function() {
|
||||||
this.$.dialog.toggle();
|
this.$.dialog.toggle();
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
<link rel="import" href="../bower_components/polymer/polymer.html">
|
||||||
|
<link rel="import" href="../bower_components/paper-button/paper-button.html">
|
||||||
|
<link rel="import" href="../bower_components/paper-spinner/paper-spinner.html">
|
||||||
|
|
||||||
|
|
||||||
|
<dom-module id='more-info-camera'>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
|
||||||
|
:host .camera-image {
|
||||||
|
width:640px;
|
||||||
|
height:480px;
|
||||||
|
}
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
:host .camera-image {
|
||||||
|
max-width: calc(100%);
|
||||||
|
height: initial
|
||||||
|
}
|
||||||
|
|
||||||
|
:host .camera-page {
|
||||||
|
max-width: calc(100%);
|
||||||
|
max-height: calc(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 620px) {
|
||||||
|
:host .camera-image {
|
||||||
|
max-width: calc(100%);
|
||||||
|
height: initial
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:host .camera-page {
|
||||||
|
width:640px;
|
||||||
|
height:520px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class$='[[computeClassNames(stateObj)]]'>
|
||||||
|
<div id="camera_container" class="camera-container camera-page">
|
||||||
|
<img src="{{camera_image_url}}" id="camera_image" class="camera-image" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
</dom-module>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var serviceActions = window.hass.serviceActions;
|
||||||
|
var uiUtil = window.hass.uiUtil;
|
||||||
|
var ATTRIBUTE_CLASSES = ['camera'];
|
||||||
|
|
||||||
|
Polymer({
|
||||||
|
is: 'more-info-camera',
|
||||||
|
|
||||||
|
properties: {
|
||||||
|
stateObj: {
|
||||||
|
type: Object,
|
||||||
|
observer: 'stateObjChanged',
|
||||||
|
},
|
||||||
|
dialogOpen: {
|
||||||
|
type: Object,
|
||||||
|
observer: 'dialogOpenChanged',
|
||||||
|
},
|
||||||
|
camera_image_url: {
|
||||||
|
type: String,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
stateObjChanged: function(newVal, oldVal) {
|
||||||
|
if (newVal) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
computeClassNames: function(stateObj) {
|
||||||
|
return uiUtil.attributeClassNames(stateObj, ATTRIBUTE_CLASSES);
|
||||||
|
},
|
||||||
|
|
||||||
|
dialogOpenChanged: function(newVal, oldVal) {
|
||||||
|
if (newVal) {
|
||||||
|
this.startImageStream();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.stopImageStream();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
startImageStream: function() {
|
||||||
|
this.camera_image_url = this.stateObj.attributes['stream_url'];
|
||||||
|
this.isStreaming = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
stopImageStream: function() {
|
||||||
|
this.camera_image_url = this.stateObj.attributes['still_image_url'] + '?t=' + Date.now();
|
||||||
|
this.isStreaming = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</polymer-element>
|
|
@ -8,6 +8,7 @@
|
||||||
<link rel='import' href='more-info-script.html'>
|
<link rel='import' href='more-info-script.html'>
|
||||||
<link rel='import' href='more-info-light.html'>
|
<link rel='import' href='more-info-light.html'>
|
||||||
<link rel='import' href='more-info-media_player.html'>
|
<link rel='import' href='more-info-media_player.html'>
|
||||||
|
<link rel='import' href='more-info-camera.html'>
|
||||||
|
|
||||||
<dom-module id='more-info-content'>
|
<dom-module id='more-info-content'>
|
||||||
<style>
|
<style>
|
||||||
|
@ -33,6 +34,7 @@
|
||||||
dialogOpen: {
|
dialogOpen: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
value: false,
|
value: false,
|
||||||
|
observer: 'dialogOpenChanged',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
(function() {
|
(function() {
|
||||||
var DOMAINS_WITH_CARD = ['thermostat', 'configurator', 'scene', 'media_player'];
|
var DOMAINS_WITH_CARD = ['thermostat', 'configurator', 'scene', 'media_player'];
|
||||||
var DOMAINS_WITH_MORE_INFO = [
|
var DOMAINS_WITH_MORE_INFO = [
|
||||||
'light', 'group', 'sun', 'configurator', 'thermostat', 'script', 'media_player'
|
'light', 'group', 'sun', 'configurator', 'thermostat', 'script', 'media_player', 'camera'
|
||||||
];
|
];
|
||||||
var DOMAINS_HIDE_MORE_INFO = [
|
var DOMAINS_HIDE_MORE_INFO = [
|
||||||
'sensor',
|
'sensor',
|
||||||
|
|
Loading…
Add table
Reference in a new issue