diff --git a/build-scripts/gulp/translations.js b/build-scripts/gulp/translations.js index 63932c0b91..af6c64eb7f 100755 --- a/build-scripts/gulp/translations.js +++ b/build-scripts/gulp/translations.js @@ -214,9 +214,7 @@ 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 { + } else if (lang !== "en") { src.push(inDir + "/" + lang + ".json"); } } diff --git a/package.json b/package.json index 25afbc9b9c..6b95d1adfb 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "@material/mwc-base": "^0.6.0", "@material/mwc-button": "^0.6.0", "@material/mwc-ripple": "^0.6.0", - "@mdi/svg": "4.2.95", + "@mdi/svg": "4.3.95", "@polymer/app-layout": "^3.0.2", "@polymer/app-localize-behavior": "^3.0.1", "@polymer/app-route": "^3.0.2", @@ -81,7 +81,7 @@ "home-assistant-js-websocket": "4.3.1", "intl-messageformat": "^2.2.0", "jquery": "^3.3.1", - "js-yaml": "^3.13.0", + "js-yaml": "^3.13.1", "leaflet": "^1.4.0", "lit-element": "^2.2.0", "lit-html": "^1.1.0", diff --git a/setup.py b/setup.py index c53a5cd5a5..8738659a58 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20190904.0", + version="20190908.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/home-assistant-polymer", author="The Home Assistant Authors", diff --git a/src/auth/ha-auth-flow.ts b/src/auth/ha-auth-flow.ts index 902c2a8e07..4799fbc155 100644 --- a/src/auth/ha-auth-flow.ts +++ b/src/auth/ha-auth-flow.ts @@ -96,6 +96,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) { return html` ${this.localize("ui.panel.page-authorize.abort_intro")}: extends LitElement { + public hass!: HomeAssistant; + @property() public label?: string; + @property() public deviceId?: string; + @property() public value?: T; + protected NO_AUTOMATION_TEXT = "No automations"; + protected UNKNOWN_AUTOMATION_TEXT = "Unknown automation"; + @property() private _automations: T[] = []; + + // Trigger an empty render so we start with a clean DOM. + // paper-listbox does not like changing things around. + @property() private _renderEmpty = false; + + private _localizeDeviceAutomation: ( + hass: HomeAssistant, + automation: T + ) => string; + private _fetchDeviceAutomations: ( + hass: HomeAssistant, + deviceId: string + ) => Promise; + private _createNoAutomation: (deviceId?: string) => T; + + constructor( + localizeDeviceAutomation: HaDeviceAutomationPicker< + T + >["_localizeDeviceAutomation"], + fetchDeviceAutomations: HaDeviceAutomationPicker< + T + >["_fetchDeviceAutomations"], + createNoAutomation: HaDeviceAutomationPicker["_createNoAutomation"] + ) { + super(); + this._localizeDeviceAutomation = localizeDeviceAutomation; + this._fetchDeviceAutomations = fetchDeviceAutomations; + this._createNoAutomation = createNoAutomation; + } + + private get _key() { + if ( + !this.value || + deviceAutomationsEqual( + this._createNoAutomation(this.deviceId), + this.value + ) + ) { + return NO_AUTOMATION_KEY; + } + + const idx = this._automations.findIndex((automation) => + deviceAutomationsEqual(automation, this.value!) + ); + + if (idx === -1) { + return UNKNOWN_AUTOMATION_KEY; + } + + return `${this._automations[idx].device_id}_${idx}`; + } + + protected render(): TemplateResult | void { + if (this._renderEmpty) { + return html``; + } + return html` + + + + + ${this._automations.map( + (automation, idx) => html` + + ${this._localizeDeviceAutomation(this.hass, automation)} + + ` + )} + + + `; + } + + protected updated(changedProps) { + super.updated(changedProps); + + if (changedProps.has("deviceId")) { + this._updateDeviceInfo(); + } + + // The value has changed, force the listbox to update + if (changedProps.has("value") || changedProps.has("_renderEmpty")) { + const listbox = this.shadowRoot!.querySelector("paper-listbox")!; + if (listbox) { + listbox._selectSelected(this._key); + } + } + } + + private async _updateDeviceInfo() { + this._automations = this.deviceId + ? await this._fetchDeviceAutomations(this.hass, this.deviceId) + : // No device, clear the list of automations + []; + + // If there is no value, or if we have changed the device ID, reset the value. + if (!this.value || this.value.device_id !== this.deviceId) { + this._setValue( + this._automations.length + ? this._automations[0] + : this._createNoAutomation(this.deviceId) + ); + } + this._renderEmpty = true; + await this.updateComplete; + this._renderEmpty = false; + } + + private _automationChanged(ev) { + this._setValue(ev.detail.item.automation); + } + + private _setValue(automation: T) { + if (this.value && deviceAutomationsEqual(automation, this.value)) { + return; + } + this.value = automation; + setTimeout(() => { + fireEvent(this, "change"); + }, 0); + } + + static get styles(): CSSResult { + return css` + ha-paper-dropdown-menu { + width: 100%; + } + paper-listbox { + min-width: 200px; + } + paper-item { + cursor: pointer; + } + `; + } +} diff --git a/src/components/device/ha-device-condition-picker.ts b/src/components/device/ha-device-condition-picker.ts new file mode 100644 index 0000000000..d0a45fe8e8 --- /dev/null +++ b/src/components/device/ha-device-condition-picker.ts @@ -0,0 +1,35 @@ +import { customElement } from "lit-element"; +import { + DeviceCondition, + fetchDeviceConditions, + localizeDeviceAutomationCondition, +} from "../../data/device_automation"; +import "../../components/ha-paper-dropdown-menu"; +import { HaDeviceAutomationPicker } from "./ha-device-automation-picker"; + +@customElement("ha-device-condition-picker") +class HaDeviceConditionPicker extends HaDeviceAutomationPicker< + DeviceCondition +> { + protected NO_AUTOMATION_TEXT = "No conditions"; + protected UNKNOWN_AUTOMATION_TEXT = "Unknown condition"; + + constructor() { + super( + localizeDeviceAutomationCondition, + fetchDeviceConditions, + (deviceId?: string) => ({ + device_id: deviceId || "", + condition: "device", + domain: "", + entity_id: "", + }) + ); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-device-condition-picker": HaDeviceConditionPicker; + } +} diff --git a/src/components/device/ha-device-trigger-picker.ts b/src/components/device/ha-device-trigger-picker.ts index 6330ae76d5..a5913220ac 100644 --- a/src/components/device/ha-device-trigger-picker.ts +++ b/src/components/device/ha-device-trigger-picker.ts @@ -1,164 +1,28 @@ -import "@polymer/paper-input/paper-input"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-item/paper-item-body"; -import "@polymer/paper-listbox/paper-listbox"; -import { - LitElement, - TemplateResult, - html, - css, - CSSResult, - customElement, - property, -} from "lit-element"; -import { HomeAssistant } from "../../types"; -import { fireEvent } from "../../common/dom/fire_event"; +import { customElement } from "lit-element"; import { DeviceTrigger, fetchDeviceTriggers, - deviceAutomationTriggersEqual, localizeDeviceAutomationTrigger, } from "../../data/device_automation"; import "../../components/ha-paper-dropdown-menu"; - -const NO_TRIGGER_KEY = "NO_TRIGGER"; -const UNKNOWN_TRIGGER_KEY = "UNKNOWN_TRIGGER"; +import { HaDeviceAutomationPicker } from "./ha-device-automation-picker"; @customElement("ha-device-trigger-picker") -class HaDeviceTriggerPicker extends LitElement { - public hass!: HomeAssistant; - @property() public label?: string; - @property() public deviceId?: string; - @property() public value?: DeviceTrigger; - @property() private _triggers: DeviceTrigger[] = []; +class HaDeviceTriggerPicker extends HaDeviceAutomationPicker { + protected NO_AUTOMATION_TEXT = "No triggers"; + protected UNKNOWN_AUTOMATION_TEXT = "Unknown trigger"; - // Trigger an empty render so we start with a clean DOM. - // paper-listbox does not like changing things around. - @property() private _renderEmpty = false; - - private get _key() { - if ( - !this.value || - deviceAutomationTriggersEqual(this._noTrigger, this.value) - ) { - return NO_TRIGGER_KEY; - } - - const idx = this._triggers.findIndex((trigger) => - deviceAutomationTriggersEqual(trigger, this.value!) + constructor() { + super( + localizeDeviceAutomationTrigger, + fetchDeviceTriggers, + (deviceId?: string) => ({ + device_id: deviceId || "", + platform: "device", + domain: "", + entity_id: "", + }) ); - - if (idx === -1) { - return UNKNOWN_TRIGGER_KEY; - } - - return `${this._triggers[idx].device_id}_${idx}`; - } - - protected render(): TemplateResult | void { - if (this._renderEmpty) { - return html``; - } - return html` - - - - - ${this._triggers.map( - (trigger, idx) => html` - - ${localizeDeviceAutomationTrigger(this.hass, trigger)} - - ` - )} - - - `; - } - - protected updated(changedProps) { - super.updated(changedProps); - - if (changedProps.has("deviceId")) { - this._updateDeviceInfo(); - } - - // The value has changed, force the listbox to update - if (changedProps.has("value") || changedProps.has("_renderEmpty")) { - const listbox = this.shadowRoot!.querySelector("paper-listbox")!; - if (listbox) { - listbox._selectSelected(this._key); - } - } - } - - private async _updateDeviceInfo() { - this._triggers = this.deviceId - ? await fetchDeviceTriggers(this.hass!, this.deviceId!) - : // No device, clear the list of triggers - []; - - // If there is no value, or if we have changed the device ID, reset the value. - if (!this.value || this.value.device_id !== this.deviceId) { - this._setValue( - this._triggers.length ? this._triggers[0] : this._noTrigger - ); - } - this._renderEmpty = true; - await this.updateComplete; - this._renderEmpty = false; - } - - private get _noTrigger() { - return { - device_id: this.deviceId || "", - platform: "device", - domain: "", - entity_id: "", - }; - } - - private _triggerChanged(ev) { - this._setValue(ev.detail.item.trigger); - } - - private _setValue(trigger: DeviceTrigger) { - if (this.value && deviceAutomationTriggersEqual(trigger, this.value)) { - return; - } - this.value = trigger; - setTimeout(() => { - fireEvent(this, "change"); - }, 0); - } - - static get styles(): CSSResult { - return css` - ha-paper-dropdown-menu { - width: 100%; - } - paper-listbox { - min-width: 200px; - } - paper-item { - cursor: pointer; - } - `; } } diff --git a/src/components/ha-markdown.ts b/src/components/ha-markdown.ts index 41d5e08b90..e811e31373 100644 --- a/src/components/ha-markdown.ts +++ b/src/components/ha-markdown.ts @@ -10,6 +10,7 @@ let worker: any | undefined; @customElement("ha-markdown") class HaMarkdown extends UpdatingElement { @property() public content = ""; + @property({ type: Boolean }) public allowSvg = false; protected update(changedProps) { super.update(changedProps); @@ -22,11 +23,17 @@ class HaMarkdown extends UpdatingElement { } private async _render() { - this.innerHTML = await worker.renderMarkdown(this.content, { - breaks: true, - gfm: true, - tables: true, - }); + this.innerHTML = await worker.renderMarkdown( + this.content, + { + breaks: true, + gfm: true, + tables: true, + }, + { + allowSvg: this.allowSvg, + } + ); this._resize(); diff --git a/src/data/device_automation.ts b/src/data/device_automation.ts index d3c7370527..b764f4b47b 100644 --- a/src/data/device_automation.ts +++ b/src/data/device_automation.ts @@ -1,30 +1,38 @@ import { HomeAssistant } from "../types"; import compute_state_name from "../common/entity/compute_state_name"; -export interface DeviceTrigger { - platform: string; +export interface DeviceAutomation { device_id: string; domain: string; entity_id: string; type?: string; + subtype?: string; event?: string; } -export interface DeviceTriggerList { - triggers: DeviceTrigger[]; +export interface DeviceCondition extends DeviceAutomation { + condition: string; } -export const fetchDeviceTriggers = (hass: HomeAssistant, deviceId: string) => - hass - .callWS({ - type: "device_automation/trigger/list", - device_id: deviceId, - }) - .then((response) => response.triggers); +export interface DeviceTrigger extends DeviceAutomation { + platform: string; +} -export const deviceAutomationTriggersEqual = ( - a: DeviceTrigger, - b: DeviceTrigger +export const fetchDeviceConditions = (hass: HomeAssistant, deviceId: string) => + hass.callWS({ + type: "device_automation/condition/list", + device_id: deviceId, + }); + +export const fetchDeviceTriggers = (hass: HomeAssistant, deviceId: string) => + hass.callWS({ + type: "device_automation/trigger/list", + device_id: deviceId, + }); + +export const deviceAutomationsEqual = ( + a: DeviceAutomation, + b: DeviceAutomation ) => { if (typeof a !== typeof b) { return false; @@ -44,14 +52,44 @@ export const deviceAutomationTriggersEqual = ( return true; }; +export const localizeDeviceAutomationCondition = ( + hass: HomeAssistant, + condition: DeviceCondition +) => { + const state = condition.entity_id + ? hass.states[condition.entity_id] + : undefined; + return hass.localize( + `component.${condition.domain}.device_automation.condition_type.${ + condition.type + }`, + "entity_name", + state ? compute_state_name(state) : "", + "subtype", + hass.localize( + `component.${condition.domain}.device_automation.condition_subtype.${ + condition.subtype + }` + ) + ); +}; + export const localizeDeviceAutomationTrigger = ( hass: HomeAssistant, trigger: DeviceTrigger -) => - hass.localize( +) => { + const state = trigger.entity_id ? hass.states[trigger.entity_id] : undefined; + return hass.localize( `component.${trigger.domain}.device_automation.trigger_type.${ trigger.type }`, - "name", - trigger.entity_id ? compute_state_name(hass!.states[trigger.entity_id]) : "" + "entity_name", + state ? compute_state_name(state) : "", + "subtype", + hass.localize( + `component.${trigger.domain}.device_automation.trigger_subtype.${ + trigger.subtype + }` + ) ); +}; diff --git a/src/data/timer.ts b/src/data/timer.ts new file mode 100644 index 0000000000..e55b936d15 --- /dev/null +++ b/src/data/timer.ts @@ -0,0 +1,11 @@ +import { + HassEntityBase, + HassEntityAttributeBase, +} from "home-assistant-js-websocket"; + +export type TimerEntity = HassEntityBase & { + attributes: HassEntityAttributeBase & { + duration: string; + remaining: string; + }; +}; diff --git a/src/data/zha.ts b/src/data/zha.ts index 9c10944f8b..4ae16c680a 100644 --- a/src/data/zha.ts +++ b/src/data/zha.ts @@ -80,6 +80,15 @@ export const fetchDevices = (hass: HomeAssistant): Promise => type: "zha/devices", }); +export const fetchZHADevice = ( + hass: HomeAssistant, + ieeeAddress: string +): Promise => + hass.callWS({ + type: "zha/device", + ieee: ieeeAddress, + }); + export const fetchBindableDevices = ( hass: HomeAssistant, ieeeAddress: string diff --git a/src/dialogs/config-flow/show-dialog-config-flow.ts b/src/dialogs/config-flow/show-dialog-config-flow.ts index 601f522d05..c91723b9c2 100644 --- a/src/dialogs/config-flow/show-dialog-config-flow.ts +++ b/src/dialogs/config-flow/show-dialog-config-flow.ts @@ -45,7 +45,7 @@ export const showConfigFlowDialog = ( return description ? html` - + ` : ""; }, @@ -64,7 +64,7 @@ export const showConfigFlowDialog = ( ); return description ? html` - + ` : ""; }, @@ -102,7 +102,7 @@ export const showConfigFlowDialog = (

${description ? html` - + ` : ""} `; @@ -119,7 +119,7 @@ export const showConfigFlowDialog = ( return html` ${description ? html` - + ` : ""}

Created config for ${step.title}.

diff --git a/src/dialogs/config-flow/show-dialog-options-flow.ts b/src/dialogs/config-flow/show-dialog-options-flow.ts index 740b364fbf..7720e1cea6 100644 --- a/src/dialogs/config-flow/show-dialog-options-flow.ts +++ b/src/dialogs/config-flow/show-dialog-options-flow.ts @@ -39,7 +39,7 @@ export const showOptionsFlowDialog = ( return description ? html` - + ` : ""; }, diff --git a/src/dialogs/more-info/controls/more-info-content.ts b/src/dialogs/more-info/controls/more-info-content.ts index 2d0f0e4cc7..51dfab0c68 100644 --- a/src/dialogs/more-info/controls/more-info-content.ts +++ b/src/dialogs/more-info/controls/more-info-content.ts @@ -21,6 +21,7 @@ import "./more-info-lock"; import "./more-info-media_player"; import "./more-info-script"; import "./more-info-sun"; +import "./more-info-timer"; import "./more-info-updater"; import "./more-info-vacuum"; import "./more-info-water_heater"; diff --git a/src/dialogs/more-info/controls/more-info-timer.ts b/src/dialogs/more-info/controls/more-info-timer.ts new file mode 100644 index 0000000000..e0ce0c5bb5 --- /dev/null +++ b/src/dialogs/more-info/controls/more-info-timer.ts @@ -0,0 +1,104 @@ +import { + LitElement, + html, + TemplateResult, + CSSResult, + css, + property, + PropertyValues, + customElement, +} from "lit-element"; +import "@material/mwc-button"; + +import { HomeAssistant } from "../../../types"; +import { TimerEntity } from "../../../data/timer"; + +@customElement("more-info-timer") +class MoreInfoTimer extends LitElement { + @property() public hass!: HomeAssistant; + + @property() public stateObj?: TimerEntity; + + protected render(): TemplateResult | void { + if (!this.hass || !this.stateObj) { + return html``; + } + + return html` + +
+ ${this.stateObj.state === "idle" || this.stateObj.state === "paused" + ? html` + + ${this.hass!.localize("ui.card.timer.actions.start")} + + ` + : ""} + ${this.stateObj.state === "active" + ? html` + + ${this.hass!.localize("ui.card.timer.actions.pause")} + + ` + : ""} + ${this.stateObj.state === "active" || this.stateObj.state === "paused" + ? html` + + ${this.hass!.localize("ui.card.timer.actions.cancel")} + + + ${this.hass!.localize("ui.card.timer.actions.finish")} + + ` + : ""} +
+ `; + } + + protected updated(changedProps: PropertyValues) { + super.updated(changedProps); + if (!changedProps.has("stateObj") || !this.stateObj) { + return; + } + } + + private _handleActionClick(e: MouseEvent): void { + const action = (e.currentTarget as any).action; + this.hass.callService("timer", action, { + entity_id: this.stateObj!.entity_id, + }); + } + + static get styles(): CSSResult { + return css` + .actions { + margin: 0 8px; + padding-top: 20px; + display: flex; + flex-wrap: wrap; + justify-content: center; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "more-info-timer": MoreInfoTimer; + } +} diff --git a/src/dialogs/zha-device-info-dialog/dialog-zha-device-info.ts b/src/dialogs/zha-device-info-dialog/dialog-zha-device-info.ts new file mode 100644 index 0000000000..6d4abc4e66 --- /dev/null +++ b/src/dialogs/zha-device-info-dialog/dialog-zha-device-info.ts @@ -0,0 +1,115 @@ +import { + LitElement, + html, + css, + CSSResult, + TemplateResult, + customElement, + property, +} from "lit-element"; +import "@polymer/paper-dialog-scrollable/paper-dialog-scrollable"; +import "../../components/dialog/ha-paper-dialog"; +// Not duplicate, is for typing +// tslint:disable-next-line +import { HaPaperDialog } from "../../components/dialog/ha-paper-dialog"; +import "../../panels/config/zha/zha-device-card"; + +import { PolymerChangedEvent } from "../../polymer-types"; +import { haStyleDialog } from "../../resources/styles"; +import { HomeAssistant } from "../../types"; +import { ZHADeviceInfoDialogParams } from "./show-dialog-zha-device-info"; +import { ZHADevice, fetchZHADevice } from "../../data/zha"; + +@customElement("dialog-zha-device-info") +class DialogZHADeviceInfo extends LitElement { + @property() public hass!: HomeAssistant; + @property() private _params?: ZHADeviceInfoDialogParams; + @property() private _error?: string; + @property() private _device?: ZHADevice; + + public async showDialog(params: ZHADeviceInfoDialogParams): Promise { + this._params = params; + this._device = await fetchZHADevice(this.hass, params.ieee); + await this.updateComplete; + this._dialog.open(); + } + + protected render(): TemplateResult | void { + if (!this._params || !this._device) { + return html``; + } + + return html` + + ${this._error + ? html` +
${this._error}
+ ` + : html` + + `} +
+ `; + } + + private _openedChanged(ev: PolymerChangedEvent): void { + if (!ev.detail.value) { + this._params = undefined; + this._error = undefined; + this._device = undefined; + } + } + + private _onDeviceRemoved(): void { + this._closeDialog(); + } + + private get _dialog(): HaPaperDialog { + return this.shadowRoot!.querySelector("ha-paper-dialog")!; + } + + private _closeDialog() { + this._dialog.close(); + } + + static get styles(): CSSResult[] { + return [ + haStyleDialog, + css` + ha-paper-dialog > * { + margin: 0; + display: block; + padding: 0; + } + .card { + box-sizing: border-box; + display: flex; + flex: 1 0 300px; + min-width: 0; + max-width: 600px; + word-wrap: break-word; + } + .error { + color: var(--google-red-500); + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-zha-device-info": DialogZHADeviceInfo; + } +} diff --git a/src/dialogs/zha-device-info-dialog/show-dialog-zha-device-info.ts b/src/dialogs/zha-device-info-dialog/show-dialog-zha-device-info.ts new file mode 100644 index 0000000000..aff1ee5447 --- /dev/null +++ b/src/dialogs/zha-device-info-dialog/show-dialog-zha-device-info.ts @@ -0,0 +1,19 @@ +import { fireEvent } from "../../common/dom/fire_event"; + +export interface ZHADeviceInfoDialogParams { + ieee: string; +} + +export const loadZHADeviceInfoDialog = () => + import(/* webpackChunkName: "dialog-zha-device-info" */ "./dialog-zha-device-info"); + +export const showZHADeviceInfoDialog = ( + element: HTMLElement, + zhaDeviceInfoParams: ZHADeviceInfoDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-zha-device-info", + dialogImport: loadZHADeviceInfoDialog, + dialogParams: zhaDeviceInfoParams, + }); +}; diff --git a/src/panels/config/cloud/account/cloud-google-pref.ts b/src/panels/config/cloud/account/cloud-google-pref.ts index 69767fe070..9035eaebd9 100644 --- a/src/panels/config/cloud/account/cloud-google-pref.ts +++ b/src/panels/config/cloud/account/cloud-google-pref.ts @@ -18,6 +18,7 @@ import { fireEvent } from "../../../../common/dom/fire_event"; import { HomeAssistant } from "../../../../types"; import { CloudStatusLoggedIn, updateCloudPref } from "../../../../data/cloud"; import { PaperInputElement } from "@polymer/paper-input/paper-input"; +import { showSaveSuccessToast } from "../../../../util/toast-saved-success"; export class CloudGooglePref extends LitElement { public hass?: HomeAssistant; @@ -83,7 +84,7 @@ export class CloudGooglePref extends LitElement { @@ -124,6 +125,7 @@ export class CloudGooglePref extends LitElement { await updateCloudPref(this.hass!, { [input.id]: input.value || null, }); + showSaveSuccessToast(this, this.hass!); fireEvent(this, "ha-refresh-cloud-status"); } catch (err) { alert(`Unable to store pin: ${err.message}`); @@ -150,7 +152,7 @@ export class CloudGooglePref extends LitElement { padding-top: 16px; } paper-input { - width: 200px; + width: 250px; } .card-actions { display: flex; diff --git a/src/panels/config/js/condition/condition_edit.js b/src/panels/config/js/condition/condition_edit.js index 3012aca885..5609ef8a96 100644 --- a/src/panels/config/js/condition/condition_edit.js +++ b/src/panels/config/js/condition/condition_edit.js @@ -3,6 +3,7 @@ import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light"; import "@polymer/paper-listbox/paper-listbox"; import "@polymer/paper-item/paper-item"; +import DeviceCondition from "./device"; import NumericStateCondition from "./numeric_state"; import StateCondition from "./state"; import SunCondition from "./sun"; @@ -11,6 +12,7 @@ import TimeCondition from "./time"; import ZoneCondition from "./zone"; const TYPES = { + device: DeviceCondition, state: StateCondition, numeric_state: NumericStateCondition, sun: SunCondition, diff --git a/src/panels/config/js/condition/device.js b/src/panels/config/js/condition/device.js new file mode 100644 index 0000000000..fdcb6a02e8 --- /dev/null +++ b/src/panels/config/js/condition/device.js @@ -0,0 +1,57 @@ +import { h, Component } from "preact"; + +import "../../../../components/device/ha-device-picker"; +import "../../../../components/device/ha-device-condition-picker"; + +import { onChangeEvent } from "../../../../common/preact/event"; + +export default class DeviceCondition extends Component { + constructor() { + super(); + this.onChange = onChangeEvent.bind(this, "condition"); + this.devicePicked = this.devicePicked.bind(this); + this.deviceConditionPicked = this.deviceConditionPicked.bind(this); + this.state.device_id = undefined; + } + + devicePicked(ev) { + this.setState({ device_id: ev.target.value }); + } + + deviceConditionPicked(ev) { + const deviceCondition = ev.target.value; + this.props.onChange( + this.props.index, + (this.props.condition = deviceCondition) + ); + } + + /* eslint-disable camelcase */ + render({ condition, hass }, { device_id }) { + if (device_id === undefined) device_id = condition.device_id; + + return ( +
+ + +
+ ); + } +} + +DeviceCondition.defaultConfig = { + device_id: "", + domain: "", + entity_id: "", +}; diff --git a/src/panels/config/js/trigger/device.js b/src/panels/config/js/trigger/device.js index f5d857ab57..ee4ef4d982 100644 --- a/src/panels/config/js/trigger/device.js +++ b/src/panels/config/js/trigger/device.js @@ -2,6 +2,7 @@ import { h, Component } from "preact"; import "../../../../components/device/ha-device-picker"; import "../../../../components/device/ha-device-trigger-picker"; +import "../../../../components/device/ha-device-automation-picker"; import { onChangeEvent } from "../../../../common/preact/event"; diff --git a/src/panels/config/zha/zha-add-devices-page.ts b/src/panels/config/zha/zha-add-devices-page.ts index 5adb181fd4..ff020d8a3e 100644 --- a/src/panels/config/zha/zha-add-devices-page.ts +++ b/src/panels/config/zha/zha-add-devices-page.ts @@ -113,12 +113,12 @@ class ZHAAddDevicesPage extends LitElement { (device) => html` ` )} diff --git a/src/panels/config/zha/zha-device-card.ts b/src/panels/config/zha/zha-device-card.ts index b34a720bff..886fb6c2f4 100644 --- a/src/panels/config/zha/zha-device-card.ts +++ b/src/panels/config/zha/zha-device-card.ts @@ -55,11 +55,11 @@ declare global { @customElement("zha-device-card") class ZHADeviceCard extends LitElement { @property() public hass!: HomeAssistant; - @property() public narrow?: boolean; @property() public device?: ZHADevice; - @property() public showHelp: boolean = false; - @property() public showActions?: boolean; - @property() public isJoinPage?: boolean; + @property({ type: Boolean }) public narrow?: boolean; + @property({ type: Boolean }) public showHelp?: boolean = false; + @property({ type: Boolean }) public showActions?: boolean; + @property({ type: Boolean }) public isJoinPage?: boolean; @property() private _serviceData?: NodeServiceData; @property() private _areas: AreaRegistryEntry[] = []; @property() private _selectedAreaIndex: number = -1; @@ -139,7 +139,7 @@ class ZHADeviceCard extends LitElement {
${this.device!.model}
${this.hass!.localize( - "ui.panel.config.integrations.config_entry.manuf", + "ui.dialogs.zha_device_info.manuf", "manufacturer", this.device!.manufacturer )} @@ -206,14 +206,14 @@ class ZHADeviceCard extends LitElement { @change="${this._saveCustomName}" .value="${this._userGivenName}" placeholder="${this.hass!.localize( - "ui.panel.config.zha.device_card.device_name_placeholder" + "ui.dialogs.zha_device_info.zha_device_card.device_name_placeholder" )}" >
@@ -223,9 +223,7 @@ class ZHADeviceCard extends LitElement { @iron-select="${this._selectedAreaChanged}" > - ${this.hass!.localize( - "ui.panel.config.integrations.config_entry.no_area" - )} + ${this.hass!.localize("ui.dialogs.zha_device_info.no_area")} ${this._areas.map( @@ -247,7 +245,7 @@ class ZHADeviceCard extends LitElement { ? html`
${this.hass!.localize( - "ui.panel.config.zha.services.reconfigure" + "ui.dialogs.zha_device_info.services.reconfigure" )}
` @@ -264,7 +262,7 @@ class ZHADeviceCard extends LitElement { ? html`
${this.hass!.localize( - "ui.panel.config.zha.services.remove" + "ui.dialogs.zha_device_info.services.remove" )}
` @@ -381,6 +379,7 @@ class ZHADeviceCard extends LitElement { } .device .manuf { color: var(--secondary-text-color); + margin-bottom: 20px; } .extra-info { margin-top: 8px; @@ -393,14 +392,17 @@ class ZHADeviceCard extends LitElement { .info { margin-left: 16px; } + dl { + display: grid; + grid-template-columns: 125px 1fr; + } dl dt { padding-left: 12px; float: left; - width: 100px; text-align: left; } - dt dd { - text-align: left; + dl dd { + max-width: 200px; } paper-icon-item { cursor: pointer; diff --git a/src/panels/config/zha/zha-node.ts b/src/panels/config/zha/zha-node.ts index e665a79ddd..01a7b5c57a 100644 --- a/src/panels/config/zha/zha-node.ts +++ b/src/panels/config/zha/zha-node.ts @@ -105,13 +105,12 @@ export class ZHANode extends LitElement { ? html` ` : ""} diff --git a/src/panels/developer-tools/service/developer-tools-service.js b/src/panels/developer-tools/service/developer-tools-service.js index 05b1e7e1bf..450e8b291d 100644 --- a/src/panels/developer-tools/service/developer-tools-service.js +++ b/src/panels/developer-tools/service/developer-tools-service.js @@ -115,9 +115,9 @@ class HaPanelDevService extends PolymerElement { autocomplete="off" spellcheck="false" > - Call Service + + Call Service + @@ -153,6 +153,12 @@ class HaPanelDevService extends PolymerElement { + +
@@ -274,11 +280,17 @@ class HaPanelDevService extends PolymerElement { this.hass.callService(this._domain, this._service, this.parsedJSON); } + _fillExampleData() { + const example = {}; + for (const attribute of this._attributes) { + example[attribute.key] = attribute.example; + } + this.serviceData = JSON.stringify(example, null, 2); + } + _entityPicked(ev) { this.serviceData = JSON.stringify( - Object.assign({}, this.parsedJSON, { - entity_id: ev.target.value, - }), + { ...this.parsedJSON, entity_id: ev.target.value }, null, 2 ); diff --git a/src/panels/developer-tools/state/developer-tools-state.js b/src/panels/developer-tools/state/developer-tools-state.js index 92e6d3e038..c02ec0de90 100644 --- a/src/panels/developer-tools/state/developer-tools-state.js +++ b/src/panels/developer-tools/state/developer-tools-state.js @@ -74,6 +74,7 @@ class HaPanelDevState extends EventsMixin(PolymerElement) { autofocus hass="[[hass]]" value="{{_entityId}}" + on-change="entityIdChanged" allow-custom-entity > entity === this._oldEntities![idx]); + + if (!isSame) { + this._oldEntities = entitiesList; + element.setConfig({ ...this._baseCardConfig!, entities: entitiesList }); + } + element.isPanel = this.isPanel; element.hass = hass; } @@ -75,6 +93,27 @@ class EntityFilterCard extends HTMLElement implements LovelaceCard { this.style.display = "block"; } + private haveEntitiesChanged(hass: HomeAssistant): boolean { + if (!this._hass) { + return true; + } + + if (!this._configEntities) { + return true; + } + + for (const config of this._configEntities) { + if ( + this._hass.states[config.entity] !== hass.states[config.entity] || + this._hass.localize !== hass.localize + ) { + return true; + } + } + + return false; + } + private _cardElement(): LovelaceCard | undefined { if (!this._element && this._config) { const element = createCardElement(this._baseCardConfig!); diff --git a/src/panels/lovelace/cards/hui-light-card.ts b/src/panels/lovelace/cards/hui-light-card.ts index a27c7709d8..7f596209f8 100644 --- a/src/panels/lovelace/cards/hui-light-card.ts +++ b/src/panels/lovelace/cards/hui-light-card.ts @@ -195,9 +195,6 @@ export class HuiLightCard extends LitElement implements LovelaceCard { ha-card { position: relative; overflow: hidden; - --brightness-font-color: white; - --brightness-font-text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, - -1px 1px 0 #000, 1px 1px 0 #000; --name-font-size: 1.2rem; --brightness-font-size: 1.2rem; --rail-border-color: transparent; @@ -290,15 +287,13 @@ export class HuiLightCard extends LitElement implements LovelaceCard { position: absolute; margin: 0 auto; left: 50%; - top: 10%; + top: 50%; transform: translate(-50%); opacity: 0; transition: opacity 0.5s ease-in-out; -moz-transition: opacity 0.5s ease-in-out; -webkit-transition: opacity 0.5s ease-in-out; cursor: pointer; - color: var(--brightness-font-color); - text-shadow: var(--brightness-font-text-shadow); pointer-events: none; } diff --git a/src/panels/lovelace/cards/hui-weather-forecast-card.js b/src/panels/lovelace/cards/hui-weather-forecast-card.js deleted file mode 100644 index 9c05fdc2c9..0000000000 --- a/src/panels/lovelace/cards/hui-weather-forecast-card.js +++ /dev/null @@ -1,24 +0,0 @@ -import "../../../cards/ha-weather-card"; - -import LegacyWrapperCard from "./hui-legacy-wrapper-card"; - -class HuiWeatherForecastCard extends LegacyWrapperCard { - static async getConfigElement() { - await import(/* webpackChunkName: "hui-weather-forecast-card-editor" */ "../editor/config-elements/hui-weather-forecast-card-editor"); - return document.createElement("hui-weather-forecast-card-editor"); - } - - static getStubConfig() { - return {}; - } - - constructor() { - super("ha-weather-card", "weather"); - } - - getCardSize() { - return 4; - } -} - -customElements.define("hui-weather-forecast-card", HuiWeatherForecastCard); diff --git a/src/panels/lovelace/cards/hui-weather-forecast-card.ts b/src/panels/lovelace/cards/hui-weather-forecast-card.ts new file mode 100644 index 0000000000..a251c92ab8 --- /dev/null +++ b/src/panels/lovelace/cards/hui-weather-forecast-card.ts @@ -0,0 +1,433 @@ +import { + html, + LitElement, + PropertyValues, + TemplateResult, + css, + CSSResult, + property, + customElement, +} from "lit-element"; + +import "../../../components/ha-card"; +import "../components/hui-warning"; + +import isValidEntityId from "../../../common/entity/valid_entity_id"; +import computeStateName from "../../../common/entity/compute_state_name"; + +import { HomeAssistant } from "../../../types"; +import { hasConfigOrEntityChanged } from "../common/has-changed"; +import { LovelaceCard, LovelaceCardEditor } from "../types"; +import { WeatherForecastCardConfig } from "./types"; +import { computeRTL } from "../../../common/util/compute_rtl"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { toggleAttribute } from "../../../common/dom/toggle_attribute"; + +const cardinalDirections = [ + "N", + "NNE", + "NE", + "ENE", + "E", + "ESE", + "SE", + "SSE", + "S", + "SSW", + "SW", + "WSW", + "W", + "WNW", + "NW", + "NNW", + "N", +]; + +const 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", + "lightning-rainy": "hass:weather-lightning-rainy", + partlycloudy: "hass:weather-partly-cloudy", + pouring: "hass:weather-pouring", + rainy: "hass:weather-rainy", + snowy: "hass:weather-snowy", + "snowy-rainy": "hass:weather-snowy-rainy", + sunny: "hass:weather-sunny", + windy: "hass:weather-windy", + "windy-variant": "hass:weather-windy-variant", +}; + +@customElement("hui-weather-forecast-card") +class HuiWeatherForecastCard extends LitElement implements LovelaceCard { + public static async getConfigElement(): Promise { + await import(/* webpackChunkName: "hui-weather-forecast-card-editor" */ "../editor/config-elements/hui-weather-forecast-card-editor"); + return document.createElement("hui-weather-forecast-card-editor"); + } + public static getStubConfig(): object { + return { entity: "" }; + } + + @property() public hass?: HomeAssistant; + + @property() private _config?: WeatherForecastCardConfig; + + public getCardSize(): number { + return 4; + } + + public setConfig(config: WeatherForecastCardConfig): void { + if (!config || !config.entity) { + throw new Error("Invalid card configuration"); + } + if (!isValidEntityId(config.entity)) { + throw new Error("Invalid Entity"); + } + + this._config = config; + } + + protected updated(changedProps: PropertyValues): void { + super.updated(changedProps); + if (changedProps.has("hass")) { + toggleAttribute(this, "rtl", computeRTL(this.hass!)); + } + } + + protected render(): TemplateResult | void { + if (!this._config || !this.hass) { + return html``; + } + + const stateObj = this.hass.states[this._config.entity]; + + if (!stateObj) { + return html` + ${this.hass.localize( + "ui.panel.lovelace.warning.entity_not_found", + "entity", + this._config.entity + )} + `; + } + + const forecast = stateObj.attributes.forecast + ? stateObj.attributes.forecast.slice(0, 5) + : undefined; + + return html` + +
+ ${this.hass.localize(`state.weather.${stateObj.state}`) || + stateObj.state} +
+ ${(this._config && this._config.name) || computeStateName(stateObj)} +
+
+
+
+
+ ${stateObj.state in weatherIcons + ? html` + + ` + : ""} +
+ ${stateObj.attributes.temperature}${this.getUnit("temperature")} +
+
+
+ ${this._showValue(stateObj.attributes.pressure) + ? html` +
+ ${this.hass.localize( + "ui.card.weather.attributes.air_pressure" + )}: + + ${stateObj.attributes.pressure} + ${this.getUnit("air_pressure")} + +
+ ` + : ""} + ${this._showValue(stateObj.attributes.humidity) + ? html` +
+ ${this.hass.localize( + "ui.card.weather.attributes.humidity" + )}: + ${stateObj.attributes.humidity} % +
+ ` + : ""} + ${this._showValue(stateObj.attributes.wind_speed) + ? html` +
+ ${this.hass.localize( + "ui.card.weather.attributes.wind_speed" + )}: + + ${stateObj.attributes.wind_speed} + ${this.getUnit("length")}/h + + ${this.getWindBearing(stateObj.attributes.wind_bearing)} +
+ ` + : ""} +
+
+ ${forecast + ? html` +
+ ${forecast.map( + (item) => html` +
+
+ ${new Date(item.datetime).toLocaleDateString( + this.hass!.language, + { weekday: "short" } + )}
+ ${!this._showValue(item.templow) + ? html` + ${new Date(item.datetime).toLocaleTimeString( + this.hass!.language, + { hour: "numeric" } + )} + ` + : ""} +
+ ${this._showValue(item.condition) + ? html` +
+ +
+ ` + : ""} + ${this._showValue(item.temperature) + ? html` +
+ ${item.temperature} + ${this.getUnit("temperature")} +
+ ` + : ""} + ${this._showValue(item.templow) + ? html` +
+ ${item.templow} ${this.getUnit("temperature")} +
+ ` + : ""} + ${this._showValue(item.precipitation) + ? html` +
+ ${item.precipitation} + ${this.getUnit("precipitation")} +
+ ` + : ""} +
+ ` + )} +
+ ` + : ""} +
+
+ `; + } + + protected shouldUpdate(changedProps: PropertyValues): boolean { + return hasConfigOrEntityChanged(this, changedProps); + } + + private handleClick(): void { + fireEvent(this, "hass-more-info", { entityId: this._config!.entity }); + } + + private getUnit(measure: string): string { + const lengthUnit = this.hass!.config.unit_system.length || ""; + switch (measure) { + case "air_pressure": + return lengthUnit === "km" ? "hPa" : "inHg"; + case "length": + return lengthUnit; + case "precipitation": + return lengthUnit === "km" ? "mm" : "in"; + default: + return this.hass!.config.unit_system[measure] || ""; + } + } + + private windBearingToText(degree: string): string { + const degreenum = parseInt(degree, 10); + if (isFinite(degreenum)) { + // tslint:disable-next-line: no-bitwise + return cardinalDirections[(((degreenum + 11.25) / 22.5) | 0) % 16]; + } + return degree; + } + + private getWindBearing(bearing: string): string { + if (bearing != null) { + const cardinalDirection = this.windBearingToText(bearing); + return `(${this.hass!.localize( + `ui.card.weather.cardinal_direction.${cardinalDirection.toLowerCase()}` + ) || cardinalDirection})`; + } + return ``; + } + + private _showValue(item: string): boolean { + return typeof item !== "undefined" && item !== null; + } + + static get styles(): CSSResult { + return css` + :host { + cursor: pointer; + } + + .content { + padding: 0 20px 20px; + } + + ha-icon { + color: var(--paper-item-icon-color); + } + + .header { + font-family: var(--paper-font-headline_-_font-family); + -webkit-font-smoothing: var( + --paper-font-headline_-_-webkit-font-smoothing + ); + font-size: var(--paper-font-headline_-_font-size); + font-weight: var(--paper-font-headline_-_font-weight); + letter-spacing: var(--paper-font-headline_-_letter-spacing); + line-height: var(--paper-font-headline_-_line-height); + text-rendering: var( + --paper-font-common-expensive-kerning_-_text-rendering + ); + opacity: var(--dark-primary-opacity); + padding: 24px 16px 16px; + display: flex; + align-items: baseline; + } + + .name { + margin-left: 16px; + font-size: 16px; + color: var(--secondary-text-color); + } + + :host([rtl]) .name { + margin-left: 0px; + margin-right: 16px; + } + + .now { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + } + + .main { + display: flex; + align-items: center; + margin-right: 32px; + } + + :host([rtl]) .main { + margin-right: 0px; + } + + .main ha-icon { + --iron-icon-height: 72px; + --iron-icon-width: 72px; + margin-right: 8px; + } + + :host([rtl]) .main ha-icon { + margin-right: 0px; + } + + .main .temp { + font-size: 52px; + line-height: 1em; + position: relative; + } + + :host([rtl]) .main .temp { + direction: ltr; + margin-right: 28px; + } + + .main .temp span { + font-size: 24px; + line-height: 1em; + position: absolute; + top: 4px; + } + + .measurand { + display: inline-block; + } + + :host([rtl]) .measurand { + direction: ltr; + } + + .forecast { + margin-top: 16px; + display: flex; + justify-content: space-between; + } + + .forecast div { + flex: 0 0 auto; + text-align: center; + } + + .forecast .icon { + margin: 4px 0; + text-align: center; + } + + :host([rtl]) .forecast .temp { + direction: ltr; + } + + .weekday { + font-weight: bold; + } + + .attributes, + .templow, + .precipitation { + color: var(--secondary-text-color); + } + + :host([rtl]) .precipitation { + direction: ltr; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-weather-forecast-card": HuiWeatherForecastCard; + } +} diff --git a/src/panels/lovelace/components/hui-views-list.ts b/src/panels/lovelace/components/hui-views-list.ts new file mode 100644 index 0000000000..b885ac32c7 --- /dev/null +++ b/src/panels/lovelace/components/hui-views-list.ts @@ -0,0 +1,99 @@ +import { + customElement, + LitElement, + property, + TemplateResult, + html, + CSSResult, + css, +} from "lit-element"; +import "@polymer/paper-listbox/paper-listbox"; +import "@polymer/paper-item/paper-icon-item"; +import "../../../../src/components/ha-icon"; +import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { LovelaceConfig } from "../../../data/lovelace"; + +declare global { + interface HASSDomEvents { + "view-selected": { + view: number; + }; + } +} + +@customElement("hui-views-list") +class HuiViewsList extends LitElement { + @property() private lovelaceConfig?: LovelaceConfig | undefined; + @property() private selected?: number | undefined; + + protected render(): TemplateResult | void { + if (!this.lovelaceConfig) { + return html``; + } + + return html` + + ${this.lovelaceConfig.views.map( + (view, index) => html` + + ${view.icon + ? html` + + ` + : ""} + ${view.title || view.path} + + ` + )} + + `; + } + + protected updated(changedProps) { + super.updated(changedProps); + toggleAttribute( + this, + "hide-icons", + this.lovelaceConfig + ? !this.lovelaceConfig.views.some((view) => view.icon) + : true + ); + } + + private async _handlePickView(ev: Event) { + const view = Number((ev.currentTarget as any).getAttribute("data-index")); + fireEvent(this, "view-selected", { view }); + } + + static get styles(): CSSResult { + return css` + paper-listbox { + padding-top: 0; + } + + paper-listbox ha-icon { + padding: 12px; + color: var(--secondary-text-color); + } + + paper-icon-item { + cursor: pointer; + } + + paper-icon-item[disabled] { + cursor: initial; + } + + :host([hide-icons]) paper-icon-item { + --paper-item-icon-width: 0px; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-views-list": HuiViewsList; + } +} diff --git a/src/panels/lovelace/editor/card-editor/hui-dialog-move-card-view.ts b/src/panels/lovelace/editor/card-editor/hui-dialog-move-card-view.ts index 9e860a75e0..dc8b282d1a 100644 --- a/src/panels/lovelace/editor/card-editor/hui-dialog-move-card-view.ts +++ b/src/panels/lovelace/editor/card-editor/hui-dialog-move-card-view.ts @@ -12,6 +12,8 @@ import "../../../../components/dialog/ha-paper-dialog"; // tslint:disable-next-line:no-duplicate-imports import { HaPaperDialog } from "../../../../components/dialog/ha-paper-dialog"; +import "../../components/hui-views-list"; + import { moveCard } from "../config-util"; import { MoveCardViewDialogParams } from "./show-move-card-view-dialog"; import { PolymerChangedEvent } from "../../../../polymer-types"; @@ -36,16 +38,11 @@ export class HuiDialogMoveCardView extends LitElement { @opened-changed="${this._openedChanged}" >

Choose view to move card

- ${this._params!.lovelace!.config.views.map((view, index) => { - return html` - ${view.title} - `; - })} + + `; } @@ -80,15 +77,14 @@ export class HuiDialogMoveCardView extends LitElement { return this.shadowRoot!.querySelector("ha-paper-dialog")!; } - private _moveCard(e: Event): void { - const newView = (e.currentTarget! as any).index; + private _moveCard(e: CustomEvent): void { + const newView = e.detail.view; const path = this._params!.path!; if (newView === path[0]) { return; } const lovelace = this._params!.lovelace!; - lovelace.saveConfig(moveCard(lovelace.config, path, [newView!])); this._dialog.close(); } diff --git a/src/panels/lovelace/editor/lovelace-editor/hui-dialog-edit-lovelace.ts b/src/panels/lovelace/editor/lovelace-editor/hui-dialog-edit-lovelace.ts index f7ebfb0fb4..5a2625ed04 100644 --- a/src/panels/lovelace/editor/lovelace-editor/hui-dialog-edit-lovelace.ts +++ b/src/panels/lovelace/editor/lovelace-editor/hui-dialog-edit-lovelace.ts @@ -55,8 +55,15 @@ export class HuiDialogEditLovelace extends LitElement { protected render(): TemplateResult | void { return html` -

Edit Lovelace

+

+ ${this.hass!.localize( + "ui.panel.lovelace.editor.edit_lovelace.header" + )} +

+ ${this.hass!.localize( + "ui.panel.lovelace.editor.edit_lovelace.explanation" + )}
- ${stateObj.attributes.device_class === "timestamp" + ${stateObj.attributes.device_class === "timestamp" && + stateObj.state !== "unavailable" ? html` @@ -90,6 +91,7 @@ class HaMfaModuleSetupFlow extends LocalizeMixin(EventsMixin(PolymerElement)) { if="[[_computeStepDescription(localize, _step)]]" > diff --git a/src/resources/markdown_worker.ts b/src/resources/markdown_worker.ts index 9c45e697f7..ec06680ce0 100644 --- a/src/resources/markdown_worker.ts +++ b/src/resources/markdown_worker.ts @@ -2,9 +2,21 @@ import marked from "marked"; // @ts-ignore import filterXSS from "xss"; -export const renderMarkdown = (content: string, markedOptions: object) => +const allowedSvgTags = ["svg", "path"]; + +const allowedTag = (tag: string) => tag === "ha-icon"; + +export const renderMarkdown = ( + content: string, + markedOptions: object, + hassOptions: { + // Do not allow SVG on untrusted content, it allows XSS. + allowSvg?: boolean; + } = {} +) => filterXSS(marked(content, markedOptions), { - onIgnoreTag(tag, html) { - return ["svg", "path", "ha-icon"].indexOf(tag) !== -1 ? html : null; - }, + onIgnoreTag: hassOptions.allowSvg + ? (tag, html) => + allowedTag(tag) || allowedSvgTags.includes(tag) ? html : null + : (tag, html) => (allowedTag(tag) ? html : null), }); diff --git a/src/state/hass-element.ts b/src/state/hass-element.ts index c837aafecf..da67ba1385 100644 --- a/src/state/hass-element.ts +++ b/src/state/hass-element.ts @@ -3,6 +3,7 @@ import AuthMixin from "./auth-mixin"; import TranslationsMixin from "./translations-mixin"; import ThemesMixin from "./themes-mixin"; import MoreInfoMixin from "./more-info-mixin"; +import ZHADialogMixin from "./zha-dialog-mixin"; import SidebarMixin from "./sidebar-mixin"; import { dialogManagerMixin } from "./dialog-manager-mixin"; import { connectionMixin } from "./connection-mixin"; @@ -25,4 +26,5 @@ export class HassElement extends ext(HassBaseMixin(LitElement), [ NotificationMixin, dialogManagerMixin, urlSyncMixin, + ZHADialogMixin, ]) {} diff --git a/src/state/zha-dialog-mixin.ts b/src/state/zha-dialog-mixin.ts new file mode 100644 index 0000000000..600c9c4972 --- /dev/null +++ b/src/state/zha-dialog-mixin.ts @@ -0,0 +1,29 @@ +import { Constructor, LitElement } from "lit-element"; +import { HassBaseEl } from "./hass-base-mixin"; +import { + showZHADeviceInfoDialog, + ZHADeviceInfoDialogParams, +} from "../dialogs/zha-device-info-dialog/show-dialog-zha-device-info"; +import { HASSDomEvent } from "../common/dom/fire_event"; + +declare global { + // for fire event + interface HASSDomEvents { + "zha-show-device-dialog": { + ieee: string; + }; + } +} + +export default (superClass: Constructor) => + class extends superClass { + protected firstUpdated(changedProps) { + super.firstUpdated(changedProps); + this.addEventListener("zha-show-device-dialog", (e) => + showZHADeviceInfoDialog( + e.target as HTMLElement, + (e as HASSDomEvent).detail + ) + ); + } + }; diff --git a/src/translations/en.json b/src/translations/en.json index aa3f678043..3af209aa68 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -446,6 +446,14 @@ "script": { "execute": "Execute" }, + "timer": { + "actions": { + "start": "start", + "pause": "pause", + "cancel": "cancel", + "finish": "finish" + } + }, "vacuum": { "actions": { "resume_cleaning": "Resume cleaning", @@ -554,6 +562,20 @@ "title": "System Options", "enable_new_entities_label": "Enable newly added entities.", "enable_new_entities_description": "If disabled, newly discovered entities will not be automatically added to Home Assistant." + }, + "zha_device_info": { + "manuf": "by {manufacturer}", + "no_area": "No Area", + "services": { + "reconfigure": "Reconfigure ZHA device (heal device). Use this if you are having issues with the device. If the device in question is a battery powered device please ensure it is awake and accepting commands when you use this service.", + "updateDeviceName": "Set a custom name for this device in the device registry.", + "remove": "Remove a device from the ZigBee network." + }, + "zha_device_card": { + "device_name_placeholder": "User given name", + "area_picker_label": "Area", + "update_name_button": "Update Name" + } } }, "duration": { @@ -780,6 +802,9 @@ "unsupported_condition": "Unsupported condition: {condition}", "type_select": "Condition type", "type": { + "device": { + "label": "Device" + }, "state": { "label": "[%key:ui::panel::config::automation::editor::triggers::type::state::label%]", "state": "[%key:ui::panel::config::automation::editor::triggers::type::state::label%]" @@ -942,16 +967,6 @@ "zha": { "caption": "ZHA", "description": "Zigbee Home Automation network management", - "services": { - "reconfigure": "Reconfigure ZHA device (heal device). Use this if you are having issues with the device. If the device in question is a battery powered device please ensure it is awake and accepting commands when you use this service.", - "updateDeviceName": "Set a custom name for this device in the device registry.", - "remove": "Remove a device from the ZigBee network." - }, - "device_card": { - "device_name_placeholder": "User given name", - "area_picker_label": "Area", - "update_name_button": "Update Name" - }, "add_device_page": { "header": "Zigbee Home Automation - Add Devices", "spinner": "Searching for ZHA Zigbee devices...", @@ -1053,6 +1068,10 @@ "unsaved_changes": "Unsaved changes", "saved": "Saved" }, + "edit_lovelace": { + "header": "Title of your Lovelace UI", + "explanation": "This title is shown above all your views in Lovelace." + }, "edit_view": { "header": "View Configuration", "add": "Add view", diff --git a/src/translations/translationMetadata.json b/src/translations/translationMetadata.json index bb18f04ba3..9fe6e8de64 100644 --- a/src/translations/translationMetadata.json +++ b/src/translations/translationMetadata.json @@ -70,6 +70,9 @@ "hu": { "nativeName": "Magyar" }, + "hy": { + "nativeName": "Հայերեն" + }, "id": { "nativeName": "Indonesia" }, diff --git a/yarn.lock b/yarn.lock index 845883feff..7ccf430710 100644 --- a/yarn.lock +++ b/yarn.lock @@ -847,10 +847,10 @@ dependencies: "@material/feature-targeting" "^0.44.1" -"@mdi/svg@4.2.95": - version "4.2.95" - resolved "https://registry.yarnpkg.com/@mdi/svg/-/svg-4.2.95.tgz#05d45a4391da211da3de2e0e25acad8272c23d67" - integrity sha512-99vNFQO8g8YakSBNa4pNx0CxmgOOvvme8fHZCPOZsgVTaW8sJepS3zugUb/csU+tDTnlRztDoPAtzcXvn2WzwA== +"@mdi/svg@4.3.95": + version "4.3.95" + resolved "https://registry.yarnpkg.com/@mdi/svg/-/svg-4.3.95.tgz#f2121132baab9e8953ee7ef71834cbe2f03065bb" + integrity sha512-RRda3q+270vhiL0Nt7oyeGX03zndEzkGJQJSz8dny1Yjwx2iVRUz51Xop6PTBPaEH4csa3sRkFY3q2PeIa2fKg== "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" @@ -8233,10 +8233,10 @@ js-yaml@3.12.0: argparse "^1.0.7" esprima "^4.0.0" -js-yaml@^3.12.0, js-yaml@^3.13.0, js-yaml@^3.7.0: - version "3.13.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.0.tgz#38ee7178ac0eea2c97ff6d96fff4b18c7d8cf98e" - integrity sha512-pZZoSxcCYco+DIKBTimr67J6Hy+EYGZDY/HCWC+iAEA9h1ByhMXAIVUXMcMFpOCxQ/xjXmPI2MkDL5HRm5eFrQ== +js-yaml@^3.12.0, js-yaml@^3.13.0, js-yaml@^3.13.1, js-yaml@^3.7.0: + version "3.13.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" + integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== dependencies: argparse "^1.0.7" esprima "^4.0.0" @@ -9502,9 +9502,9 @@ mississippi@^3.0.0: through2 "^2.0.0" mixin-deep@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe" - integrity sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ== + version "1.3.2" + resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566" + integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA== dependencies: for-in "^1.0.2" is-extendable "^1.0.1"