diff --git a/gallery/src/data/media_player_images/media_player.bedroom.jpg b/gallery/public/api/media_player_proxy/media_player.bedroom similarity index 100% rename from gallery/src/data/media_player_images/media_player.bedroom.jpg rename to gallery/public/api/media_player_proxy/media_player.bedroom diff --git a/gallery/src/data/media_player_images/media_player.living_room.jpg b/gallery/public/api/media_player_proxy/media_player.living_room similarity index 100% rename from gallery/src/data/media_player_images/media_player.living_room.jpg rename to gallery/public/api/media_player_proxy/media_player.living_room diff --git a/gallery/src/data/media_player_images/media_player.walkman.jpg b/gallery/public/api/media_player_proxy/media_player.walkman similarity index 100% rename from gallery/src/data/media_player_images/media_player.walkman.jpg rename to gallery/public/api/media_player_proxy/media_player.walkman diff --git a/gallery/src/components/demo-card.js b/gallery/src/components/demo-card.js index 186584908f..83b0c51f4d 100644 --- a/gallery/src/components/demo-card.js +++ b/gallery/src/components/demo-card.js @@ -48,6 +48,10 @@ class DemoCard extends PolymerElement { static get properties() { return { + hass: { + type: Object, + observer: '_hassChanged', + }, config: { type: Object, observer: '_configChanged' @@ -62,17 +66,27 @@ class DemoCard extends PolymerElement { card.removeChild(card.lastChild); } - const hass = new HomeAssistant(); - hass.config = demoConfig; - hass.resources = demoResources; - hass.language = 'en'; - hass.states = demoStates; - const el = createCardElement(JsYaml.safeLoad(config.config)[0]); - el.hass = hass; + + if (this.hass) { + el.hass = this.hass; + } else { + const hass = new HomeAssistant(demoStates); + hass.config = demoConfig; + hass.resources = demoResources; + hass.language = 'en'; + hass.states = demoStates; + el.hass = hass; + } + card.appendChild(el); } + _hassChanged(hass) { + const card = this.$.card.lastChild; + if (card) card.hass = hass; + } + _trim(config) { return config.trim(); } diff --git a/gallery/src/components/demo-cards.js b/gallery/src/components/demo-cards.js index e71c2cf8e2..f4d6edf755 100644 --- a/gallery/src/components/demo-cards.js +++ b/gallery/src/components/demo-cards.js @@ -36,6 +36,7 @@ class DemoCards extends PolymerElement { @@ -45,6 +46,7 @@ class DemoCards extends PolymerElement { static get properties() { return { configs: Object, + hass: Object, showConfig: { type: Boolean, value: false, diff --git a/gallery/src/data/entity.js b/gallery/src/data/entity.js new file mode 100644 index 0000000000..756354e0fa --- /dev/null +++ b/gallery/src/data/entity.js @@ -0,0 +1,116 @@ +const now = () => new Date().toISOString(); +const randomTime = () => + new Date(new Date().getTime() - (Math.random() * 80 * 60 * 1000)).toISOString(); + +/* eslint-disable no-unused-vars */ + +export class Entity { + constructor(domain, objectId, state, baseAttributes) { + this.domain = domain; + this.objectId = objectId; + this.entityId = `${domain}.${objectId}`; + this.lastChanged = randomTime(); + this.lastUpdated = randomTime(); + this.state = state; + // These are the attributes that we always write to the state machine + this.baseAttributes = baseAttributes; + this.attributes = baseAttributes; + } + + async handleService(domain, service, data) { + console.log(`Unmocked service for ${this.entityId}: ${domain}/${service}`, data); + } + + update(state, attributes = {}) { + this.state = state; + this.lastUpdated = now(); + this.lastChanged = state === this.state ? this.lastChanged : this.lastUpdated; + this.attributes = Object.assign({}, this.baseAttributes, attributes); + + console.log('update', this.entityId, this); + + this.hass.updateStates({ + [this.entityId]: this.toState() + }); + } + + toState() { + return { + entity_id: this.entityId, + state: this.state, + attributes: this.attributes, + last_changed: this.lastChanged, + last_updated: this.lastUpdated, + }; + } +} + +export class LightEntity extends Entity { + async handleService(domain, service, data) { + if (!['homeassistant', this.domain].includes(domain)) return; + + if (service === 'turn_on') { + // eslint-disable-next-line + const { brightness, hs_color } = data; + this.update('on', Object.assign(this.attributes, { + brightness, + hs_color, + })); + } else if (service === 'turn_off') { + this.update('off'); + } else if (service === 'toggle') { + if (this.state === 'on') { + this.handleService(domain, 'turn_off', data); + } else { + this.handleService(domain, 'turn_on', data); + } + } + } +} + +export class LockEntity extends Entity { + async handleService(domain, service, data) { + if (domain !== this.domain) return; + + if (service === 'lock') { + this.update('locked'); + } else if (service === 'unlock') { + this.update('unlocked'); + } + } +} + +export class CoverEntity extends Entity { + async handleService(domain, service, data) { + if (domain !== this.domain) return; + + if (service === 'open_cover') { + this.update('open'); + } else if (service === 'close_cover') { + this.update('closing'); + } + } +} + +export class GroupEntity extends Entity { + async handleService(domain, service, data) { + if (!['homeassistant', this.domain].includes(domain)) return; + + await Promise.all(this.attributes.entity_id.map((ent) => { + const entity = this.hass.mockEntities[ent]; + return entity.handleService(entity.domain, service, data); + })); + + this.update(service === 'turn_on' ? 'on' : 'off'); + } +} + +const TYPES = { + light: LightEntity, + lock: LockEntity, + cover: CoverEntity, + group: GroupEntity, +}; + +export default (domain, objectId, state, baseAttributes = {}) => + new (TYPES[domain] || Entity)(domain, objectId, state, baseAttributes); diff --git a/gallery/src/data/hass.js b/gallery/src/data/hass.js index 59179d6637..918132af4c 100644 --- a/gallery/src/data/hass.js +++ b/gallery/src/data/hass.js @@ -1,6 +1,6 @@ export default class FakeHass { - constructor() { - this.states = {}; + constructor(states = {}) { + this.states = states; this._wsCommands = {}; } diff --git a/gallery/src/data/provide_hass.js b/gallery/src/data/provide_hass.js new file mode 100644 index 0000000000..8ebadb022b --- /dev/null +++ b/gallery/src/data/provide_hass.js @@ -0,0 +1,79 @@ +import fireEvent from '../../../src/common/dom/fire_event.js'; + +import demoConfig from './demo_config.js'; +import demoResources from './demo_resources.js'; + +const ensureArray = val => (Array.isArray(val) ? val : [val]); + +export default (elements, { initialStates = {} } = {}) => { + elements = ensureArray(elements); + + const wsCommands = {}; + let hass; + const entities = {}; + + function updateHass(obj) { + hass = Object.assign({}, hass, obj); + elements.forEach((el) => { el.hass = hass; }); + } + + updateHass({ + // Home Assistant properties + config: demoConfig, + language: 'en', + resources: demoResources, + states: initialStates, + + // Mock properties + mockEntities: entities, + + // Home Assistant functions + async callService(domain, service, data) { + fireEvent(elements[0], 'show-notification', { message: `Called service ${domain}/${service}` }); + if (data.entity_id) { + await Promise.all(ensureArray(data.entity_id).map(ent => + entities[ent].handleService(domain, service, data))); + } else { + console.log('unmocked callService', domain, service, data); + } + }, + + async callWS(msg) { + const callback = wsCommands[msg.type]; + return callback ? callback(msg) : Promise.reject({ + code: 'command_not_mocked', + message: 'This command is not implemented in the gallery.', + }); + }, + + async sendWS(msg) { + const callback = wsCommands[msg.type]; + + if (callback) { + callback(msg); + } else { + console.error(`Unknown command: ${msg.type}`); + } + console.log('sendWS', msg); + }, + + // Mock functions + updateHass, + updateStates(newStates) { + updateHass({ + states: Object.assign({}, hass.states, newStates), + }); + }, + addEntities(newEntities) { + const states = {}; + ensureArray(newEntities).forEach((ent) => { + ent.hass = hass; + entities[ent.entityId] = ent; + states[ent.entityId] = ent.toState(); + }); + this.updateStates(states); + } + }); + + return hass; +}; diff --git a/gallery/src/demos/demo-hui-entities-card.js b/gallery/src/demos/demo-hui-entities-card.js index bd4d73cdc1..f473b01bb9 100644 --- a/gallery/src/demos/demo-hui-entities-card.js +++ b/gallery/src/demos/demo-hui-entities-card.js @@ -1,8 +1,45 @@ import { html } from '@polymer/polymer/lib/utils/html-tag.js'; import { PolymerElement } from '@polymer/polymer/polymer-element.js'; +import getEntity from '../data/entity.js'; +import provideHass from '../data/provide_hass.js'; import '../components/demo-cards.js'; +const ENTITIES = [ + getEntity('light', 'bed_light', 'on', { + friendly_name: 'Bed Light' + }), + getEntity('group', 'kitchen', 'on', { + entity_id: [ + 'light.bed_light', + ], + order: 8, + friendly_name: 'Kitchen' + }), + getEntity('lock', 'kitchen_door', 'locked', { + friendly_name: 'Kitchen Door' + }), + getEntity('cover', 'kitchen_window', 'open', { + friendly_name: 'Kitchen Window', + supported_features: 11 + }), + getEntity('scene', 'romantic_lights', 'scening', { + entity_id: [ + 'light.bed_light', + 'light.ceiling_lights' + ], + friendly_name: 'Romantic lights' + }), + getEntity('device_tracker', 'demo_paulus', 'home', { + source_type: 'gps', + latitude: 32.877105, + longitude: 117.232185, + gps_accuracy: 91, + battery: 71, + friendly_name: 'Paulus' + }), +]; + const CONFIGS = [ { heading: 'Basic', @@ -48,7 +85,7 @@ const CONFIGS = [ ` }, { - heading: 'With title, cant\'t toggle', + heading: 'With title, can\'t toggle', config: ` - type: entities entities: @@ -80,7 +117,11 @@ const CONFIGS = [ class DemoEntities extends PolymerElement { static get template() { return html` - + `; } @@ -89,9 +130,16 @@ class DemoEntities extends PolymerElement { _configs: { type: Object, value: CONFIGS - } + }, + hass: Object, }; } + + ready() { + super.ready(); + const hass = provideHass(this.$.demos); + hass.addEntities(ENTITIES); + } } customElements.define('demo-hui-entities-card', DemoEntities); diff --git a/gallery/src/ha-gallery.js b/gallery/src/ha-gallery.js index fbcd9fc943..d09dd5e0d7 100644 --- a/gallery/src/ha-gallery.js +++ b/gallery/src/ha-gallery.js @@ -9,6 +9,8 @@ import '@polymer/paper-icon-button/paper-icon-button.js'; import { html } from '@polymer/polymer/lib/utils/html-tag.js'; import { PolymerElement } from '@polymer/polymer/polymer-element.js'; +import '../../src/managers/notification-manager.js'; + const demos = require.context('./demos', true, /^(.*\.(js$))[^.]*$/im); const fixPath = path => path.substr(2, path.length - 5); @@ -91,6 +93,7 @@ class HaGallery extends PolymerElement { + `; } @@ -111,8 +114,15 @@ class HaGallery extends PolymerElement { ready() { super.ready(); + this.addEventListener( + 'show-notification', + ev => this.$.notifications.showNotification(ev.detail.message) + ); + this.addEventListener('hass-more-info', (ev) => { - if (ev.detail.entityId) alert(`Showing more info for ${ev.detail.entityId}`); + if (ev.detail.entityId) { + this.$.notifications.showNotification(`Showing more info for ${ev.detail.entityId}`); + } }); window.addEventListener('hashchange', () => { this._demo = document.location.hash.substr(1); }); diff --git a/gallery/webpack.config.js b/gallery/webpack.config.js index 91f782c155..31ced8cfcb 100644 --- a/gallery/webpack.config.js +++ b/gallery/webpack.config.js @@ -49,12 +49,11 @@ module.exports = { plugins: [ new CopyWebpackPlugin([ 'public', + { from: '../public', to: 'static' }, + { from: '../build-translations/output', to: 'static/translations' }, { from: '../node_modules/leaflet/dist/leaflet.css', to: 'static/images/leaflet/' }, { from: '../node_modules/@polymer/font-roboto-local/fonts', to: 'static/fonts' }, { from: '../node_modules/leaflet/dist/images', to: 'static/images/leaflet/' }, - { from: './src/data/media_player_images/media_player.bedroom.jpg', to: 'api/media_player_proxy/media_player.bedroom' }, - { from: './src/data/media_player_images/media_player.living_room.jpg', to: 'api/media_player_proxy/media_player.living_room' }, - { from: './src/data/media_player_images/media_player.walkman.jpg', to: 'api/media_player_proxy/media_player.walkman' }, ]), isProd && new UglifyJsPlugin({ extractComments: true, diff --git a/src/managers/notification-manager.js b/src/managers/notification-manager.js index c55eff9347..a3820862df 100644 --- a/src/managers/notification-manager.js +++ b/src/managers/notification-manager.js @@ -13,7 +13,7 @@ class NotificationManager extends LocalizeMixin(PolymerElement) { } - + `; } @@ -40,11 +40,6 @@ class NotificationManager extends LocalizeMixin(PolymerElement) { value: false, }, - _text: { - type: String, - readOnly: true, - }, - toastClass: { type: String, value: '', @@ -90,8 +85,7 @@ class NotificationManager extends LocalizeMixin(PolymerElement) { } showNotification(message) { - this._set_text(message); - this.$.toast.show(); + this.$.toast.show(message); } }