From abb9190c98237412ae1a8ac5a24d58513f3421b5 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 4 Sep 2019 14:21:03 +0200 Subject: [PATCH] 20190904.0 (#3613) * Alarm codes (#3566) * Handle alarm codes from keyboard input Closes https://github.com/home-assistant/home-assistant-polymer/issues/2602 * remove friendly_name changes * remove unnecessary TS check * Update azure-pipelines-release.yml for Azure Pipelines * Don't remove `hvac_action` from history attributes (#3570) So it can be used to plot a fill when active in the graph. * Update the map when making config changes (#3568) * Add haptic feedback to handle click (#3569) * Filter camera service entities (#3583) Closes https://github.com/home-assistant/home-assistant-polymer/issues/3582 * Notification drawer RTL support (#3580) * add exceptional icon (#3572) * Add options to badges (#3552) * Add options to badges name icon entity_picture * lint * lint * rename entityPicture to image * Align styling cast buttons (#3579) * Align styling cast buttons * Split dev constants * Ignore dev_const * Update README.md * Move lovelace background settings to theme (#3561) * Move lovelace background settings to theme While being backwards compatible * Also update cast * Don't allow overwrite of english lang (#3590) * Update hui-card-options.ts (#3591) * Fix display of no triggers text if no device is selected or device has no triggers (#3592) * Fix timing issue in external auth (#3587) * Fix timing issue in external auth * add await 0 * Show toast on successfull save (#3576) * Show toast on successfull save We need to make a list of places where this could benefit the user experience. * Helper method * Rename * handle unavailable lights (#3549) * handle unavailable lights * unavailable overlay * extract unavailable overlay * Option to display last changed in glance-card (#3584) * Option to display last changed in glance-card Closes https://github.com/home-assistant/ui-schema/issues/110 * move show_last_changed to entity-level * address review comments * Filter alerts in services (#3598) Closes https://github.com/home-assistant/home-assistant-polymer/issues/3597 * Add exceptional in weather to translations (#3599) * Add MQTT subscribe to dev tools (#3589) * Add mqtt subscribe to dev tools * Update mqtt-subscribe-card.ts * Comments * type * Wrap long attributes in more-info-default (#3601) Can likely be applied in many other places Closes https://github.com/home-assistant/home-assistant-polymer/issues/2811 * Bumped version to 20190904.0 (#3612) --- .gitignore | 3 + azure-pipelines-release.yml | 2 +- build-scripts/gulp/translations.js | 2 + cast/README.md | 2 +- cast/src/receiver/layout/hc-lovelace.ts | 12 +- setup.py | 2 +- src/cards/ha-weather-card.js | 1 + src/cast/cast_manager.ts | 3 +- src/cast/const.ts | 10 +- src/cast/dev_const.ts | 7 + src/cast/receiver_messages.ts | 3 +- src/common/dom/setup-leaflet-map.ts | 17 +- .../device/ha-device-trigger-picker.ts | 5 +- src/components/entity/ha-state-label-badge.ts | 16 +- src/components/ha-attributes.js | 1 + src/data/entity.ts | 2 + src/data/history.ts | 1 + src/data/mqtt.ts | 19 +++ .../more-info/controls/more-info-weather.js | 1 + .../notifications/notification-drawer.js | 2 +- src/external_app/external_auth.ts | 28 ++-- src/panels/config/js/trigger/device.js | 2 + src/panels/config/users/ha-user-editor.ts | 2 + .../event/event-subscribe-card.ts | 6 +- .../ha-panel-developer-tools.ts | 1 - .../mqtt/developer-tools-mqtt.js | 76 --------- .../mqtt/developer-tools-mqtt.ts | 126 +++++++++++++++ .../mqtt/mqtt-subscribe-card.ts | 153 ++++++++++++++++++ .../lovelace/cards/hui-alarm-panel-card.ts | 10 +- src/panels/lovelace/cards/hui-glance-card.ts | 26 +-- src/panels/lovelace/cards/hui-light-card.ts | 8 + src/panels/lovelace/cards/hui-map-card.ts | 27 ++++ src/panels/lovelace/cards/types.ts | 4 + src/panels/lovelace/common/handle-click.ts | 3 + .../lovelace/components/hui-card-options.ts | 9 +- .../lovelace/components/hui-unavailable.ts | 55 +++++++ src/panels/lovelace/entity-rows/types.ts | 1 + src/panels/lovelace/hui-root.ts | 16 +- src/panels/lovelace/hui-unused-entities.ts | 3 + src/panels/lovelace/hui-view.ts | 9 +- .../lovelace/special-rows/hui-cast-row.ts | 15 +- src/translations/en.json | 4 +- src/util/toast-saved-success.ts | 7 + 43 files changed, 565 insertions(+), 137 deletions(-) create mode 100644 src/cast/dev_const.ts create mode 100644 src/data/mqtt.ts delete mode 100644 src/panels/developer-tools/mqtt/developer-tools-mqtt.js create mode 100644 src/panels/developer-tools/mqtt/developer-tools-mqtt.ts create mode 100644 src/panels/developer-tools/mqtt/mqtt-subscribe-card.ts create mode 100644 src/panels/lovelace/components/hui-unavailable.ts create mode 100644 src/util/toast-saved-success.ts diff --git a/.gitignore b/.gitignore index 743889ba4e17..8b568f884c1f 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,9 @@ dist .vscode/* !.vscode/extensions.json +# Cast dev settings +src/cast/dev_const.ts + # Secrets .lokalise_token yarn-error.log diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index f7b47f9813e8..5fceabc52a33 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -8,7 +8,7 @@ trigger: pr: none variables: - name: versionWheels - value: '1.1-3.7-alpine3.10' + value: '1.3-3.7-alpine3.10' - name: versionNode value: '12.1' - group: twine diff --git a/build-scripts/gulp/translations.js b/build-scripts/gulp/translations.js index 014df9f01819..63932c0b917c 100755 --- a/build-scripts/gulp/translations.js +++ b/build-scripts/gulp/translations.js @@ -214,6 +214,8 @@ gulp.task( const lang = subtags.slice(0, i).join("-"); if (lang === "test") { src.push(workDir + "/test.json"); + } else if (lang === "en") { + src.push("src/translations/en.json"); } else { src.push(inDir + "/" + lang + ".json"); } diff --git a/cast/README.md b/cast/README.md index aaf314175917..c1741fc989ee 100644 --- a/cast/README.md +++ b/cast/README.md @@ -25,7 +25,7 @@ Home Assistant Cast is made up of two separate applications: ### Setting dev variables -Open `src/cast/const.ts` and change `CAST_DEV` to `true` and `CAST_DEV_APP_ID` to the ID of the app you just created. +Open `src/cast/dev_const.ts` and change `CAST_DEV_APP_ID` to the ID of the app you just created. And set the `CAST_DEV_HASS_URL` to the url of you development machine. ### Changing configuration diff --git a/cast/src/receiver/layout/hc-lovelace.ts b/cast/src/receiver/layout/hc-lovelace.ts index 0be9488a54c9..3c52475fd6be 100644 --- a/cast/src/receiver/layout/hc-lovelace.ts +++ b/cast/src/receiver/layout/hc-lovelace.ts @@ -57,10 +57,16 @@ class HcLovelace extends LitElement { const index = this._viewIndex; if (index !== undefined) { - this.shadowRoot!.querySelector("hui-view")!.style.background = + const configBackground = this.lovelaceConfig.views[index].background || - this.lovelaceConfig.background || - ""; + this.lovelaceConfig.background; + + if (configBackground) { + this.shadowRoot!.querySelector("hui-view")!.style.setProperty( + "--lovelace-background", + configBackground + ); + } } } } diff --git a/setup.py b/setup.py index f2bf44985c11..c53a5cd5a5cb 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name="home-assistant-frontend", - version="20190901.0", + version="20190904.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/home-assistant-polymer", author="The Home Assistant Authors", diff --git a/src/cards/ha-weather-card.js b/src/cards/ha-weather-card.js index 3a86d465e1e9..52caa5afaec0 100644 --- a/src/cards/ha-weather-card.js +++ b/src/cards/ha-weather-card.js @@ -281,6 +281,7 @@ class HaWeatherCard extends LocalizeMixin(EventsMixin(PolymerElement)) { this.weatherIcons = { "clear-night": "hass:weather-night", cloudy: "hass:weather-cloudy", + exceptional: "hass:alert-circle-outline", fog: "hass:weather-fog", hail: "hass:weather-hail", lightning: "hass:weather-lightning", diff --git a/src/cast/cast_manager.ts b/src/cast/cast_manager.ts index 87404d38418e..6c9ea0a8e19f 100644 --- a/src/cast/cast_manager.ts +++ b/src/cast/cast_manager.ts @@ -1,5 +1,6 @@ import { castApiAvailable } from "./cast_framework"; -import { CAST_APP_ID, CAST_NS, CAST_DEV_HASS_URL, CAST_DEV } from "./const"; +import { CAST_APP_ID, CAST_NS, CAST_DEV } from "./const"; +import { CAST_DEV_HASS_URL } from "./dev_const"; import { castSendAuth, HassMessage as ReceiverMessage, diff --git a/src/cast/const.ts b/src/cast/const.ts index 784c159a721f..fe578e8ad15a 100644 --- a/src/cast/const.ts +++ b/src/cast/const.ts @@ -1,11 +1,7 @@ +import { CAST_DEV_APP_ID } from "./dev_const"; + // Guard dev mode with `__dev__` so it can only ever be enabled in dev mode. export const CAST_DEV = __DEV__ && true; -// Replace this with your own unpublished cast app that points at your local dev -const CAST_DEV_APP_ID = "5FE44367"; + export const CAST_APP_ID = CAST_DEV ? CAST_DEV_APP_ID : "B12CE3CA"; export const CAST_NS = "urn:x-cast:com.nabucasa.hast"; - -// Chromecast SDK will only load on localhost and HTTPS -// So during local development we have to send our dev IP address, -// but then run the UI on localhost. -export const CAST_DEV_HASS_URL = "http://192.168.1.234:8123"; diff --git a/src/cast/dev_const.ts b/src/cast/dev_const.ts new file mode 100644 index 000000000000..0e87495ce8f4 --- /dev/null +++ b/src/cast/dev_const.ts @@ -0,0 +1,7 @@ +// Replace this with your own unpublished cast app that points at your local dev +export const CAST_DEV_APP_ID = "5FE44367"; + +// Chromecast SDK will only load on localhost and HTTPS +// So during local development we have to send our dev IP address, +// but then run the UI on localhost. +export const CAST_DEV_HASS_URL = "http://192.168.1.234:8123"; diff --git a/src/cast/receiver_messages.ts b/src/cast/receiver_messages.ts index 529d5e4e701e..b3d96c712b96 100644 --- a/src/cast/receiver_messages.ts +++ b/src/cast/receiver_messages.ts @@ -4,7 +4,8 @@ import { Auth } from "home-assistant-js-websocket"; import { CastManager } from "./cast_manager"; import { BaseCastMessage } from "./types"; -import { CAST_DEV_HASS_URL, CAST_DEV } from "./const"; +import { CAST_DEV } from "./const"; +import { CAST_DEV_HASS_URL } from "./dev_const"; export interface GetStatusMessage extends BaseCastMessage { type: "get_status"; diff --git a/src/common/dom/setup-leaflet-map.ts b/src/common/dom/setup-leaflet-map.ts index bbbb03a1045f..d0532721a626 100644 --- a/src/common/dom/setup-leaflet-map.ts +++ b/src/common/dom/setup-leaflet-map.ts @@ -20,10 +20,19 @@ export const setupLeafletMap = async ( style.setAttribute("rel", "stylesheet"); mapElement.parentNode.appendChild(style); map.setView([52.3731339, 4.8903147], 13); - Leaflet.tileLayer( + createTileLayer(Leaflet, darkMode).addTo(map); + + return [map, Leaflet]; +}; + +export const createTileLayer = ( + leaflet: LeafletModuleType, + darkMode: boolean +) => { + return leaflet.tileLayer( `https://{s}.basemaps.cartocdn.com/${ darkMode ? "dark_all" : "light_all" - }/{z}/{x}/{y}${Leaflet.Browser.retina ? "@2x.png" : ".png"}`, + }/{z}/{x}/{y}${leaflet.Browser.retina ? "@2x.png" : ".png"}`, { attribution: '© OpenStreetMap, © CARTO', @@ -31,7 +40,5 @@ export const setupLeafletMap = async ( minZoom: 0, maxZoom: 20, } - ).addTo(map); - - return [map, Leaflet]; + ); }; diff --git a/src/components/device/ha-device-trigger-picker.ts b/src/components/device/ha-device-trigger-picker.ts index 7ff3b6d48d4d..6330ae76d5d1 100644 --- a/src/components/device/ha-device-trigger-picker.ts +++ b/src/components/device/ha-device-trigger-picker.ts @@ -37,7 +37,10 @@ class HaDeviceTriggerPicker extends LitElement { @property() private _renderEmpty = false; private get _key() { - if (!this.value) { + if ( + !this.value || + deviceAutomationTriggersEqual(this._noTrigger, this.value) + ) { return NO_TRIGGER_KEY; } diff --git a/src/components/entity/ha-state-label-badge.ts b/src/components/entity/ha-state-label-badge.ts index 8f87caca18c4..0361adfcfd0b 100644 --- a/src/components/entity/ha-state-label-badge.ts +++ b/src/components/entity/ha-state-label-badge.ts @@ -29,6 +29,12 @@ export class HaStateLabelBadge extends LitElement { @property() public state?: HassEntity; + @property() public name?: string; + + @property() public icon?: string; + + @property() public image?: string; + @property() private _timerTimeRemaining?: number; private _connected?: boolean; @@ -72,10 +78,14 @@ export class HaStateLabelBadge extends LitElement { "has-unit_of_measurement": "unit_of_measurement" in state.attributes, })}" .value="${this._computeValue(domain, state)}" - .icon="${this._computeIcon(domain, state)}" - .image="${state.attributes.entity_picture}" + .icon="${this.icon ? this.icon : this._computeIcon(domain, state)}" + .image="${this.icon + ? "" + : this.image + ? this.image + : state.attributes.entity_picture}" .label="${this._computeLabel(domain, state, this._timerTimeRemaining)}" - .description="${computeStateName(state)}" + .description="${this.name ? this.name : computeStateName(state)}" > `; } diff --git a/src/components/ha-attributes.js b/src/components/ha-attributes.js index ae57a454b00e..1a1836c2f271 100644 --- a/src/components/ha-attributes.js +++ b/src/components/ha-attributes.js @@ -11,6 +11,7 @@ class HaAttributes extends PolymerElement { - +
[[localize('ui.notification_drawer.title')]]
diff --git a/src/external_app/external_auth.ts b/src/external_app/external_auth.ts index e69a252837ae..a7ae64f89172 100644 --- a/src/external_app/external_auth.ts +++ b/src/external_app/external_auth.ts @@ -63,6 +63,15 @@ class ExternalAuth extends Auth { public async refreshAccessToken() { const callbackPayload = { callback: CALLBACK_SET_TOKEN }; + const callbackPromise = new Promise( + (resolve, reject) => { + window[CALLBACK_SET_TOKEN] = (success, data) => + success ? resolve(data) : reject(data); + } + ); + + await 0; + if (window.externalApp) { window.externalApp.getExternalAuth(JSON.stringify(callbackPayload)); } else { @@ -71,12 +80,7 @@ class ExternalAuth extends Auth { ); } - const tokens = await new Promise( - (resolve, reject) => { - window[CALLBACK_SET_TOKEN] = (success, data) => - success ? resolve(data) : reject(data); - } - ); + const tokens = await callbackPromise; this.data.access_token = tokens.access_token; this.data.expires = tokens.expires_in * 1000 + Date.now(); @@ -85,6 +89,13 @@ class ExternalAuth extends Auth { public async revoke() { const callbackPayload = { callback: CALLBACK_REVOKE_TOKEN }; + const callbackPromise = new Promise((resolve, reject) => { + window[CALLBACK_REVOKE_TOKEN] = (success, data) => + success ? resolve(data) : reject(data); + }); + + await 0; + if (window.externalApp) { window.externalApp.revokeExternalAuth(JSON.stringify(callbackPayload)); } else { @@ -93,10 +104,7 @@ class ExternalAuth extends Auth { ); } - await new Promise((resolve, reject) => { - window[CALLBACK_REVOKE_TOKEN] = (success, data) => - success ? resolve(data) : reject(data); - }); + await callbackPromise; } } diff --git a/src/panels/config/js/trigger/device.js b/src/panels/config/js/trigger/device.js index 504eabdb7a64..f5d857ab5743 100644 --- a/src/panels/config/js/trigger/device.js +++ b/src/panels/config/js/trigger/device.js @@ -49,4 +49,6 @@ export default class DeviceTrigger extends Component { DeviceTrigger.defaultConfig = { device_id: "", + domain: "", + entity_id: "", }; diff --git a/src/panels/config/users/ha-user-editor.ts b/src/panels/config/users/ha-user-editor.ts index eb4686f31e9d..66775e4247fb 100644 --- a/src/panels/config/users/ha-user-editor.ts +++ b/src/panels/config/users/ha-user-editor.ts @@ -23,6 +23,7 @@ import { SYSTEM_GROUP_ID_USER, SYSTEM_GROUP_ID_ADMIN, } from "../../../data/user"; +import { showSaveSuccessToast } from "../../../util/toast-saved-success"; declare global { interface HASSDomEvents { @@ -150,6 +151,7 @@ class HaUserEditor extends LitElement { await updateUser(this.hass!, this.user!.id, { group_ids: [newGroup], }); + showSaveSuccessToast(this, this.hass!); fireEvent(this, "reload-users"); } catch (err) { alert(`Group update failed: ${err.message}`); diff --git a/src/panels/developer-tools/event/event-subscribe-card.ts b/src/panels/developer-tools/event/event-subscribe-card.ts index 4b56290f8629..d1973b9ffe9c 100644 --- a/src/panels/developer-tools/event/event-subscribe-card.ts +++ b/src/panels/developer-tools/event/event-subscribe-card.ts @@ -18,9 +18,13 @@ import format_time from "../../../common/datetime/format_time"; @customElement("event-subscribe-card") class EventSubscribeCard extends LitElement { @property() public hass?: HomeAssistant; + @property() private _eventType = ""; + @property() private _subscribed?: () => void; + @property() private _events: Array<{ id: number; event: HassEvent }> = []; + private _eventCount = 0; public disconnectedCallback() { @@ -33,7 +37,7 @@ class EventSubscribeCard extends LitElement { protected render(): TemplateResult { return html` - +
- :host { - -ms-user-select: initial; - -webkit-user-select: initial; - -moz-user-select: initial; - } - - .content { - padding: 24px 0 32px; - max-width: 600px; - margin: 0 auto; - direction: ltr; - } - - mwc-button { - background-color: white; - } - - - - - - - -
- -
- - - -
-
- Publish -
-
-
- `; - } - - static get properties() { - return { - hass: Object, - topic: String, - payload: String, - }; - } - - _publish() { - this.hass.callService("mqtt", "publish", { - topic: this.topic, - payload_template: this.payload, - }); - } -} - -customElements.define("developer-tools-mqtt", HaPanelDevMqtt); diff --git a/src/panels/developer-tools/mqtt/developer-tools-mqtt.ts b/src/panels/developer-tools/mqtt/developer-tools-mqtt.ts new file mode 100644 index 000000000000..3320eb52c30a --- /dev/null +++ b/src/panels/developer-tools/mqtt/developer-tools-mqtt.ts @@ -0,0 +1,126 @@ +import { + LitElement, + customElement, + TemplateResult, + html, + property, + CSSResultArray, + css, +} from "lit-element"; +import "@material/mwc-button"; +import "@polymer/paper-input/paper-input"; +import "@polymer/paper-input/paper-textarea"; + +import { HomeAssistant } from "../../../types"; + +import { haStyle } from "../../../resources/styles"; +import "../../../components/ha-card"; +import "./mqtt-subscribe-card"; + +@customElement("developer-tools-mqtt") +class HaPanelDevMqtt extends LitElement { + @property() public hass?: HomeAssistant; + + @property() private topic = ""; + + @property() private payload = ""; + + private inited: boolean = false; + + protected firstUpdated() { + if (localStorage && localStorage["panel-dev-mqtt-topic"]) { + this.topic = localStorage["panel-dev-mqtt-topic"]; + } + if (localStorage && localStorage["panel-dev-mqtt-payload"]) { + this.payload = localStorage["panel-dev-mqtt-payload"]; + } + this.inited = true; + } + + protected render(): TemplateResult { + return html` +
+ +
+ + + +
+
+ Publish +
+
+ + +
+ `; + } + + private _handleTopic(ev: CustomEvent) { + this.topic = ev.detail.value; + if (localStorage && this.inited) { + localStorage["panel-dev-mqtt-topic"] = this.topic; + } + } + + private _handlePayload(ev: CustomEvent) { + this.payload = ev.detail.value; + if (localStorage && this.inited) { + localStorage["panel-dev-mqtt-payload"] = this.payload; + } + } + + private _publish(): void { + if (!this.hass) { + return; + } + this.hass.callService("mqtt", "publish", { + topic: this.topic, + payload_template: this.payload, + }); + } + + static get styles(): CSSResultArray { + return [ + haStyle, + css` + :host { + -ms-user-select: initial; + -webkit-user-select: initial; + -moz-user-select: initial; + } + + .content { + padding: 24px 0 32px; + max-width: 600px; + margin: 0 auto; + direction: ltr; + } + + mwc-button { + background-color: white; + } + + mqtt-subscribe-card { + display: block; + margin: 16px auto; + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "developer-tools-mqtt": HaPanelDevMqtt; + } +} diff --git a/src/panels/developer-tools/mqtt/mqtt-subscribe-card.ts b/src/panels/developer-tools/mqtt/mqtt-subscribe-card.ts new file mode 100644 index 000000000000..0795fcad305c --- /dev/null +++ b/src/panels/developer-tools/mqtt/mqtt-subscribe-card.ts @@ -0,0 +1,153 @@ +import { + LitElement, + customElement, + TemplateResult, + html, + property, + CSSResult, + css, +} from "lit-element"; +import "@material/mwc-button"; +import "@polymer/paper-input/paper-input"; +import { HomeAssistant } from "../../../types"; +import "../../../components/ha-card"; +import format_time from "../../../common/datetime/format_time"; + +import { subscribeMQTTTopic, MQTTMessage } from "../../../data/mqtt"; + +@customElement("mqtt-subscribe-card") +class MqttSubscribeCard extends LitElement { + @property() public hass?: HomeAssistant; + + @property() private _topic = ""; + + @property() private _subscribed?: () => void; + + @property() private _messages: Array<{ + id: number; + message: MQTTMessage; + payload: string; + time: Date; + }> = []; + + private _messageCount = 0; + + public disconnectedCallback() { + super.disconnectedCallback(); + if (this._subscribed) { + this._subscribed(); + this._subscribed = undefined; + } + } + + protected render(): TemplateResult { + return html` + + + + + ${this._subscribed ? "Stop listening" : "Start listening"} + + +
+ ${this._messages.map( + (msg) => html` +
+ Message ${msg.id} received on ${msg.message.topic} at + ${format_time(msg.time, this.hass!.language)}: +
${msg.payload}
+
+ QoS: ${msg.message.qos} - Retain: + ${Boolean(msg.message.retain)} +
+
+ ` + )} +
+
+ `; + } + + private _valueChanged(ev: CustomEvent): void { + this._topic = ev.detail.value; + } + + private async _handleSubmit(): Promise { + if (this._subscribed) { + this._subscribed(); + this._subscribed = undefined; + } else { + this._subscribed = await subscribeMQTTTopic( + this.hass!, + this._topic, + (message) => this._handleMessage(message) + ); + } + } + + private _handleMessage(message: MQTTMessage) { + const tail = + this._messages.length > 30 ? this._messages.slice(0, 29) : this._messages; + let payload: string; + try { + payload = JSON.stringify(JSON.parse(message.payload), null, 4); + } catch (e) { + payload = message.payload; + } + this._messages = [ + { + payload, + message, + time: new Date(), + id: this._messageCount++, + }, + ...tail, + ]; + } + + static get styles(): CSSResult { + return css` + form { + display: block; + padding: 16px; + } + paper-input { + display: inline-block; + width: 200px; + } + .events { + margin: -16px 0; + padding: 0 16px; + } + .event { + border-bottom: 1px solid var(--divider-color); + padding-bottom: 16px; + margin: 16px 0; + } + .event:last-child { + border-bottom: 0; + } + .bottom { + font-size: 80%; + color: var(--secondary-text-color); + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "mqtt-subscribe-card": MqttSubscribeCard; + } +} diff --git a/src/panels/lovelace/cards/hui-alarm-panel-card.ts b/src/panels/lovelace/cards/hui-alarm-panel-card.ts index 899a2c6eb95b..f9b3a2a1a1ec 100644 --- a/src/panels/lovelace/cards/hui-alarm-panel-card.ts +++ b/src/panels/lovelace/cards/hui-alarm-panel-card.ts @@ -21,6 +21,7 @@ import { FORMAT_NUMBER, } from "../../../data/alarm_control_panel"; import { AlarmPanelCardConfig } from "./types"; +import { PaperInputElement } from "@polymer/paper-input/paper-input"; const ICONS = { armed_away: "hass:shield-lock", @@ -144,6 +145,7 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard { ? html`` : html` 0 ? input.value : ""); callAlarmAction( this.hass!, this._config!.entity, (e.currentTarget! as any).action, - this._code! + code ); this._code = ""; } diff --git a/src/panels/lovelace/cards/hui-glance-card.ts b/src/panels/lovelace/cards/hui-glance-card.ts index 69dd4a319922..be7ee5d4f67d 100644 --- a/src/panels/lovelace/cards/hui-glance-card.ts +++ b/src/panels/lovelace/cards/hui-glance-card.ts @@ -13,6 +13,7 @@ import { classMap } from "lit-html/directives/class-map"; import computeStateDisplay from "../../../common/entity/compute_state_display"; import computeStateName from "../../../common/entity/compute_state_name"; import applyThemesOnElement from "../../../common/dom/apply_themes_on_element"; +import relativeTime from "../../../common/datetime/relative_time"; import "../../../components/entity/state-badge"; import "../../../components/ha-card"; @@ -24,7 +25,7 @@ import { LovelaceCard, LovelaceCardEditor } from "../types"; import { longPress } from "../common/directives/long-press-directive"; import { processConfigEntities } from "../common/process-config-entities"; import { handleClick } from "../common/handle-click"; -import { GlanceCardConfig, ConfigEntity } from "./types"; +import { GlanceCardConfig, GlanceConfigEntity } from "./types"; @customElement("hui-glance-card") export class HuiGlanceCard extends LitElement implements LovelaceCard { @@ -41,7 +42,7 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard { @property() private _config?: GlanceCardConfig; - private _configEntities?: ConfigEntity[]; + private _configEntities?: GlanceConfigEntity[]; public getCardSize(): number { return ( @@ -52,7 +53,7 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard { public setConfig(config: GlanceCardConfig): void { this._config = { theme: "default", ...config }; - const entities = processConfigEntities(config.entities); + const entities = processConfigEntities(config.entities); for (const entity of entities) { if ( @@ -207,11 +208,16 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard { ${this._config!.show_state !== false ? html`
- ${computeStateDisplay( - this.hass!.localize, - stateObj, - this.hass!.language - )} + ${entityConf.show_last_changed + ? relativeTime( + new Date(stateObj.last_changed), + this.hass!.localize + ) + : computeStateDisplay( + this.hass!.localize, + stateObj, + this.hass!.language + )}
` : ""} @@ -220,12 +226,12 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard { } private _handleTap(ev: MouseEvent): void { - const config = (ev.currentTarget as any).entityConf as ConfigEntity; + const config = (ev.currentTarget as any).entityConf as GlanceConfigEntity; handleClick(this, this.hass!, config, false); } private _handleHold(ev: MouseEvent): void { - const config = (ev.currentTarget as any).entityConf as ConfigEntity; + const config = (ev.currentTarget as any).entityConf as GlanceConfigEntity; handleClick(this, this.hass!, config, true); } } diff --git a/src/panels/lovelace/cards/hui-light-card.ts b/src/panels/lovelace/cards/hui-light-card.ts index 9618e4a3bf2f..a27c7709d89a 100644 --- a/src/panels/lovelace/cards/hui-light-card.ts +++ b/src/panels/lovelace/cards/hui-light-card.ts @@ -15,6 +15,7 @@ import applyThemesOnElement from "../../../common/dom/apply_themes_on_element"; import "../../../components/ha-card"; import "../../../components/ha-icon"; import "../components/hui-warning"; +import "../components/hui-unavailable"; import { fireEvent } from "../../../common/dom/fire_event"; import { styleMap } from "lit-html/directives/style-map"; @@ -94,6 +95,13 @@ export class HuiLightCard extends LitElement implements LovelaceCard { return html` ${this.renderStyle()} + ${stateObj.state === "unavailable" + ? html` + + ` + : ""} - ${this.hass!.localize( + + ${this.hass!.localize( "ui.panel.lovelace.editor.edit_card.move" )} - ${this.hass!.localize( + + ${this.hass!.localize( "ui.panel.lovelace.editor.edit_card.delete" )} diff --git a/src/panels/lovelace/components/hui-unavailable.ts b/src/panels/lovelace/components/hui-unavailable.ts new file mode 100644 index 000000000000..37ad96d77c51 --- /dev/null +++ b/src/panels/lovelace/components/hui-unavailable.ts @@ -0,0 +1,55 @@ +import { + html, + LitElement, + TemplateResult, + CSSResult, + css, + customElement, + property, +} from "lit-element"; + +@customElement("hui-unavailable") +export class HuiUnavailable extends LitElement { + @property() public text?: string; + + protected render(): TemplateResult | void { + return html` +
+
${this.text}
+
+ `; + } + + static get styles(): CSSResult { + return css` + .disabled-overlay { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: var(--state-icon-unavailable-color); + opacity: 0.5; + z-index: 50; + } + + .disabled-overlay div { + position: absolute; + top: 50%; + left: 50%; + font-size: 50px; + color: var(--primary-text-color); + transform: translate(-50%, -50%); + -ms-transform: translate(-50%, -50%); + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-unavailable": HuiUnavailable; + } +} diff --git a/src/panels/lovelace/entity-rows/types.ts b/src/panels/lovelace/entity-rows/types.ts index 19ba7b134f77..c827e6b349a1 100644 --- a/src/panels/lovelace/entity-rows/types.ts +++ b/src/panels/lovelace/entity-rows/types.ts @@ -5,6 +5,7 @@ export interface EntityConfig { type?: string; name?: string; icon?: string; + image?: string; } export interface DividerConfig { type: "divider"; diff --git a/src/panels/lovelace/hui-root.ts b/src/panels/lovelace/hui-root.ts index ce513e27bb29..778007026251 100644 --- a/src/panels/lovelace/hui-root.ts +++ b/src/panels/lovelace/hui-root.ts @@ -568,7 +568,12 @@ class HUIRoot extends LitElement { unusedEntities.hass = this.hass!; } ); - root.style.background = this.config.background || ""; + if (this.config.background) { + unusedEntities.style.setProperty( + "--lovelace-background", + this.config.background + ); + } root.append(unusedEntities); return; } @@ -597,8 +602,13 @@ class HUIRoot extends LitElement { } view.hass = this.hass; - root.style.background = - viewConfig.background || this.config.background || ""; + + const configBackground = viewConfig.background || this.config.background; + + if (configBackground) { + view.style.setProperty("--lovelace-background", configBackground); + } + root.append(view); } } diff --git a/src/panels/lovelace/hui-unused-entities.ts b/src/panels/lovelace/hui-unused-entities.ts index 949dd0f38a40..55ab49cf1e5d 100644 --- a/src/panels/lovelace/hui-unused-entities.ts +++ b/src/panels/lovelace/hui-unused-entities.ts @@ -56,6 +56,9 @@ export class HuiUnusedEntities extends LitElement { private renderStyle(): TemplateResult { return html`