@@ -114,7 +114,8 @@ class HuiPictureGlanceCard extends LocalizeMixin(EventsMixin(PolymerElement)) {
}
setConfig(config) {
- if (!config || !config.entities || !Array.isArray(config.entities) || !config.image) {
+ if (!config || !config.entities || !Array.isArray(config.entities) ||
+ !(config.image || config.camera_image)) {
throw new Error('Invalid card configuration');
}
diff --git a/src/panels/lovelace/common/is-valid-object.js b/src/panels/lovelace/common/is-valid-object.js
new file mode 100644
index 0000000000..3d76c19f74
--- /dev/null
+++ b/src/panels/lovelace/common/is-valid-object.js
@@ -0,0 +1,4 @@
+// Check if given obj is a JS object and optionally contains all required keys
+export default function isValidObject(obj, requiredKeys = []) {
+ return obj && typeof obj === 'object' && !Array.isArray(obj) && requiredKeys.every(k => k in obj);
+}
diff --git a/src/panels/lovelace/components/hui-image.js b/src/panels/lovelace/components/hui-image.js
new file mode 100644
index 0000000000..9ec876a68b
--- /dev/null
+++ b/src/panels/lovelace/components/hui-image.js
@@ -0,0 +1,143 @@
+import { html } from '@polymer/polymer/lib/utils/html-tag.js';
+import { PolymerElement } from '@polymer/polymer/polymer-element.js';
+import '@polymer/paper-toggle-button/paper-toggle-button.js';
+
+import { STATES_OFF } from '../../../common/const.js';
+import LocalizeMixin from '../../../mixins/localize-mixin.js';
+import isValidObject from '../common/is-valid-object';
+
+const UPDATE_INTERVAL = 10000;
+
+/*
+ * @appliesMixin LocalizeMixin
+ */
+class HuiImage extends LocalizeMixin(PolymerElement) {
+ static get template() {
+ return html`
+
+
+
+
+
+
+ [[localize('ui.card.camera.not_available')]]
+
+`;
+ }
+
+ static get properties() {
+ return {
+ hass: Object,
+ state: {
+ type: Object,
+ value: null,
+ observer: '_stateChanged'
+ },
+ image: String,
+ stateImage: Object,
+ cameraImage: String,
+ _error: {
+ type: Boolean,
+ value: false
+ },
+ _imageClass: String,
+ _imageSrc: String
+ };
+ }
+
+ static get observers() {
+ return ['_configChanged(image, stateImage, cameraImage)'];
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ if (this.cameraImage) {
+ this.timer = setInterval(() => this._updateCameraImageSrc(), UPDATE_INTERVAL);
+ }
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback();
+ clearInterval(this.timer);
+ }
+
+ _configChanged(image, stateImage, cameraImage) {
+ if (cameraImage) {
+ this._updateCameraImageSrc();
+ } else if (image && !stateImage) {
+ this._imageSrc = image;
+ }
+ }
+
+ _onImageError() {
+ this.setProperties({
+ _imageSrc: null,
+ _error: true
+ });
+ }
+
+ _onImageLoad() {
+ this._error = false;
+ }
+
+ _stateChanged(state) {
+ if (this.cameraImage) {
+ return;
+ }
+
+ if (!this.stateImage) {
+ this._imageClass = (!isValidObject(state, ['state']) || STATES_OFF.includes(state.state)) ? 'state-off' : '';
+ return;
+ }
+
+ const stateImg = isValidObject(state, ['state']) ? this.stateImage[state.state] : this.stateImage.offline;
+
+ this.setProperties({
+ _imageSrc: stateImg || this.stateImage.default || this.image,
+ _imageClass: ''
+ });
+ }
+
+ _updateCameraImageSrc() {
+ this.hass.connection.sendMessagePromise({
+ type: 'camera_thumbnail',
+ entity_id: this.cameraImage,
+ }).then((resp) => {
+ if (resp.success) {
+ this.setProperties({
+ _imageSrc: `data:${resp.result.content_type};base64, ${resp.result.content}`,
+ _error: false
+ });
+ } else {
+ this.setProperties({
+ _imageSrc: null,
+ _error: true
+ });
+ }
+ });
+ }
+}
+
+customElements.define('hui-image', HuiImage);