Skip to content

Commit

Permalink
Merge pull request #28 from custom-cards/feature/no-web-sockets
Browse files Browse the repository at this point in the history
#22 Use img tags for getting camera thumbs, store more meta on camera…
  • Loading branch information
niemyjski authored Jul 3, 2020
2 parents 62b98fc + 1524fcb commit dae5a8f
Showing 1 changed file with 175 additions and 104 deletions.
279 changes: 175 additions & 104 deletions surveillance-card.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,180 @@
import {
LitElement, html
} from 'https://unpkg.com/[email protected]/lit-element.js?module';
css,
html,
LitElement,
} from "https://unpkg.com/[email protected]/lit-element.js?module";

class SurveillanceCard extends LitElement {
/* eslint-disable indent,object-curly-newline */
render() {
const accessToken = this.currentCamera && this._hass.states[this.currentCamera] && this._hass.states[this.currentCamera].attributes.access_token;
if (!this.cameras) {
return html`<div class="loading">Loading Cameras...</div>`;
}

return html`
<style>
<div class="container">
<div class="thumbs">
${this.cameras.filter((c) => c.access_token).map((camera) => {
let thumbClass = camera.has_motion ? "thumb motion" : "thumb";
return html`
<div class="${thumbClass}" @click="${() => this._updateSelectedCamera(camera)}">
<img src="${camera.url}" alt="${camera.name}" />
</div>
`;
})}
</div>
<div class="mainImage">
<img src="${this.selectedCamera.stream_url}" alt="${this.selectedCamera.name}" />
</div>
</div>
`;
}

static get properties() {
return {
hass: { type: Object },
cameras: { type: Array },
selectedCamera: { type: Object },
focusOnMotion: { type: Boolean },
thumbInterval: { type: Number },
updateInterval: { type: Number }
};
}

get hass() {
return this._hass;
}

set hass(hass) {
this._hass = hass;
this.updateCameras();
}

connectedCallback() {
super.connectedCallback();
this.thumbUpdater = setInterval(() => this.updateCameras(), this.thumbInterval);
}

disconnectedCallback() {
super.disconnectedCallback();
clearInterval(this.thumbUpdater);
}

setConfig(config) {
if (!config.cameras) {
throw new Error("You need to define cameras");
}

this.focusOnMotion = config.focus_motion !== false;
this.thumbInterval = (config.thumb_interval || 10.0) * 1000;
this.updateInterval = config.update_interval || 1.0;

const now = Date.now();
this.cameras = config.cameras.map((camera) => {
const entity = this.hass && hass.states[camera.entity];
const attributes = entity && entity.attributes;
return {
access_token: attributes && attributes.access_token,
entity: camera.entity,
motion_entity: camera.motion_entity,
name: attributes && attributes.friendly_name,
has_motion: this.hass && this.hass.states[camera.motion_entity].state === "on",
last_motion: now,
last_update: now,
stream_url: "",
url: attributes && attributes.entity_picture,
};
});
this.updateCameras = this.throttle(() => this._updateCameras(), this.thumbInterval);
this._updateSelectedCamera();
}

_updateCameras() {
const now = Date.now();
const { states } = this.hass;
const activatedCameras = [];

for (const camera of this.cameras) {
const hadMotion = camera.has_motion === true;
const { motion_entity } = camera;
camera.has_motion = motion_entity in states && states[motion_entity].state === "on";
if (camera.has_motion) {
camera.last_motion = now;
}

const motionActivated = !hadMotion && camera.has_motion;
if (motionActivated) {
activatedCameras.push(camera);
}

// update if there was just motion occurred or the thumb interval was reached.
if (motionActivated || now - camera.last_update > this.thumbInterval) {
camera.last_update = now;
}

camera.stream_url = `/api/camera_proxy_stream/${camera.entity}?token=${camera.access_token}&interval=${this.updateInterval}`;
const attributes = camera.entity in states ? states[camera.entity].attributes || {} : {};
camera.access_token = attributes.access_token;
camera.name = attributes.friendly_name;
camera.url = `${attributes.entity_picture}&last_update=${camera.last_update}`;
}

if (this.focusOnMotion && activatedCameras.length > 0) {
this._updateSelectedCamera(activatedCameras.find((c) => c === this.selectedCamera) || activatedCameras[0]);
}

this.cameras.sort(this._cameraSortComparer);
this.cameras = [...this.cameras];
}

_updateSelectedCamera(camera) {
if (!camera || !camera.access_token) {
let availableCameras = this.cameras.filter((c) => c.access_token && c.has_motion);
availableCameras.sort(this._cameraSortComparer);
camera = availableCameras[0] || this.cameras[0];
}

if (this.selectedCamera !== camera) {
this.selectedCamera = camera;
}
}

_cameraSortComparer(cameraA, cameraB) {
// prefer has_motion
if (cameraA.has_motion < cameraB.has_motion) {
return 1;
}

if (cameraA.has_motion === cameraB.has_motion) {
// prefer last_update
if (cameraA.last_update < cameraB.last_update) {
return 0;
}

return cameraA.last_update === cameraB.last_update ? 0 : -1;
}
}

static get styles() {
return css`
.container {
height: 100%;
width: 100%;
display: flex;
align-items: stretch;
position: absolute;
background: #000;
}
.thumbs {
flex: 1;
overflow-y: auto;
position:relative;
position: relative;
}
.thumb > img {
width: 100%;
height: auto;
min-height: 22px;
border: 1px solid var(--primary-color);
}
Expand Down Expand Up @@ -60,116 +209,38 @@ class SurveillanceCard extends LitElement {
}
.loading {
color: #FFF;
text-align: center;
font-size: 1.2rem;
margin-top: 3rem;
}
</style>
<div class="container">
<div class="thumbs">
${this.imageSources ? this.cameras.map((camera) => {
const thumbClass = this.lastMotion && this.lastMotion === camera.motion ? 'thumb motion' : 'thumb';
const source = this.imageSources[camera.entity] || '';
return html`
<div class="${thumbClass}" @click="${() => { this.currentCamera = camera.entity; }}">
<img src="${source}" />
</div>
`;
}) : html`<div class="loading">Loading Cameras...</div>`}
</div>
<div class="mainImage">
<img src="${this.currentCamera ? `/api/camera_proxy_stream/${this.currentCamera}?token=${accessToken}&interval=${this.updateInterval}` : ''}" />
</div>
</div>
`;
}
/* eslint-enable indent,object-curly-newline */

static get properties() {
return {
_hass: { type: Object },
cameras: { type: Array },
currentCamera: { type: String },
imageSources: { type: Object },
lastMotion: { type: String },
thumbInterval: { type: Number },
updateInterval: { type: Number }
};
}

setConfig(config) {
this.cameras = config.cameras.map(c => ({
entity: c.entity,
motion: c.motion_entity
}));
this.currentCamera = this.cameras[0].entity;
this.thumbInterval = (config.thumb_interval || 10) * 1000;
this.updateInterval = config.update_interval || 1;
this.focusMotion = config.focus_motion !== false;
}
throttle(callback, delay) {
let isThrottled = false,
args,
context;

set hass(hass) {
this._hass = hass;

for (const camera of this.cameras) {
const { motion } = camera;
if ((motion in hass.states) && hass.states[motion].state === 'on') {
if (this.focusMotion && this.lastMotion !== motion) {
this.currentCamera = camera.entity;
}
this.lastMotion = motion;
function wrapper() {
if (isThrottled) {
args = arguments;
context = this;
return;
}
}
this.lastMotion = null;
}

connectedCallback() {
super.connectedCallback();
this.thumbUpdater = setInterval(() => this._updateThumbs(), this.thumbInterval);
}
isThrottled = true;
callback.apply(this, arguments);

disconnectedCallback() {
super.disconnectedCallback();
clearInterval(this.thumbUpdater);
this.imageSources = null;
this.currentCamera = '';
}

_firstRendered() {
this._updateThumbs();
}

async _updateCameraImageSrc(entity) {
try {
const { content_type: contentType, content } = await this._hass.callWS({
type: 'camera_thumbnail',
entity_id: entity,
});

return {
entityId: entity,
src: `data:${contentType};base64, ${content}`
};
} catch (err) {
return {
entityId: entity,
src: null
};
setTimeout(() => {
isThrottled = false;
if (args) {
wrapper.apply(context, args);
args = context = null;
}
}, delay);
}
}

_updateThumbs() {
const sources = {};
const promises = this.cameras.map(camera => this._updateCameraImageSrc(camera.entity));
Promise.all(promises).then((vals) => {
this.cameras.forEach((camera) => {
const target = vals.find(val => val.entityId === camera.entity);
sources[camera.entity] = target.src;
});
this.imageSources = sources;
});
return wrapper;
}
}
customElements.define('surveillance-card', SurveillanceCard);
customElements.define("surveillance-card", SurveillanceCard);

0 comments on commit dae5a8f

Please sign in to comment.