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.NO_AUTOMATION_TEXT}
+
+
+ ${this.UNKNOWN_AUTOMATION_TEXT}
+
+ ${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`
-
-
-
- No triggers
-
-
- Unknown trigger
-
- ${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
+
Invalid JSON
@@ -153,6 +153,12 @@ class HaPanelDevService extends PolymerElement {
+
+
+
+ Fill Example Data
+
+
@@ -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`
+
+
+
+
+
+ ${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"