@@ -307,7 +308,7 @@ export class HcConnect extends LitElement {
color: darkred;
}
- mwc-button ha-icon {
+ mwc-button ha-svg-icon {
margin-left: 8px;
}
diff --git a/demo/src/custom-cards/cast-demo-row.ts b/demo/src/custom-cards/cast-demo-row.ts
index 8fbecb4eb5..e400031878 100644
--- a/demo/src/custom-cards/cast-demo-row.ts
+++ b/demo/src/custom-cards/cast-demo-row.ts
@@ -1,3 +1,4 @@
+import { mdiTelevision } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, state } from "lit/decorators";
import { CastManager } from "../../../src/cast/cast_manager";
@@ -27,7 +28,7 @@ class CastDemoRow extends LitElement implements LovelaceRow {
return html``;
}
return html`
-
Show Chromecast interface
@@ -72,7 +73,7 @@ class CastDemoRow extends LitElement implements LovelaceRow {
display: flex;
align-items: center;
}
- ha-icon {
+ ha-svg-icon {
padding: 8px;
color: var(--paper-item-icon-color);
}
diff --git a/demo/src/stubs/area_registry.ts b/demo/src/stubs/area_registry.ts
new file mode 100644
index 0000000000..b7d8e5a34b
--- /dev/null
+++ b/demo/src/stubs/area_registry.ts
@@ -0,0 +1,7 @@
+import { AreaRegistryEntry } from "../../../src/data/area_registry";
+import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
+
+export const mockAreaRegistry = (
+ hass: MockHomeAssistant,
+ data: AreaRegistryEntry[] = []
+) => hass.mockWS("config/area_registry/list", () => data);
diff --git a/demo/src/stubs/device_registry.ts b/demo/src/stubs/device_registry.ts
new file mode 100644
index 0000000000..28c47e4a96
--- /dev/null
+++ b/demo/src/stubs/device_registry.ts
@@ -0,0 +1,7 @@
+import { DeviceRegistryEntry } from "../../../src/data/device_registry";
+import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
+
+export const mockDeviceRegistry = (
+ hass: MockHomeAssistant,
+ data: DeviceRegistryEntry[] = []
+) => hass.mockWS("config/device_registry/list", () => data);
diff --git a/demo/src/stubs/entity_registry.ts b/demo/src/stubs/entity_registry.ts
new file mode 100644
index 0000000000..8f548629e7
--- /dev/null
+++ b/demo/src/stubs/entity_registry.ts
@@ -0,0 +1,7 @@
+import { EntityRegistryEntry } from "../../../src/data/entity_registry";
+import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
+
+export const mockEntityRegistry = (
+ hass: MockHomeAssistant,
+ data: EntityRegistryEntry[] = []
+) => hass.mockWS("config/entity_registry/list", () => data);
diff --git a/demo/src/stubs/hassio_supervisor.ts b/demo/src/stubs/hassio_supervisor.ts
new file mode 100644
index 0000000000..95c0d330d4
--- /dev/null
+++ b/demo/src/stubs/hassio_supervisor.ts
@@ -0,0 +1,59 @@
+import { HassioSupervisorInfo } from "../../../src/data/hassio/supervisor";
+import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
+
+export const mockHassioSupervisor = (hass: MockHomeAssistant) => {
+ hass.config.components.push("hassio");
+ hass.mockWS("supervisor/api", (msg) => {
+ if (msg.endpoint === "/supervisor/info") {
+ const data: HassioSupervisorInfo = {
+ version: "2021.10.dev0805",
+ version_latest: "2021.10.dev0806",
+ update_available: true,
+ channel: "dev",
+ arch: "aarch64",
+ supported: true,
+ healthy: true,
+ ip_address: "172.30.32.2",
+ wait_boot: 5,
+ timezone: "America/Los_Angeles",
+ logging: "info",
+ debug: false,
+ debug_block: false,
+ diagnostics: true,
+ addons: [
+ {
+ name: "Visual Studio Code",
+ slug: "a0d7b954_vscode",
+ description:
+ "Fully featured VSCode experience, to edit your HA config in the browser, including auto-completion!",
+ state: "started",
+ version: "3.6.2",
+ version_latest: "3.6.2",
+ update_available: false,
+ repository: "a0d7b954",
+ icon: true,
+ logo: true,
+ },
+ {
+ name: "Z-Wave JS",
+ slug: "core_zwave_js",
+ description:
+ "Control a ZWave network with Home Assistant Z-Wave JS",
+ state: "started",
+ version: "0.1.45",
+ version_latest: "0.1.45",
+ update_available: false,
+ repository: "core",
+ icon: true,
+ logo: true,
+ },
+ ] as any,
+ addons_repositories: [
+ "https://github.com/hassio-addons/repository",
+ ] as any,
+ };
+ return data;
+ }
+ return Promise.reject(`${msg.method} ${msg.endpoint} is not implemented`);
+ });
+};
diff --git a/gallery/script/netlify_build_gallery b/gallery/script/netlify_build_gallery
new file mode 100755
index 0000000000..173b77d73f
--- /dev/null
+++ b/gallery/script/netlify_build_gallery
@@ -0,0 +1,35 @@
+#!/bin/bash
+
+TARGET_LABEL="Needs gallery preview"
+
+if [[ "$NETLIFY" != "true" ]]; then
+ echo "This script can only be run on Netlify"
+ exit 1
+fi
+
+function createStatus() {
+ state="$1"
+ description="$2"
+ target_url="$3"
+ curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: token $GITHUB_TOKEN" \
+ "https://api.github.com/repos/home-assistant/frontend/statuses/$COMMIT_REF" \
+ -d '{"state": "'"${state}"'", "context": "Netlify/Gallery Preview Build", "description": "'"$description"'", "target_url": "'"$target_url"'"}'
+}
+
+
+if [[ "${PULL_REQUEST}" == "false" ]]; then
+ gulp build-gallery
+else
+ if [[ "$(curl -sSLf -H "Accept: application/vnd.github.v3+json" -H "Authorization: token $GITHUB_TOKEN" \
+ "https://api.github.com/repos/home-assistant/frontend/pulls/${REVIEW_ID}" | jq '.labels[].name' -r)" =~ "$TARGET_LABEL" ]]; then
+ createStatus "pending" "Building gallery preview" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID"
+ gulp build-gallery
+ if [ $? -eq 0 ]; then
+ createStatus "success" "Build complete" "$DEPLOY_URL"
+ else
+ createStatus "error" "Build failed" "https://app.netlify.com/sites/home-assistant-gallery/deploys/$BUILD_ID"
+ fi
+ else
+ createStatus "success" "Build was not requested by PR label"
+ fi
+fi
diff --git a/gallery/src/components/demo-black-white-row.ts b/gallery/src/components/demo-black-white-row.ts
new file mode 100644
index 0000000000..a3b1233ffb
--- /dev/null
+++ b/gallery/src/components/demo-black-white-row.ts
@@ -0,0 +1,143 @@
+import { Button } from "@material/mwc-button";
+import { html, LitElement, css, TemplateResult } from "lit";
+import { customElement, property } from "lit/decorators";
+import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
+import { fireEvent } from "../../../src/common/dom/fire_event";
+
+@customElement("demo-black-white-row")
+class DemoBlackWhiteRow extends LitElement {
+ @property() title!: string;
+
+ @property() value!: any;
+
+ @property() disabled = false;
+
+ protected render(): TemplateResult {
+ return html`
+
+
+
+
+
+
+
+
+ Submit
+
+
+
+
+
+
+
+
+
+
+
+ Submit
+
+
+
+
${JSON.stringify(this.value, undefined, 2)}
+
+
+ `;
+ }
+
+ firstUpdated(changedProps) {
+ super.firstUpdated(changedProps);
+ applyThemesOnElement(
+ this.shadowRoot!.querySelector(".dark"),
+ {
+ default_theme: "default",
+ default_dark_theme: "default",
+ themes: {},
+ darkMode: false,
+ },
+ "default",
+ { dark: true }
+ );
+ }
+
+ handleSubmit(ev) {
+ const content = (ev.target as Button).closest(".content")!;
+ fireEvent(this, "submitted" as any, {
+ slot: content.classList.contains("light") ? "light" : "dark",
+ });
+ }
+
+ static styles = css`
+ .row {
+ display: flex;
+ }
+ .content {
+ padding: 50px 0;
+ background-color: var(--primary-background-color);
+ }
+ .light {
+ flex: 1;
+ padding-left: 50px;
+ padding-right: 50px;
+ box-sizing: border-box;
+ }
+ .light ha-card {
+ margin-left: auto;
+ }
+ .dark {
+ display: flex;
+ flex: 1;
+ padding-left: 50px;
+ box-sizing: border-box;
+ flex-wrap: wrap;
+ }
+ ha-card {
+ width: 400px;
+ }
+ pre {
+ width: 300px;
+ margin: 0 16px 0;
+ overflow: auto;
+ color: var(--primary-text-color);
+ }
+ .card-actions {
+ display: flex;
+ flex-direction: row-reverse;
+ border-top: none;
+ }
+ @media only screen and (max-width: 1500px) {
+ .light {
+ flex: initial;
+ }
+ }
+ @media only screen and (max-width: 1000px) {
+ .light,
+ .dark {
+ padding: 16px;
+ }
+ .row,
+ .dark {
+ flex-direction: column;
+ }
+ ha-card {
+ margin: 0 auto;
+ width: 100%;
+ max-width: 400px;
+ }
+ pre {
+ margin: 16px auto;
+ }
+ }
+ `;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "demo-black-white-row": DemoBlackWhiteRow;
+ }
+}
diff --git a/gallery/src/demos/demo-automation-editor-action.ts b/gallery/src/demos/demo-automation-editor-action.ts
new file mode 100644
index 0000000000..1aea950b92
--- /dev/null
+++ b/gallery/src/demos/demo-automation-editor-action.ts
@@ -0,0 +1,91 @@
+/* eslint-disable lit/no-template-arrow */
+import { LitElement, TemplateResult, html } from "lit";
+import { customElement, state } from "lit/decorators";
+import { provideHass } from "../../../src/fake_data/provide_hass";
+import type { HomeAssistant } from "../../../src/types";
+import "../components/demo-black-white-row";
+import { mockEntityRegistry } from "../../../demo/src/stubs/entity_registry";
+import { mockDeviceRegistry } from "../../../demo/src/stubs/device_registry";
+import { mockAreaRegistry } from "../../../demo/src/stubs/area_registry";
+import { mockHassioSupervisor } from "../../../demo/src/stubs/hassio_supervisor";
+import "../../../src/panels/config/automation/action/ha-automation-action";
+import { HaChooseAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-choose";
+import { HaDelayAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-delay";
+import { HaDeviceAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-device_id";
+import { HaEventAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-event";
+import { HaRepeatAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-repeat";
+import { HaSceneAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-scene";
+import { HaServiceAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-service";
+import { HaWaitForTriggerAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-wait_for_trigger";
+import { HaWaitAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-wait_template";
+import { Action } from "../../../src/data/script";
+import { HaConditionAction } from "../../../src/panels/config/automation/action/types/ha-automation-action-condition";
+
+const SCHEMAS: { name: string; actions: Action[] }[] = [
+ { name: "Event", actions: [HaEventAction.defaultConfig] },
+ { name: "Device", actions: [HaDeviceAction.defaultConfig] },
+ { name: "Service", actions: [HaServiceAction.defaultConfig] },
+ { name: "Condition", actions: [HaConditionAction.defaultConfig] },
+ { name: "Delay", actions: [HaDelayAction.defaultConfig] },
+ { name: "Scene", actions: [HaSceneAction.defaultConfig] },
+ { name: "Wait", actions: [HaWaitAction.defaultConfig] },
+ { name: "WaitForTrigger", actions: [HaWaitForTriggerAction.defaultConfig] },
+ { name: "Repeat", actions: [HaRepeatAction.defaultConfig] },
+ { name: "Choose", actions: [HaChooseAction.defaultConfig] },
+ { name: "Variables", actions: [{ variables: { hello: "1" } }] },
+];
+
+@customElement("demo-automation-editor-action")
+class DemoHaAutomationEditorAction extends LitElement {
+ @state() private hass!: HomeAssistant;
+
+ private data: any = SCHEMAS.map((info) => info.actions);
+
+ constructor() {
+ super();
+ const hass = provideHass(this);
+ hass.updateTranslations(null, "en");
+ hass.updateTranslations("config", "en");
+ mockEntityRegistry(hass);
+ mockDeviceRegistry(hass);
+ mockAreaRegistry(hass);
+ mockHassioSupervisor(hass);
+ }
+
+ protected render(): TemplateResult {
+ const valueChanged = (ev) => {
+ const sampleIdx = ev.target.sampleIdx;
+ this.data[sampleIdx] = ev.detail.value;
+ this.requestUpdate();
+ };
+ return html`
+ ${SCHEMAS.map(
+ (info, sampleIdx) => html`
+
+ ${["light", "dark"].map(
+ (slot) =>
+ html`
+
+ `
+ )}
+
+ `
+ )}
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "demo-ha-automation-editor-action": DemoHaAutomationEditorAction;
+ }
+}
diff --git a/gallery/src/demos/demo-automation-editor-condition.ts b/gallery/src/demos/demo-automation-editor-condition.ts
new file mode 100644
index 0000000000..4b1ebd8a13
--- /dev/null
+++ b/gallery/src/demos/demo-automation-editor-condition.ts
@@ -0,0 +1,127 @@
+/* eslint-disable lit/no-template-arrow */
+import { LitElement, TemplateResult, html } from "lit";
+import { customElement, state } from "lit/decorators";
+import { provideHass } from "../../../src/fake_data/provide_hass";
+import type { HomeAssistant } from "../../../src/types";
+import "../components/demo-black-white-row";
+import { mockEntityRegistry } from "../../../demo/src/stubs/entity_registry";
+import { mockDeviceRegistry } from "../../../demo/src/stubs/device_registry";
+import { mockAreaRegistry } from "../../../demo/src/stubs/area_registry";
+import { mockHassioSupervisor } from "../../../demo/src/stubs/hassio_supervisor";
+import type { Condition } from "../../../src/data/automation";
+import "../../../src/panels/config/automation/condition/ha-automation-condition";
+import { HaDeviceCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-device";
+import { HaLogicalCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-logical";
+import HaNumericStateCondition from "../../../src/panels/config/automation/condition/types/ha-automation-condition-numeric_state";
+import { HaStateCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-state";
+import { HaSunCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-sun";
+import { HaTemplateCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-template";
+import { HaTimeCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-time";
+import { HaTriggerCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-trigger";
+import { HaZoneCondition } from "../../../src/panels/config/automation/condition/types/ha-automation-condition-zone";
+
+const SCHEMAS: { name: string; conditions: Condition[] }[] = [
+ {
+ name: "State",
+ conditions: [{ condition: "state", ...HaStateCondition.defaultConfig }],
+ },
+ {
+ name: "Numeric State",
+ conditions: [
+ { condition: "numeric_state", ...HaNumericStateCondition.defaultConfig },
+ ],
+ },
+ {
+ name: "Sun",
+ conditions: [{ condition: "sun", ...HaSunCondition.defaultConfig }],
+ },
+ {
+ name: "Zone",
+ conditions: [{ condition: "zone", ...HaZoneCondition.defaultConfig }],
+ },
+ {
+ name: "Time",
+ conditions: [{ condition: "time", ...HaTimeCondition.defaultConfig }],
+ },
+ {
+ name: "Template",
+ conditions: [
+ { condition: "template", ...HaTemplateCondition.defaultConfig },
+ ],
+ },
+ {
+ name: "Device",
+ conditions: [{ condition: "device", ...HaDeviceCondition.defaultConfig }],
+ },
+ {
+ name: "And",
+ conditions: [{ condition: "and", ...HaLogicalCondition.defaultConfig }],
+ },
+ {
+ name: "Or",
+ conditions: [{ condition: "or", ...HaLogicalCondition.defaultConfig }],
+ },
+ {
+ name: "Not",
+ conditions: [{ condition: "not", ...HaLogicalCondition.defaultConfig }],
+ },
+ {
+ name: "Trigger",
+ conditions: [{ condition: "trigger", ...HaTriggerCondition.defaultConfig }],
+ },
+];
+
+@customElement("demo-automation-editor-condition")
+class DemoHaAutomationEditorCondition extends LitElement {
+ @state() private hass!: HomeAssistant;
+
+ private data: any = SCHEMAS.map((info) => info.conditions);
+
+ constructor() {
+ super();
+ const hass = provideHass(this);
+ hass.updateTranslations(null, "en");
+ hass.updateTranslations("config", "en");
+ mockEntityRegistry(hass);
+ mockDeviceRegistry(hass);
+ mockAreaRegistry(hass);
+ mockHassioSupervisor(hass);
+ }
+
+ protected render(): TemplateResult {
+ const valueChanged = (ev) => {
+ const sampleIdx = ev.target.sampleIdx;
+ this.data[sampleIdx] = ev.detail.value;
+ this.requestUpdate();
+ };
+ return html`
+ ${SCHEMAS.map(
+ (info, sampleIdx) => html`
+
+ ${["light", "dark"].map(
+ (slot) =>
+ html`
+
+ `
+ )}
+
+ `
+ )}
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "demo-ha-automation-editor-condition": DemoHaAutomationEditorCondition;
+ }
+}
diff --git a/gallery/src/demos/demo-automation-editor-trigger.ts b/gallery/src/demos/demo-automation-editor-trigger.ts
new file mode 100644
index 0000000000..0bc04b7435
--- /dev/null
+++ b/gallery/src/demos/demo-automation-editor-trigger.ts
@@ -0,0 +1,159 @@
+/* eslint-disable lit/no-template-arrow */
+import { LitElement, TemplateResult, html } from "lit";
+import { customElement, state } from "lit/decorators";
+import { provideHass } from "../../../src/fake_data/provide_hass";
+import type { HomeAssistant } from "../../../src/types";
+import "../components/demo-black-white-row";
+import { mockEntityRegistry } from "../../../demo/src/stubs/entity_registry";
+import { mockDeviceRegistry } from "../../../demo/src/stubs/device_registry";
+import { mockAreaRegistry } from "../../../demo/src/stubs/area_registry";
+import { mockHassioSupervisor } from "../../../demo/src/stubs/hassio_supervisor";
+import type { Trigger } from "../../../src/data/automation";
+import { HaGeolocationTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-geo_location";
+import { HaEventTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-event";
+import { HaHassTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-homeassistant";
+import { HaNumericStateTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state";
+import { HaSunTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-sun";
+import { HaTagTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-tag";
+import { HaTemplateTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-template";
+import { HaTimeTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-time";
+import { HaTimePatternTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-time_pattern";
+import { HaWebhookTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-webhook";
+import { HaZoneTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-zone";
+import { HaDeviceTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-device";
+import { HaStateTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-state";
+import { HaMQTTTrigger } from "../../../src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt";
+import "../../../src/panels/config/automation/trigger/ha-automation-trigger";
+
+const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
+ {
+ name: "State",
+ triggers: [{ platform: "state", ...HaStateTrigger.defaultConfig }],
+ },
+
+ {
+ name: "MQTT",
+ triggers: [{ platform: "mqtt", ...HaMQTTTrigger.defaultConfig }],
+ },
+
+ {
+ name: "GeoLocation",
+ triggers: [
+ { platform: "geo_location", ...HaGeolocationTrigger.defaultConfig },
+ ],
+ },
+
+ {
+ name: "Home Assistant",
+ triggers: [{ platform: "homeassistant", ...HaHassTrigger.defaultConfig }],
+ },
+
+ {
+ name: "Numeric State",
+ triggers: [
+ { platform: "numeric_state", ...HaNumericStateTrigger.defaultConfig },
+ ],
+ },
+
+ {
+ name: "Sun",
+ triggers: [{ platform: "sun", ...HaSunTrigger.defaultConfig }],
+ },
+
+ {
+ name: "Time Pattern",
+ triggers: [
+ { platform: "time_pattern", ...HaTimePatternTrigger.defaultConfig },
+ ],
+ },
+
+ {
+ name: "Webhook",
+ triggers: [{ platform: "webhook", ...HaWebhookTrigger.defaultConfig }],
+ },
+
+ {
+ name: "Zone",
+ triggers: [{ platform: "zone", ...HaZoneTrigger.defaultConfig }],
+ },
+
+ {
+ name: "Tag",
+ triggers: [{ platform: "tag", ...HaTagTrigger.defaultConfig }],
+ },
+
+ {
+ name: "Time",
+ triggers: [{ platform: "time", ...HaTimeTrigger.defaultConfig }],
+ },
+
+ {
+ name: "Template",
+ triggers: [{ platform: "template", ...HaTemplateTrigger.defaultConfig }],
+ },
+
+ {
+ name: "Event",
+ triggers: [{ platform: "event", ...HaEventTrigger.defaultConfig }],
+ },
+
+ {
+ name: "Device Trigger",
+ triggers: [{ platform: "device", ...HaDeviceTrigger.defaultConfig }],
+ },
+];
+
+@customElement("demo-automation-editor-trigger")
+class DemoHaAutomationEditorTrigger extends LitElement {
+ @state() private hass!: HomeAssistant;
+
+ private data: any = SCHEMAS.map((info) => info.triggers);
+
+ constructor() {
+ super();
+ const hass = provideHass(this);
+ hass.updateTranslations(null, "en");
+ hass.updateTranslations("config", "en");
+ mockEntityRegistry(hass);
+ mockDeviceRegistry(hass);
+ mockAreaRegistry(hass);
+ mockHassioSupervisor(hass);
+ }
+
+ protected render(): TemplateResult {
+ const valueChanged = (ev) => {
+ const sampleIdx = ev.target.sampleIdx;
+ this.data[sampleIdx] = ev.detail.value;
+ this.requestUpdate();
+ };
+ return html`
+ ${SCHEMAS.map(
+ (info, sampleIdx) => html`
+
+ ${["light", "dark"].map(
+ (slot) =>
+ html`
+
+ `
+ )}
+
+ `
+ )}
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "demo-ha-automation-editor-trigger": DemoHaAutomationEditorTrigger;
+ }
+}
diff --git a/gallery/src/demos/demo-ha-bar.ts b/gallery/src/demos/demo-ha-bar.ts
new file mode 100644
index 0000000000..83f47f5dd6
--- /dev/null
+++ b/gallery/src/demos/demo-ha-bar.ts
@@ -0,0 +1,85 @@
+import { html, css, LitElement, TemplateResult } from "lit";
+import { customElement } from "lit/decorators";
+import { classMap } from "lit/directives/class-map";
+import "../../../src/components/ha-bar";
+import "../../../src/components/ha-card";
+
+const bars: {
+ min?: number;
+ max?: number;
+ value: number;
+ warning?: number;
+ error?: number;
+}[] = [
+ {
+ value: 33,
+ },
+ {
+ value: 150,
+ },
+ {
+ min: -10,
+ value: 0,
+ },
+ {
+ value: 80,
+ },
+ {
+ value: 200,
+ max: 13,
+ },
+ {
+ value: 4,
+ min: 13,
+ },
+];
+
+@customElement("demo-ha-bar")
+export class DemoHaBar extends LitElement {
+ protected render(): TemplateResult {
+ return html`
+ ${bars
+ .map((bar) => ({ min: 0, max: 100, warning: 70, error: 90, ...bar }))
+ .map(
+ (bar) => html`
+
+
+
Config: ${JSON.stringify(bar)}
+
bar.warning,
+ error: bar.value > bar.error,
+ })}
+ .min=${bar.min}
+ .max=${bar.max}
+ .value=${bar.value}
+ >
+
+
+
+ `
+ )}
+ `;
+ }
+
+ static get styles() {
+ return css`
+ ha-card {
+ max-width: 600px;
+ margin: 24px auto;
+ }
+ .warning {
+ --ha-bar-primary-color: var(--warning-color);
+ }
+ .error {
+ --ha-bar-primary-color: var(--error-color);
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "demo-ha-bar": DemoHaBar;
+ }
+}
diff --git a/gallery/src/demos/demo-ha-chip.ts b/gallery/src/demos/demo-ha-chip.ts
new file mode 100644
index 0000000000..8344d4eb64
--- /dev/null
+++ b/gallery/src/demos/demo-ha-chip.ts
@@ -0,0 +1,61 @@
+import { mdiHomeAssistant } from "@mdi/js";
+import { css, html, LitElement, TemplateResult } from "lit";
+import { customElement } from "lit/decorators";
+import "../../../src/components/ha-card";
+import "../../../src/components/ha-chip";
+import "../../../src/components/ha-svg-icon";
+
+const chips: {
+ icon?: string;
+ content?: string;
+}[] = [
+ {},
+ {
+ icon: mdiHomeAssistant,
+ },
+ {
+ content: "Content",
+ },
+ {
+ icon: mdiHomeAssistant,
+ content: "Content",
+ },
+];
+
+@customElement("demo-ha-chip")
+export class DemoHaChip extends LitElement {
+ protected render(): TemplateResult {
+ return html`
+
+
+ ${chips.map(
+ (chip) => html`
+
+ ${chip.icon
+ ? html`
+ `
+ : ""}
+ ${chip.content}
+
+ `
+ )}
+
+
+ `;
+ }
+
+ static get styles() {
+ return css`
+ ha-card {
+ max-width: 600px;
+ margin: 24px auto;
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "demo-ha-chip": DemoHaChip;
+ }
+}
diff --git a/gallery/src/demos/demo-ha-form.ts b/gallery/src/demos/demo-ha-form.ts
index 9f5f9a3d54..abc2cca571 100644
--- a/gallery/src/demos/demo-ha-form.ts
+++ b/gallery/src/demos/demo-ha-form.ts
@@ -1,23 +1,25 @@
/* eslint-disable lit/no-template-arrow */
-import { LitElement, TemplateResult, css, html } from "lit";
+import "@material/mwc-button";
+import { LitElement, TemplateResult, html } from "lit";
import { customElement } from "lit/decorators";
+import { computeInitialHaFormData } from "../../../src/components/ha-form/compute-initial-ha-form-data";
+import type { HaFormSchema } from "../../../src/components/ha-form/types";
import "../../../src/components/ha-form/ha-form";
-import "../../../src/components/ha-card";
-import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element";
-import type { HaFormSchema } from "../../../src/components/ha-form/ha-form";
+import "../components/demo-black-white-row";
const SCHEMAS: {
title: string;
translations?: Record
;
error?: Record;
schema: HaFormSchema[];
+ data?: Record;
}[] = [
{
title: "Authentication",
translations: {
username: "Username",
password: "Password",
- invalid_login: "Invalid login",
+ invalid_login: "Invalid username or password",
},
error: {
base: "invalid_login",
@@ -57,6 +59,11 @@ const SCHEMAS: {
optional: true,
default: 10,
},
+ {
+ type: "float",
+ name: "float",
+ required: true,
+ },
{
type: "string",
name: "string",
@@ -83,6 +90,80 @@ const SCHEMAS: {
optional: true,
default: ["default"],
},
+ {
+ type: "positive_time_period_dict",
+ name: "time",
+ required: true,
+ },
+ ],
+ },
+ {
+ title: "Numbers",
+ schema: [
+ {
+ type: "integer",
+ name: "int",
+ required: true,
+ },
+ {
+ type: "integer",
+ name: "int with default",
+ optional: true,
+ default: 10,
+ },
+ {
+ type: "integer",
+ name: "int range required",
+ required: true,
+ default: 5,
+ valueMin: 0,
+ valueMax: 10,
+ },
+ {
+ type: "integer",
+ name: "int range optional",
+ optional: true,
+ valueMin: 0,
+ valueMax: 10,
+ },
+ ],
+ },
+ {
+ title: "select",
+ schema: [
+ {
+ type: "select",
+ options: [
+ ["default", "Default"],
+ ["other", "Other"],
+ ],
+ name: "select",
+ required: true,
+ default: "default",
+ },
+ {
+ type: "select",
+ options: [
+ ["default", "Default"],
+ ["other", "Other"],
+ ],
+ name: "select optional",
+ optional: true,
+ },
+ {
+ type: "select",
+ options: [
+ ["default", "Default"],
+ ["other", "Other"],
+ ["uno", "mas"],
+ ["one", "more"],
+ ["and", "another_one"],
+ ["option", "1000"],
+ ],
+ name: "select many otions",
+ optional: true,
+ default: "default",
+ },
],
},
{
@@ -95,7 +176,7 @@ const SCHEMAS: {
other: "Other",
},
name: "multi",
- optional: true,
+ required: true,
default: ["default"],
},
{
@@ -108,101 +189,90 @@ const SCHEMAS: {
and: "another_one",
option: "1000",
},
- name: "multi",
+ name: "multi many otions",
optional: true,
default: ["default"],
},
],
},
+ {
+ title: "Field specific error",
+ data: {
+ new_password: "hello",
+ new_password_2: "bye",
+ },
+ translations: {
+ new_password: "New Password",
+ new_password_2: "Re-type Password",
+ not_match: "The passwords do not match",
+ },
+ error: {
+ new_password_2: "not_match",
+ },
+ schema: [
+ {
+ type: "string",
+ name: "new_password",
+ required: true,
+ },
+ {
+ type: "string",
+ name: "new_password_2",
+ required: true,
+ },
+ ],
+ },
];
@customElement("demo-ha-form")
class DemoHaForm extends LitElement {
- private lightModeData: any = [];
+ private data = SCHEMAS.map(
+ ({ schema, data }) => data || computeInitialHaFormData(schema)
+ );
- private darkModeData: any = [];
+ private disabled = SCHEMAS.map(() => false);
protected render(): TemplateResult {
return html`
${SCHEMAS.map((info, idx) => {
const translations = info.translations || {};
- const computeLabel = (schema) =>
- translations[schema.name] || schema.name;
- const computeError = (error) => translations[error] || error;
-
- return [
- [this.lightModeData, "light"],
- [this.darkModeData, "dark"],
- ].map(
- ([data, type]) => html`
-
-
-
- {
- data[idx] = e.detail.value;
- this.requestUpdate();
- }}
- >
-
-
-
${JSON.stringify(data[idx], undefined, 2)}
-
- `
- );
+ return html`
+ {
+ this.disabled[idx] = true;
+ this.requestUpdate();
+ setTimeout(() => {
+ this.disabled[idx] = false;
+ this.requestUpdate();
+ }, 2000);
+ }}
+ >
+ ${["light", "dark"].map(
+ (slot) => html`
+ translations[error] || error}
+ .computeLabel=${(schema) =>
+ translations[schema.name] || schema.name}
+ @value-changed=${(e) => {
+ this.data[idx] = e.detail.value;
+ this.requestUpdate();
+ }}
+ >
+ `
+ )}
+
+ `;
})}
`;
}
-
- firstUpdated(changedProps) {
- super.firstUpdated(changedProps);
- this.shadowRoot!.querySelectorAll("[data-type=dark]").forEach((el) => {
- applyThemesOnElement(
- el,
- {
- default_theme: "default",
- default_dark_theme: "default",
- themes: {},
- darkMode: false,
- },
- "default",
- { dark: true }
- );
- });
- }
-
- static styles = css`
- .row {
- margin: 0 auto;
- max-width: 800px;
- display: flex;
- padding: 50px;
- background-color: var(--primary-background-color);
- }
- ha-card {
- width: 100%;
- max-width: 384px;
- }
- pre {
- width: 400px;
- margin: 0 16px;
- overflow: auto;
- color: var(--primary-text-color);
- }
- @media only screen and (max-width: 800px) {
- .row {
- flex-direction: column;
- }
- pre {
- margin: 16px 0;
- }
- }
- `;
}
declare global {
diff --git a/gallery/src/demos/demo-ha-label-badge.ts b/gallery/src/demos/demo-ha-label-badge.ts
new file mode 100644
index 0000000000..277c539040
--- /dev/null
+++ b/gallery/src/demos/demo-ha-label-badge.ts
@@ -0,0 +1,122 @@
+import { html, css, LitElement, TemplateResult } from "lit";
+import { customElement } from "lit/decorators";
+import "../../../src/components/ha-label-badge";
+import "../../../src/components/ha-card";
+
+const colors = ["#03a9f4", "#ffa600", "#43a047"];
+
+const badges: {
+ label?: string;
+ description?: string;
+ image?: string;
+}[] = [
+ {
+ label: "label",
+ },
+ {
+ label: "label",
+ description: "Description",
+ },
+ {
+ description: "Description",
+ },
+ {
+ label: "label",
+ description: "Description",
+ image: "/images/living_room.png",
+ },
+ {
+ description: "Description",
+ image: "/images/living_room.png",
+ },
+ {
+ label: "label",
+ image: "/images/living_room.png",
+ },
+ {
+ image: "/images/living_room.png",
+ },
+ {
+ label: "big label",
+ },
+ {
+ label: "big label",
+ description: "Description",
+ },
+ {
+ label: "big label",
+ description: "Description",
+ image: "/images/living_room.png",
+ },
+];
+
+@customElement("demo-ha-label-badge")
+export class DemoHaLabelBadge extends LitElement {
+ protected render(): TemplateResult {
+ return html`
+
+
+ ${badges.map(
+ (badge) => html`
+
+
+ `
+ )}
+
+
+
+
+ ${badges.map(
+ (badge) => html`
+
+
+
+
${JSON.stringify(badge, null, 2)}
+
+ `
+ )}
+
+
+ `;
+ }
+
+ static get styles() {
+ return css`
+ ha-card {
+ max-width: 600px;
+ margin: 24px auto;
+ }
+ pre {
+ margin-left: 16px;
+ background-color: var(--markdown-code-background-color);
+ padding: 8px;
+ }
+ .badge {
+ display: flex;
+ flex-direction: row;
+ margin-bottom: 16px;
+ align-items: center;
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "demo-ha-label-badge": DemoHaLabelBadge;
+ }
+}
diff --git a/gallery/src/demos/demo-ha-selector.ts b/gallery/src/demos/demo-ha-selector.ts
new file mode 100644
index 0000000000..919a2c2419
--- /dev/null
+++ b/gallery/src/demos/demo-ha-selector.ts
@@ -0,0 +1,131 @@
+/* eslint-disable lit/no-template-arrow */
+import "@material/mwc-button";
+import { LitElement, TemplateResult, css, html } from "lit";
+import { customElement, state } from "lit/decorators";
+import "../../../src/components/ha-selector/ha-selector";
+import "../../../src/components/ha-settings-row";
+import { provideHass } from "../../../src/fake_data/provide_hass";
+import type { HomeAssistant } from "../../../src/types";
+import "../components/demo-black-white-row";
+import { BlueprintInput } from "../../../src/data/blueprint";
+import { mockEntityRegistry } from "../../../demo/src/stubs/entity_registry";
+import { mockDeviceRegistry } from "../../../demo/src/stubs/device_registry";
+import { mockAreaRegistry } from "../../../demo/src/stubs/area_registry";
+import { mockHassioSupervisor } from "../../../demo/src/stubs/hassio_supervisor";
+
+const SCHEMAS: {
+ name: string;
+ input: Record;
+}[] = [
+ {
+ name: "One of each",
+ input: {
+ entity: { name: "Entity", selector: { entity: {} } },
+ device: { name: "Device", selector: { device: {} } },
+ addon: { name: "Addon", selector: { addon: {} } },
+ area: { name: "Area", selector: { area: {} } },
+ target: { name: "Target", selector: { target: {} } },
+ number_box: {
+ name: "Number Box",
+ selector: {
+ number: {
+ min: 0,
+ max: 10,
+ mode: "box",
+ },
+ },
+ },
+ number_slider: {
+ name: "Number Slider",
+ selector: {
+ number: {
+ min: 0,
+ max: 10,
+ mode: "slider",
+ },
+ },
+ },
+ boolean: { name: "Boolean", selector: { boolean: {} } },
+ time: { name: "Time", selector: { time: {} } },
+ action: { name: "Action", selector: { action: {} } },
+ text: { name: "Text", selector: { text: { multiline: false } } },
+ text_multiline: {
+ name: "Text multiline",
+ selector: { text: { multiline: true } },
+ },
+ object: { name: "Object", selector: { object: {} } },
+ select: {
+ name: "Select",
+ selector: { select: { options: ["Option 1", "Option 2"] } },
+ },
+ },
+ },
+];
+
+@customElement("demo-ha-selector")
+class DemoHaSelector extends LitElement {
+ @state() private hass!: HomeAssistant;
+
+ private data = SCHEMAS.map(() => ({}));
+
+ constructor() {
+ super();
+ const hass = provideHass(this);
+ hass.updateTranslations(null, "en");
+ hass.updateTranslations("config", "en");
+ mockEntityRegistry(hass);
+ mockDeviceRegistry(hass);
+ mockAreaRegistry(hass);
+ mockHassioSupervisor(hass);
+ }
+
+ protected render(): TemplateResult {
+ return html`
+ ${SCHEMAS.map((info, idx) => {
+ const data = this.data[idx];
+ const valueChanged = (ev) => {
+ this.data[idx] = {
+ ...data,
+ [ev.target.key]: ev.detail.value,
+ };
+ this.requestUpdate();
+ };
+ return html`
+
+ ${["light", "dark"].map((slot) =>
+ Object.entries(info.input).map(
+ ([key, value]) =>
+ html`
+
+ ${value?.name || key}
+ ${value?.description}
+
+
+ `
+ )
+ )}
+
+ `;
+ })}
+ `;
+ }
+
+ static styles = css`
+ paper-input,
+ ha-selector {
+ width: 60;
+ }
+ `;
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "demo-ha-selector": DemoHaSelector;
+ }
+}
diff --git a/gallery/src/demos/demo-integration-card.ts b/gallery/src/demos/demo-integration-card.ts
index 0b08a0aa3e..4da42f1279 100644
--- a/gallery/src/demos/demo-integration-card.ts
+++ b/gallery/src/demos/demo-integration-card.ts
@@ -187,6 +187,7 @@ const createEntityRegistryEntries = (
device_id: "mock-device-id",
area_id: null,
disabled_by: null,
+ entity_category: null,
entity_id: "binary_sensor.updater",
name: null,
icon: null,
@@ -211,6 +212,7 @@ const createDeviceRegistryEntries = (
area_id: null,
name_by_user: null,
disabled_by: null,
+ configuration_url: null,
},
];
diff --git a/gallery/src/ha-gallery.js b/gallery/src/ha-gallery.js
index 9d09da0119..7f53dd717a 100644
--- a/gallery/src/ha-gallery.js
+++ b/gallery/src/ha-gallery.js
@@ -65,10 +65,11 @@ class HaGallery extends PolymerElement {
+ >
+
+
[[_withDefault(_demo, "Home Assistant Gallery")]]
diff --git a/hassio/src/addon-store/hassio-addon-store.ts b/hassio/src/addon-store/hassio-addon-store.ts
index a1dbe3237f..4cc55898a1 100644
--- a/hassio/src/addon-store/hassio-addon-store.ts
+++ b/hassio/src/addon-store/hassio-addon-store.ts
@@ -1,4 +1,3 @@
-import "@material/mwc-icon-button/mwc-icon-button";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical } from "@mdi/js";
@@ -18,7 +17,7 @@ import { navigate } from "../../../src/common/navigate";
import "../../../src/common/search/search-input";
import { extractSearchParam } from "../../../src/common/url/search-params";
import "../../../src/components/ha-button-menu";
-import "../../../src/components/ha-svg-icon";
+import "../../../src/components/ha-icon-button";
import {
HassioAddonInfo,
HassioAddonRepository,
@@ -92,9 +91,11 @@ class HassioAddonStore extends LitElement {
slot="toolbar-icon"
@action=${this._handleAction}
>
-
-
-
+
${this.supervisor.localize("store.repositories")}
@@ -113,6 +114,7 @@ class HassioAddonStore extends LitElement {
: html`
+ // @ts-expect-error supervisor does not implement [string, string] for select.options[]
+ schema.map((entry) =>
+ entry.type === "select"
+ ? {
+ ...entry,
+ options: entry.options.map((option) => [option, option]),
+ }
+ : entry
+ )
+ );
+
private _filteredShchema = memoizeOne(
(options: Record, schema: HaFormSchema[]) =>
schema.filter((entry) => entry.name in options || entry.required)
@@ -100,9 +113,11 @@ class HassioAddonConfig extends LitElement {
-
- `
+ @alert-action-clicked=${this._protectionToggled}
+ >
+ ${this.supervisor.localize(
+ "addon.dashboard.protection_mode.content"
+ )}
+
+ `
: ""}
@@ -249,6 +261,163 @@ class HassioAddonInfo extends LitElement {
>`}
+
+ ${this.addon.stage !== "stable"
+ ? html`
+
+
+ ${this.supervisor.localize(
+ `addon.dashboard.capability.stages.${this.addon.stage}`
+ )}
+ `
+ : ""}
+
+
+
+
+
+ ${this.supervisor.localize(
+ "addon.dashboard.capability.label.rating"
+ )}
+
+ ${this.addon.host_network
+ ? html`
+
+
+ ${this.supervisor.localize(
+ "addon.dashboard.capability.label.host"
+ )}
+
+ `
+ : ""}
+ ${this.addon.full_access
+ ? html`
+
+
+ ${this.supervisor.localize(
+ "addon.dashboard.capability.label.hardware"
+ )}
+
+ `
+ : ""}
+ ${this.addon.homeassistant_api
+ ? html`
+
+
+ ${this.supervisor.localize(
+ "addon.dashboard.capability.label.core"
+ )}
+
+ `
+ : ""}
+ ${this._computeHassioApi
+ ? html`
+
+
+ ${this.supervisor.localize(
+ `addon.dashboard.capability.role.${this.addon.hassio_role}`
+ ) || this.addon.hassio_role}
+
+ `
+ : ""}
+ ${this.addon.docker_api
+ ? html`
+
+
+ ${this.supervisor.localize(
+ "addon.dashboard.capability.label.docker"
+ )}
+
+ `
+ : ""}
+ ${this.addon.host_pid
+ ? html`
+
+
+ ${this.supervisor.localize(
+ "addon.dashboard.capability.label.host_pid"
+ )}
+
+ `
+ : ""}
+ ${this.addon.apparmor !== "default"
+ ? html`
+
+
+ ${this.supervisor.localize(
+ "addon.dashboard.capability.label.apparmor"
+ )}
+
+ `
+ : ""}
+ ${this.addon.auth_api
+ ? html`
+
+
+ ${this.supervisor.localize(
+ "addon.dashboard.capability.label.auth"
+ )}
+
+ `
+ : ""}
+ ${this.addon.ingress
+ ? html`
+
+
+ ${this.supervisor.localize(
+ "addon.dashboard.capability.label.ingress"
+ )}
+
+ `
+ : ""}
+
+
${this.addon.description}.
${this.supervisor.localize(
@@ -269,172 +438,6 @@ class HassioAddonInfo extends LitElement {
/>
`
: ""}
-
- ${this.addon.stage !== "stable"
- ? html`
-
- `
- : ""}
-
-
- ${this.addon.rating}
-
- ${this.addon.host_network
- ? html`
-
-
-
- `
- : ""}
- ${this.addon.full_access
- ? html`
-
-
-
- `
- : ""}
- ${this.addon.homeassistant_api
- ? html`
-
-
-
- `
- : ""}
- ${this._computeHassioApi
- ? html`
-
-
-
- `
- : ""}
- ${this.addon.docker_api
- ? html`
-
-
-
- `
- : ""}
- ${this.addon.host_pid
- ? html`
-
-
-
- `
- : ""}
- ${this.addon.apparmor
- ? html`
-
-
-
- `
- : ""}
- ${this.addon.auth_api
- ? html`
-
-
-
- `
- : ""}
- ${this.addon.ingress
- ? html`
-
-
-
- `
- : ""}
-
-
${this.addon.version
? html`
-
-
-
+
${this.supervisor?.localize("common.reload")}
@@ -216,13 +220,15 @@ export class HassioBackups extends LitElement {
`
: html`
-
-
-
+ >
${this.supervisor.localize("backup.delete_selected")}
@@ -368,7 +374,7 @@ export class HassioBackups extends LitElement {
margin-right: -12px;
}
.header-btns > mwc-button,
- .header-btns > mwc-icon-button {
+ .header-btns > ha-icon-button {
margin: 8px;
}
`,
diff --git a/hassio/src/components/hassio-upload-backup.ts b/hassio/src/components/hassio-upload-backup.ts
index bbb6f43d7f..70b326bff9 100644
--- a/hassio/src/components/hassio-upload-backup.ts
+++ b/hassio/src/components/hassio-upload-backup.ts
@@ -1,4 +1,3 @@
-import "@material/mwc-icon-button/mwc-icon-button";
import { mdiFolderUpload } from "@mdi/js";
import "@polymer/paper-input/paper-input-container";
import { html, LitElement, TemplateResult } from "lit";
@@ -6,9 +5,8 @@ import { customElement, state } from "lit/decorators";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/ha-circular-progress";
import "../../../src/components/ha-file-upload";
-import "../../../src/components/ha-svg-icon";
-import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { HassioBackup, uploadBackup } from "../../../src/data/hassio/backup";
+import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import { HomeAssistant } from "../../../src/types";
@@ -31,6 +29,7 @@ export class HassioUploadBackup extends LitElement {
public render(): TemplateResult {
return html`
Upload backup
-
-
-
+
${this._backup.name}
-
-
-
+
${this._restoringBackup
@@ -110,9 +113,11 @@ class HassioBackupDialog
@action=${this._handleMenuAction}
@closed=${stopPropagation}
>
-
-
-
+
Download Backup
Delete Backup
`
@@ -126,9 +131,6 @@ class HassioBackupDialog
haStyle,
haStyleDialog,
css`
- ha-svg-icon {
- color: var(--primary-text-color);
- }
ha-circular-progress {
display: block;
text-align: center;
@@ -139,6 +141,9 @@ class HassioBackupDialog
flex-shrink: 0;
display: block;
}
+ ha-icon-button {
+ color: var(--secondary-text-color);
+ }
`,
];
}
diff --git a/hassio/src/dialogs/hardware/dialog-hassio-hardware.ts b/hassio/src/dialogs/hardware/dialog-hassio-hardware.ts
index a8d1481c84..b3e82c46d2 100755
--- a/hassio/src/dialogs/hardware/dialog-hassio-hardware.ts
+++ b/hassio/src/dialogs/hardware/dialog-hassio-hardware.ts
@@ -7,6 +7,7 @@ import "../../../../src/common/search/search-input";
import { stringCompare } from "../../../../src/common/string/compare";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel";
+import "../../../../src/components/ha-icon-button";
import { HassioHardwareInfo } from "../../../../src/data/hassio/hardware";
import { dump } from "../../../../src/resources/js-yaml-dump";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
@@ -70,10 +71,13 @@ class HassioHardwareDialog extends LitElement {
${this._dialogParams.supervisor.localize("dialog.hardware.title")}
-
-
-
+
${this.supervisor.localize("dialog.network.title")}
-
-
-
+
${this._interfaces.length > 1
? html`
-
-
-
+ >
`
)
@@ -234,7 +232,7 @@ class HassioRegistriesDialog extends LitElement {
mwc-button {
margin-left: 8px;
}
- mwc-icon-button {
+ ha-icon-button {
color: var(--error-color);
margin: -10px;
}
diff --git a/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts b/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts
index 64eb324a20..03f1fc0395 100644
--- a/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts
+++ b/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts
@@ -1,5 +1,4 @@
import "@material/mwc-button/mwc-button";
-import "@material/mwc-icon-button/mwc-icon-button";
import { mdiDelete } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
@@ -13,7 +12,7 @@ import { caseInsensitiveStringCompare } from "../../../../src/common/string/comp
import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-circular-progress";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
-import "../../../../src/components/ha-svg-icon";
+import "../../../../src/components/ha-icon-button";
import {
fetchHassioAddonsInfo,
HassioAddonRepository,
@@ -90,15 +89,14 @@ class HassioRepositoriesDialog extends LitElement {
${repo.maintainer}
${repo.url}
-
-
-
+ >
`
)
diff --git a/hassio/src/dialogs/update/dialog-supervisor-update.ts b/hassio/src/dialogs/update/dialog-supervisor-update.ts
index ca832ff776..cc4f80bb09 100644
--- a/hassio/src/dialogs/update/dialog-supervisor-update.ts
+++ b/hassio/src/dialogs/update/dialog-supervisor-update.ts
@@ -6,7 +6,6 @@ import "../../../../src/components/ha-alert";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-settings-row";
-import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-switch";
import {
extractApiErrorMessage,
diff --git a/hassio/src/hassio-main.ts b/hassio/src/hassio-main.ts
index 93181aa106..8ded32bbf0 100644
--- a/hassio/src/hassio-main.ts
+++ b/hassio/src/hassio-main.ts
@@ -113,12 +113,6 @@ export class HassioMain extends SupervisorBaseElement {
: this.hass.themes.default_theme);
themeSettings = this.hass.selectedTheme;
- if (themeSettings?.dark === undefined) {
- themeSettings = {
- ...this.hass.selectedTheme,
- dark: this.hass.themes.darkMode,
- };
- }
} else {
themeName =
(this.hass.selectedTheme as unknown as string) ||
diff --git a/hassio/src/ingress-view/hassio-ingress-view.ts b/hassio/src/ingress-view/hassio-ingress-view.ts
index 91c338a3e5..9deb9bb45a 100644
--- a/hassio/src/ingress-view/hassio-ingress-view.ts
+++ b/hassio/src/ingress-view/hassio-ingress-view.ts
@@ -12,6 +12,7 @@ import { fireEvent } from "../../../src/common/dom/fire_event";
import { navigate } from "../../../src/common/navigate";
import { extractSearchParam } from "../../../src/common/url/search-params";
import { nextRender } from "../../../src/common/util/render-status";
+import "../../../src/components/ha-icon-button";
import {
fetchHassioAddonInfo,
HassioAddonDetails,
@@ -72,12 +73,11 @@ class HassioIngressView extends LitElement {
return html`${this.narrow || this.hass.dockedSidebar === "always_hidden"
? html`
${iframe}`
@@ -241,7 +241,7 @@ class HassioIngressView extends LitElement {
flex-grow: 1;
}
- mwc-icon-button {
+ ha-icon-button {
pointer-events: auto;
}
diff --git a/hassio/src/system/hassio-host-info.ts b/hassio/src/system/hassio-host-info.ts
index 89aab52da9..64b7f90e8f 100644
--- a/hassio/src/system/hassio-host-info.ts
+++ b/hassio/src/system/hassio-host-info.ts
@@ -9,6 +9,7 @@ import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card";
+import "../../../src/components/ha-icon-button";
import "../../../src/components/ha-settings-row";
import {
extractApiErrorMessage,
@@ -181,9 +182,11 @@ class HassioHostInfo extends LitElement {
: ""}
-
-
-
+
html`
- ${UNSUPPORTED_REASON_URL[reason]
- ? html`
- ${this.supervisor.localize(
- `system.supervisor.unsupported_reason.${reason}`
- ) || reason}
- `
- : reason}
+
+ ${this.supervisor.localize(
+ `system.supervisor.unsupported_reason.${reason}`
+ ) || reason}
+
`
)}
@@ -456,20 +435,19 @@ class HassioSupervisorInfo extends LitElement {
${this.supervisor.resolution.unhealthy.map(
(reason) => html`
- ${UNHEALTHY_REASON_URL[reason]
- ? html`
- ${this.supervisor.localize(
- `system.supervisor.unhealthy_reason.${reason}`
- ) || reason}
- `
- : reason}
+
+ ${this.supervisor.localize(
+ `system.supervisor.unhealthy_reason.${reason}`
+ ) || reason}
+
`
)}
diff --git a/package.json b/package.json
index 3a3dfe5645..c50c3c9ad6 100644
--- a/package.json
+++ b/package.json
@@ -22,23 +22,23 @@
"license": "Apache-2.0",
"dependencies": {
"@braintree/sanitize-url": "^5.0.2",
- "@codemirror/commands": "^0.19.2",
- "@codemirror/gutter": "^0.19.1",
- "@codemirror/highlight": "^0.19.2",
+ "@codemirror/commands": "^0.19.5",
+ "@codemirror/gutter": "^0.19.3",
+ "@codemirror/highlight": "^0.19.6",
"@codemirror/history": "^0.19.0",
"@codemirror/legacy-modes": "^0.19.0",
- "@codemirror/rectangular-selection": "^0.19.0",
- "@codemirror/search": "^0.19.0",
- "@codemirror/state": "^0.19.1",
- "@codemirror/stream-parser": "^0.19.1",
- "@codemirror/text": "^0.19.2",
- "@codemirror/view": "^0.19.4",
- "@formatjs/intl-datetimeformat": "^4.2.4",
- "@formatjs/intl-getcanonicallocales": "^1.7.3",
- "@formatjs/intl-locale": "^2.4.38",
- "@formatjs/intl-numberformat": "^7.2.4",
- "@formatjs/intl-pluralrules": "^4.1.4",
- "@formatjs/intl-relativetimeformat": "^9.3.1",
+ "@codemirror/rectangular-selection": "^0.19.1",
+ "@codemirror/search": "^0.19.2",
+ "@codemirror/state": "^0.19.2",
+ "@codemirror/stream-parser": "^0.19.2",
+ "@codemirror/text": "^0.19.4",
+ "@codemirror/view": "^0.19.9",
+ "@formatjs/intl-datetimeformat": "^4.2.5",
+ "@formatjs/intl-getcanonicallocales": "^1.8.0",
+ "@formatjs/intl-locale": "^2.4.40",
+ "@formatjs/intl-numberformat": "^7.2.5",
+ "@formatjs/intl-pluralrules": "^4.1.5",
+ "@formatjs/intl-relativetimeformat": "^9.3.2",
"@formatjs/intl-utils": "^3.8.4",
"@fullcalendar/common": "5.9.0",
"@fullcalendar/core": "5.9.0",
@@ -46,45 +46,38 @@
"@fullcalendar/interaction": "5.9.0",
"@fullcalendar/list": "5.9.0",
"@lit-labs/virtualizer": "patch:@lit-labs/virtualizer@0.6.0#./.yarn/patches/@lit-labs/virtualizer/0.7.0.patch",
- "@material/chips": "13.0.0-canary.65125b3a6.0",
- "@material/data-table": "13.0.0-canary.65125b3a6.0",
- "@material/mwc-button": "0.25.1",
- "@material/mwc-checkbox": "0.25.1",
- "@material/mwc-circular-progress": "0.25.1",
- "@material/mwc-dialog": "0.25.1",
- "@material/mwc-fab": "0.25.1",
- "@material/mwc-formfield": "0.25.1",
- "@material/mwc-icon-button": "0.25.1",
- "@material/mwc-linear-progress": "0.25.1",
- "@material/mwc-list": "0.25.1",
- "@material/mwc-menu": "0.25.1",
- "@material/mwc-radio": "0.25.1",
- "@material/mwc-ripple": "0.25.1",
- "@material/mwc-switch": "0.25.1",
- "@material/mwc-tab": "0.25.1",
- "@material/mwc-tab-bar": "0.25.1",
- "@material/top-app-bar": "13.0.0-canary.65125b3a6.0",
- "@mdi/js": "6.2.95",
- "@mdi/svg": "6.2.95",
+ "@material/chips": "14.0.0-canary.261f2db59.0",
+ "@material/data-table": "14.0.0-canary.261f2db59.0",
+ "@material/mwc-button": "0.25.3",
+ "@material/mwc-checkbox": "0.25.3",
+ "@material/mwc-circular-progress": "0.25.3",
+ "@material/mwc-dialog": "0.25.3",
+ "@material/mwc-fab": "0.25.3",
+ "@material/mwc-formfield": "0.25.3",
+ "@material/mwc-icon-button": "patch:@material/mwc-icon-button@0.25.3#./.yarn/patches/@material/mwc-icon-button/remove-icon.patch",
+ "@material/mwc-linear-progress": "0.25.3",
+ "@material/mwc-list": "0.25.3",
+ "@material/mwc-menu": "0.25.3",
+ "@material/mwc-radio": "0.25.3",
+ "@material/mwc-ripple": "0.25.3",
+ "@material/mwc-select": "0.25.3",
+ "@material/mwc-slider": "0.25.3",
+ "@material/mwc-switch": "0.25.3",
+ "@material/mwc-tab": "0.25.3",
+ "@material/mwc-tab-bar": "0.25.3",
+ "@material/mwc-textfield": "0.25.3",
+ "@material/top-app-bar": "14.0.0-canary.261f2db59.0",
+ "@mdi/js": "6.4.95",
+ "@mdi/svg": "6.4.95",
"@polymer/app-layout": "^3.1.0",
"@polymer/iron-flex-layout": "^3.0.1",
"@polymer/iron-icon": "^3.0.1",
"@polymer/iron-input": "^3.0.1",
- "@polymer/iron-overlay-behavior": "^3.0.3",
"@polymer/iron-resizable-behavior": "^3.0.1",
- "@polymer/paper-checkbox": "^3.1.0",
- "@polymer/paper-dialog": "^3.0.1",
- "@polymer/paper-dialog-behavior": "^3.0.1",
- "@polymer/paper-dialog-scrollable": "^3.0.1",
"@polymer/paper-dropdown-menu": "^3.2.0",
"@polymer/paper-input": "^3.2.1",
"@polymer/paper-item": "^3.0.1",
"@polymer/paper-listbox": "^3.0.1",
- "@polymer/paper-menu-button": "^3.1.0",
- "@polymer/paper-progress": "^3.0.1",
- "@polymer/paper-radio-button": "^3.0.1",
- "@polymer/paper-radio-group": "^3.0.1",
- "@polymer/paper-ripple": "^3.0.2",
"@polymer/paper-slider": "^3.0.1",
"@polymer/paper-styles": "^3.0.1",
"@polymer/paper-tabs": "^3.1.0",
@@ -115,7 +108,7 @@
"js-yaml": "^4.1.0",
"leaflet": "^1.7.1",
"leaflet-draw": "^1.0.4",
- "lit": "^2.0.0",
+ "lit": "^2.0.2",
"lit-vaadin-helpers": "^0.2.1",
"marked": "^3.0.2",
"memoize-one": "^5.2.1",
@@ -187,7 +180,7 @@
"eslint-import-resolver-webpack": "^0.13.1",
"eslint-plugin-disable": "^2.0.1",
"eslint-plugin-import": "^2.24.2",
- "eslint-plugin-lit": "^1.5.1",
+ "eslint-plugin-lit": "^1.6.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-unused-imports": "^1.1.5",
"eslint-plugin-wc": "^1.3.2",
@@ -237,10 +230,10 @@
"resolutions": {
"@polymer/polymer": "patch:@polymer/polymer@3.4.1#./.yarn/patches/@polymer/polymer/pr-5569.patch",
"@webcomponents/webcomponentsjs": "^2.2.10",
- "lit": "^2.0.0",
- "lit-html": "2.0.0",
- "lit-element": "3.0.0",
- "@lit/reactive-element": "1.0.0"
+ "lit": "^2.0.2",
+ "lit-html": "2.0.1",
+ "lit-element": "3.0.1",
+ "@lit/reactive-element": "1.0.1"
},
"main": "src/home-assistant.js",
"husky": {
diff --git a/setup.py b/setup.py
index 21bd283ba7..c5f9e949f6 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
- version="20211007.1",
+ version="20211026.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/frontend",
author="The Home Assistant Authors",
diff --git a/src/auth/ha-auth-flow.ts b/src/auth/ha-auth-flow.ts
index d046ca6dae..ff0a31e0f9 100644
--- a/src/auth/ha-auth-flow.ts
+++ b/src/auth/ha-auth-flow.ts
@@ -7,16 +7,20 @@ import {
PropertyValues,
TemplateResult,
} from "lit";
-import "./ha-password-manager-polyfill";
import { property, state } from "lit/decorators";
+import "../components/ha-checkbox";
import "../components/ha-form/ha-form";
+import "../components/ha-formfield";
import "../components/ha-markdown";
+import "../components/ha-alert";
import { AuthProvider } from "../data/auth";
import {
DataEntryFlowStep,
DataEntryFlowStepForm,
} from "../data/data_entry_flow";
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
+import { computeInitialHaFormData } from "../components/ha-form/compute-initial-ha-form-data";
+import "./ha-password-manager-polyfill";
type State = "loading" | "error" | "step";
@@ -31,12 +35,44 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
@state() private _state: State = "loading";
- @state() private _stepData: any = {};
+ @state() private _stepData?: Record;
@state() private _step?: DataEntryFlowStep;
@state() private _errorMessage?: string;
+ @state() private _submitting = false;
+
+ @state() private _storeToken = false;
+
+ willUpdate(changedProps: PropertyValues) {
+ super.willUpdate(changedProps);
+
+ if (!changedProps.has("_step")) {
+ return;
+ }
+
+ if (!this._step) {
+ this._stepData = undefined;
+ return;
+ }
+
+ const oldStep = changedProps.get("_step") as HaAuthFlow["_step"];
+
+ if (
+ !oldStep ||
+ this._step.flow_id !== oldStep.flow_id ||
+ (this._step.type === "form" &&
+ oldStep.type === "form" &&
+ this._step.step_id !== oldStep.step_id)
+ ) {
+ this._stepData =
+ this._step.type === "form"
+ ? computeInitialHaFormData(this._step.data_schema)
+ : undefined;
+ }
+ }
+
protected render() {
return html`
@@ -76,6 +112,24 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
if (changedProps.has("authProvider")) {
this._providerChanged(this.authProvider);
}
+
+ if (!changedProps.has("_step") || this._step?.type !== "form") {
+ return;
+ }
+
+ // 100ms to give all the form elements time to initialize.
+ setTimeout(() => {
+ const form = this.renderRoot.querySelector("ha-form");
+ if (form) {
+ (form as any).focus();
+ }
+ }, 100);
+
+ setTimeout(() => {
+ this.renderRoot.querySelector(
+ "ha-password-manager-polyfill"
+ )!.boundingRect = this.getBoundingClientRect();
+ }, 500);
}
private _renderForm(): TemplateResult {
@@ -87,27 +141,33 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
return html`
${this._renderStep(this._step)}
- ${this._step.type === "form"
- ? this.localize("ui.panel.page-authorize.form.next")
- : this.localize(
- "ui.panel.page-authorize.form.start_over"
- )}
+ ${this._step.type === "form"
+ ? this.localize("ui.panel.page-authorize.form.next")
+ : this.localize("ui.panel.page-authorize.form.start_over")}
+
`;
case "error":
return html`
-
+
${this.localize(
"ui.panel.page-authorize.form.error",
"error",
this._errorMessage
)}
-
+
`;
case "loading":
- return html` ${this.localize("ui.panel.page-authorize.form.working")} `;
+ return html`
+
+ ${this.localize("ui.panel.page-authorize.form.working")}
+
+ `;
default:
return html``;
}
@@ -140,16 +200,34 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
.data=${this._stepData}
.schema=${step.data_schema}
.error=${step.errors}
+ .disabled=${this._submitting}
.computeLabel=${this._computeLabelCallback(step)}
.computeError=${this._computeErrorCallback(step)}
@value-changed=${this._stepDataChanged}
>
+ ${this.clientId === window.location.origin && step.step_id !== "mfa"
+ ? html`
+
+
+
+ `
+ : ""}
`;
default:
return html``;
}
}
+ private _storeTokenChanged(e: CustomEvent) {
+ this._storeToken = (e.currentTarget as HTMLInputElement).checked;
+ }
+
private async _providerChanged(newProvider?: AuthProvider) {
if (this._step && this._step.type === "form") {
fetch(`/auth/login_flow/${this._step.flow_id}`, {
@@ -189,7 +267,8 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
return;
}
- await this._updateStep(data);
+ this._step = data;
+ this._state = "step";
} else {
this._state = "error";
this._errorMessage = data.message;
@@ -216,43 +295,13 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
if (this.oauth2State) {
url += `&state=${encodeURIComponent(this.oauth2State)}`;
}
+ if (this._storeToken) {
+ url += `&storeToken=true`;
+ }
document.location.assign(url);
}
- private async _updateStep(step: DataEntryFlowStep) {
- let stepData: any = null;
- if (
- this._step &&
- (step.flow_id !== this._step.flow_id ||
- (step.type === "form" &&
- this._step.type === "form" &&
- step.step_id !== this._step.step_id))
- ) {
- stepData = {};
- }
- this._step = step;
- this._state = "step";
- if (stepData != null) {
- this._stepData = stepData;
- }
-
- await this.updateComplete;
- // 100ms to give all the form elements time to initialize.
- setTimeout(() => {
- const form = this.renderRoot.querySelector("ha-form");
- if (form) {
- (form as any).focus();
- }
- }, 100);
-
- setTimeout(() => {
- this.renderRoot.querySelector(
- "ha-password-manager-polyfill"
- )!.boundingRect = this.getBoundingClientRect();
- }, 500);
- }
-
private _stepDataChanged(ev: CustomEvent) {
this._stepData = ev.detail.value;
}
@@ -297,9 +346,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
this._providerChanged(this.authProvider);
return;
}
- this._state = "loading";
- // To avoid a jumping UI.
- this.style.setProperty("min-height", `${this.offsetHeight}px`);
+ this._submitting = true;
const postData = { ...this._stepData, client_id: this.clientId };
@@ -316,29 +363,28 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
this._redirect(newStep.result);
return;
}
- await this._updateStep(newStep);
+ this._step = newStep;
+ this._state = "step";
} catch (err: any) {
// eslint-disable-next-line no-console
console.error("Error submitting step", err);
this._state = "error";
this._errorMessage = this._unknownError();
} finally {
- this.style.setProperty("min-height", "");
+ this._submitting = false;
}
}
static get styles(): CSSResultGroup {
return css`
- :host {
- /* So we can set min-height to avoid jumping during loading */
- display: block;
- }
.action {
margin: 24px 0 8px;
text-align: center;
}
- .error {
- color: red;
+ /* Align with the rest of the form. */
+ .store-token {
+ margin-top: 10px;
+ margin-left: -16px;
}
`;
}
diff --git a/src/auth/ha-authorize.ts b/src/auth/ha-authorize.ts
index 9608ad0642..acd98952c9 100644
--- a/src/auth/ha-authorize.ts
+++ b/src/auth/ha-authorize.ts
@@ -174,6 +174,10 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
display: block;
margin-top: 48px;
}
+ ha-auth-flow {
+ display: block;
+ margin-top: 24px;
+ }
`;
}
}
diff --git a/src/auth/ha-password-manager-polyfill.ts b/src/auth/ha-password-manager-polyfill.ts
index c0c46c0ab0..a0f2488b78 100644
--- a/src/auth/ha-password-manager-polyfill.ts
+++ b/src/auth/ha-password-manager-polyfill.ts
@@ -2,8 +2,8 @@
import { html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
-import { HaFormSchema } from "../components/ha-form/ha-form";
-import { DataEntryFlowStep } from "../data/data_entry_flow";
+import type { HaFormSchema } from "../components/ha-form/types";
+import type { DataEntryFlowStep } from "../data/data_entry_flow";
declare global {
interface HTMLElementTagNameMap {
diff --git a/src/common/auth/token_storage.ts b/src/common/auth/token_storage.ts
index 6f8eea5d43..6b432a7029 100644
--- a/src/common/auth/token_storage.ts
+++ b/src/common/auth/token_storage.ts
@@ -30,6 +30,14 @@ export function askWrite() {
export function saveTokens(tokens: AuthData | null) {
tokenCache.tokens = tokens;
+
+ if (
+ !tokenCache.writeEnabled &&
+ new URLSearchParams(window.location.search).get("storeToken") === "true"
+ ) {
+ tokenCache.writeEnabled = true;
+ }
+
if (tokenCache.writeEnabled) {
try {
storage.hassTokens = JSON.stringify(tokens);
@@ -45,7 +53,6 @@ export function enableWrite() {
saveTokens(tokenCache.tokens);
}
}
-
export function loadTokens() {
if (tokenCache.tokens === undefined) {
try {
diff --git a/src/common/config/version.ts b/src/common/config/version.ts
index ca9b8ae7af..ec89a3407e 100644
--- a/src/common/config/version.ts
+++ b/src/common/config/version.ts
@@ -4,6 +4,10 @@ export const atLeastVersion = (
minor: number,
patch?: number
): boolean => {
+ if (__DEMO__) {
+ return true;
+ }
+
const [haMajor, haMinor, haPatch] = version.split(".", 3);
return (
diff --git a/src/common/const.ts b/src/common/const.ts
index dbc9ba4aac..6559ece953 100644
--- a/src/common/const.ts
+++ b/src/common/const.ts
@@ -1,88 +1,146 @@
/** Constants to be used in the frontend. */
+import {
+ mdiAccount,
+ mdiAirFilter,
+ mdiAlert,
+ mdiAngleAcute,
+ mdiAppleSafari,
+ mdiBell,
+ mdiBookmark,
+ mdiBrightness5,
+ mdiBullhorn,
+ mdiCalendar,
+ mdiCalendarClock,
+ mdiCash,
+ mdiClock,
+ mdiCloudUpload,
+ mdiCog,
+ mdiCommentAlert,
+ mdiCounter,
+ mdiCurrentAc,
+ mdiEye,
+ mdiFan,
+ mdiFlash,
+ mdiFlower,
+ mdiFormatListBulleted,
+ mdiFormTextbox,
+ mdiGasCylinder,
+ mdiGauge,
+ mdiGoogleAssistant,
+ mdiGoogleCirclesCommunities,
+ mdiHomeAssistant,
+ mdiHomeAutomation,
+ mdiImageFilterFrames,
+ mdiLightbulb,
+ mdiLightningBolt,
+ mdiMailbox,
+ mdiMapMarkerRadius,
+ mdiMolecule,
+ mdiMoleculeCo,
+ mdiMoleculeCo2,
+ mdiPalette,
+ mdiRayVertex,
+ mdiRemote,
+ mdiRobot,
+ mdiRobotVacuum,
+ mdiScriptText,
+ mdiSineWave,
+ mdiTextToSpeech,
+ mdiThermometer,
+ mdiThermostat,
+ mdiTimerOutline,
+ mdiToggleSwitchOutline,
+ mdiVideo,
+ mdiWaterPercent,
+ mdiWeatherCloudy,
+ mdiWhiteBalanceSunny,
+ mdiWifi,
+} from "@mdi/js";
+
// Constants should be alphabetically sorted by name.
// Arrays with values should be alphabetically sorted if order doesn't matter.
// Each constant should have a description what it is supposed to be used for.
/** Icon to use when no icon specified for domain. */
-export const DEFAULT_DOMAIN_ICON = "hass:bookmark";
+export const DEFAULT_DOMAIN_ICON = mdiBookmark;
/** Icons for each domain */
export const FIXED_DOMAIN_ICONS = {
- alert: "hass:alert",
- alexa: "hass:amazon-alexa",
- air_quality: "hass:air-filter",
- automation: "hass:robot",
- calendar: "hass:calendar",
- camera: "hass:video",
- climate: "hass:thermostat",
- configurator: "hass:cog",
- conversation: "hass:text-to-speech",
- counter: "hass:counter",
- device_tracker: "hass:account",
- fan: "hass:fan",
- google_assistant: "hass:google-assistant",
- group: "hass:google-circles-communities",
- homeassistant: "hass:home-assistant",
- homekit: "hass:home-automation",
- image_processing: "hass:image-filter-frames",
- input_boolean: "hass:toggle-switch-outline",
- input_datetime: "hass:calendar-clock",
- input_number: "hass:ray-vertex",
- input_select: "hass:format-list-bulleted",
- input_text: "hass:form-textbox",
- light: "hass:lightbulb",
- mailbox: "hass:mailbox",
- notify: "hass:comment-alert",
- number: "hass:ray-vertex",
- persistent_notification: "hass:bell",
- person: "hass:account",
- plant: "hass:flower",
- proximity: "hass:apple-safari",
- remote: "hass:remote",
- scene: "hass:palette",
- script: "hass:script-text",
- select: "hass:format-list-bulleted",
- sensor: "hass:eye",
- simple_alarm: "hass:bell",
- sun: "hass:white-balance-sunny",
- switch: "hass:flash",
- timer: "hass:timer-outline",
- updater: "hass:cloud-upload",
- vacuum: "hass:robot-vacuum",
- water_heater: "hass:thermometer",
- weather: "hass:weather-cloudy",
- zone: "hass:map-marker-radius",
+ alert: mdiAlert,
+ air_quality: mdiAirFilter,
+ automation: mdiRobot,
+ calendar: mdiCalendar,
+ camera: mdiVideo,
+ climate: mdiThermostat,
+ configurator: mdiCog,
+ conversation: mdiTextToSpeech,
+ counter: mdiCounter,
+ device_tracker: mdiAccount,
+ fan: mdiFan,
+ google_assistant: mdiGoogleAssistant,
+ group: mdiGoogleCirclesCommunities,
+ homeassistant: mdiHomeAssistant,
+ homekit: mdiHomeAutomation,
+ image_processing: mdiImageFilterFrames,
+ input_boolean: mdiToggleSwitchOutline,
+ input_datetime: mdiCalendarClock,
+ input_number: mdiRayVertex,
+ input_select: mdiFormatListBulleted,
+ input_text: mdiFormTextbox,
+ light: mdiLightbulb,
+ mailbox: mdiMailbox,
+ notify: mdiCommentAlert,
+ number: mdiRayVertex,
+ persistent_notification: mdiBell,
+ person: mdiAccount,
+ plant: mdiFlower,
+ proximity: mdiAppleSafari,
+ remote: mdiRemote,
+ scene: mdiPalette,
+ script: mdiScriptText,
+ select: mdiFormatListBulleted,
+ sensor: mdiEye,
+ siren: mdiBullhorn,
+ simple_alarm: mdiBell,
+ sun: mdiWhiteBalanceSunny,
+ switch: mdiFlash,
+ timer: mdiTimerOutline,
+ updater: mdiCloudUpload,
+ vacuum: mdiRobotVacuum,
+ water_heater: mdiThermometer,
+ weather: mdiWeatherCloudy,
+ zone: mdiMapMarkerRadius,
};
export const FIXED_DEVICE_CLASS_ICONS = {
- aqi: "hass:air-filter",
- battery: "hass:battery",
- carbon_dioxide: "mdi:molecule-co2",
- carbon_monoxide: "mdi:molecule-co",
- current: "hass:current-ac",
- date: "hass:calendar",
- energy: "hass:lightning-bolt",
- gas: "hass:gas-cylinder",
- humidity: "hass:water-percent",
- illuminance: "hass:brightness-5",
- monetary: "mdi:cash",
- nitrogen_dioxide: "mdi:molecule",
- nitrogen_monoxide: "mdi:molecule",
- nitrous_oxide: "mdi:molecule",
- ozone: "mdi:molecule",
- pm1: "mdi:molecule",
- pm10: "mdi:molecule",
- pm25: "mdi:molecule",
- power: "hass:flash",
- power_factor: "hass:angle-acute",
- pressure: "hass:gauge",
- signal_strength: "hass:wifi",
- sulphur_dioxide: "mdi:molecule",
- temperature: "hass:thermometer",
- timestamp: "hass:clock",
- volatile_organic_compounds: "mdi:molecule",
- voltage: "hass:sine-wave",
+ aqi: mdiAirFilter,
+ // battery: mdiBattery, => not included by design since `sensorIcon()` will dynamically determine the icon
+ carbon_dioxide: mdiMoleculeCo2,
+ carbon_monoxide: mdiMoleculeCo,
+ current: mdiCurrentAc,
+ date: mdiCalendar,
+ energy: mdiLightningBolt,
+ gas: mdiGasCylinder,
+ humidity: mdiWaterPercent,
+ illuminance: mdiBrightness5,
+ monetary: mdiCash,
+ nitrogen_dioxide: mdiMolecule,
+ nitrogen_monoxide: mdiMolecule,
+ nitrous_oxide: mdiMolecule,
+ ozone: mdiMolecule,
+ pm1: mdiMolecule,
+ pm10: mdiMolecule,
+ pm25: mdiMolecule,
+ power: mdiFlash,
+ power_factor: mdiAngleAcute,
+ pressure: mdiGauge,
+ signal_strength: mdiWifi,
+ sulphur_dioxide: mdiMolecule,
+ temperature: mdiThermometer,
+ timestamp: mdiClock,
+ volatile_organic_compounds: mdiMolecule,
+ voltage: mdiSineWave,
};
/** Domains that have a state card. */
diff --git a/src/common/dom/apply_themes_on_element.ts b/src/common/dom/apply_themes_on_element.ts
index 721863e7f3..fca6e2262a 100644
--- a/src/common/dom/apply_themes_on_element.ts
+++ b/src/common/dom/apply_themes_on_element.ts
@@ -36,55 +36,62 @@ export const applyThemesOnElement = (
let cacheKey = selectedTheme;
let themeRules: Partial = {};
- if (themeSettings) {
- if (themeSettings.dark) {
- cacheKey = `${cacheKey}__dark`;
- themeRules = { ...darkStyles };
+ // If there is no explicitly desired dark mode provided, we automatically
+ // use the active one from hass.themes.
+ if (!themeSettings || themeSettings?.dark === undefined) {
+ themeSettings = {
+ ...themeSettings,
+ dark: themes.darkMode,
+ };
+ }
+
+ if (themeSettings.dark) {
+ cacheKey = `${cacheKey}__dark`;
+ themeRules = { ...darkStyles };
+ }
+
+ if (selectedTheme === "default") {
+ // Determine the primary and accent colors from the current settings.
+ // Fallbacks are implicitly the HA default blue and orange or the
+ // derived "darkStyles" values, depending on the light vs dark mode.
+ const primaryColor = themeSettings.primaryColor;
+ const accentColor = themeSettings.accentColor;
+
+ if (themeSettings.dark && primaryColor) {
+ themeRules["app-header-background-color"] = hexBlend(
+ primaryColor,
+ "#121212",
+ 8
+ );
}
- if (selectedTheme === "default") {
- // Determine the primary and accent colors from the current settings.
- // Fallbacks are implicitly the HA default blue and orange or the
- // derived "darkStyles" values, depending on the light vs dark mode.
- const primaryColor = themeSettings.primaryColor;
- const accentColor = themeSettings.accentColor;
+ if (primaryColor) {
+ cacheKey = `${cacheKey}__primary_${primaryColor}`;
+ const rgbPrimaryColor = hex2rgb(primaryColor);
+ const labPrimaryColor = rgb2lab(rgbPrimaryColor);
+ themeRules["primary-color"] = primaryColor;
+ const rgbLightPrimaryColor = lab2rgb(labBrighten(labPrimaryColor));
+ themeRules["light-primary-color"] = rgb2hex(rgbLightPrimaryColor);
+ themeRules["dark-primary-color"] = lab2hex(labDarken(labPrimaryColor));
+ themeRules["text-primary-color"] =
+ rgbContrast(rgbPrimaryColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
+ themeRules["text-light-primary-color"] =
+ rgbContrast(rgbLightPrimaryColor, [33, 33, 33]) < 6
+ ? "#fff"
+ : "#212121";
+ themeRules["state-icon-color"] = themeRules["dark-primary-color"];
+ }
+ if (accentColor) {
+ cacheKey = `${cacheKey}__accent_${accentColor}`;
+ themeRules["accent-color"] = accentColor;
+ const rgbAccentColor = hex2rgb(accentColor);
+ themeRules["text-accent-color"] =
+ rgbContrast(rgbAccentColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
+ }
- if (themeSettings.dark && primaryColor) {
- themeRules["app-header-background-color"] = hexBlend(
- primaryColor,
- "#121212",
- 8
- );
- }
-
- if (primaryColor) {
- cacheKey = `${cacheKey}__primary_${primaryColor}`;
- const rgbPrimaryColor = hex2rgb(primaryColor);
- const labPrimaryColor = rgb2lab(rgbPrimaryColor);
- themeRules["primary-color"] = primaryColor;
- const rgbLightPrimaryColor = lab2rgb(labBrighten(labPrimaryColor));
- themeRules["light-primary-color"] = rgb2hex(rgbLightPrimaryColor);
- themeRules["dark-primary-color"] = lab2hex(labDarken(labPrimaryColor));
- themeRules["text-primary-color"] =
- rgbContrast(rgbPrimaryColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
- themeRules["text-light-primary-color"] =
- rgbContrast(rgbLightPrimaryColor, [33, 33, 33]) < 6
- ? "#fff"
- : "#212121";
- themeRules["state-icon-color"] = themeRules["dark-primary-color"];
- }
- if (accentColor) {
- cacheKey = `${cacheKey}__accent_${accentColor}`;
- themeRules["accent-color"] = accentColor;
- const rgbAccentColor = hex2rgb(accentColor);
- themeRules["text-accent-color"] =
- rgbContrast(rgbAccentColor, [33, 33, 33]) < 6 ? "#fff" : "#212121";
- }
-
- // Nothing was changed
- if (element._themes?.cacheKey === cacheKey) {
- return;
- }
+ // Nothing was changed
+ if (element._themes?.cacheKey === cacheKey) {
+ return;
}
}
diff --git a/src/common/entity/alarm_panel_icon.ts b/src/common/entity/alarm_panel_icon.ts
index aa2590ed67..cdbb736b3a 100644
--- a/src/common/entity/alarm_panel_icon.ts
+++ b/src/common/entity/alarm_panel_icon.ts
@@ -1,24 +1,36 @@
/** Return an icon representing a alarm panel state. */
+import {
+ mdiShieldLock,
+ mdiShieldAirplane,
+ mdiShieldHome,
+ mdiShieldMoon,
+ mdiSecurity,
+ mdiShieldOutline,
+ mdiBellRing,
+ mdiShieldOff,
+ mdiShield,
+} from "@mdi/js";
+
export const alarmPanelIcon = (state?: string) => {
switch (state) {
case "armed_away":
- return "hass:shield-lock";
+ return mdiShieldLock;
case "armed_vacation":
- return "hass:shield-airplane";
+ return mdiShieldAirplane;
case "armed_home":
- return "hass:shield-home";
+ return mdiShieldHome;
case "armed_night":
- return "hass:shield-moon";
+ return mdiShieldMoon;
case "armed_custom_bypass":
- return "hass:security";
+ return mdiSecurity;
case "pending":
- return "hass:shield-outline";
+ return mdiShieldOutline;
case "triggered":
- return "hass:bell-ring";
+ return mdiBellRing;
case "disarmed":
- return "hass:shield-off";
+ return mdiShieldOff;
default:
- return "hass:shield";
+ return mdiShield;
}
};
diff --git a/src/common/entity/battery_icon.ts b/src/common/entity/battery_icon.ts
index f4b1784232..d73cdbe489 100644
--- a/src/common/entity/battery_icon.ts
+++ b/src/common/entity/battery_icon.ts
@@ -1,35 +1,92 @@
/** Return an icon representing a battery state. */
+import {
+ mdiBattery,
+ mdiBattery10,
+ mdiBattery20,
+ mdiBattery30,
+ mdiBattery40,
+ mdiBattery50,
+ mdiBattery60,
+ mdiBattery70,
+ mdiBattery80,
+ mdiBattery90,
+ mdiBatteryAlert,
+ mdiBatteryAlertVariantOutline,
+ mdiBatteryCharging,
+ mdiBatteryCharging10,
+ mdiBatteryCharging20,
+ mdiBatteryCharging30,
+ mdiBatteryCharging40,
+ mdiBatteryCharging50,
+ mdiBatteryCharging60,
+ mdiBatteryCharging70,
+ mdiBatteryCharging80,
+ mdiBatteryCharging90,
+ mdiBatteryChargingOutline,
+ mdiBatteryUnknown,
+} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
-export const batteryIcon = (
+const BATTERY_ICONS = {
+ 10: mdiBattery10,
+ 20: mdiBattery20,
+ 30: mdiBattery30,
+ 40: mdiBattery40,
+ 50: mdiBattery50,
+ 60: mdiBattery60,
+ 70: mdiBattery70,
+ 80: mdiBattery80,
+ 90: mdiBattery90,
+ 100: mdiBattery,
+};
+const BATTERY_CHARGING_ICONS = {
+ 10: mdiBatteryCharging10,
+ 20: mdiBatteryCharging20,
+ 30: mdiBatteryCharging30,
+ 40: mdiBatteryCharging40,
+ 50: mdiBatteryCharging50,
+ 60: mdiBatteryCharging60,
+ 70: mdiBatteryCharging70,
+ 80: mdiBatteryCharging80,
+ 90: mdiBatteryCharging90,
+ 100: mdiBatteryCharging,
+};
+
+export const batteryStateIcon = (
batteryState: HassEntity,
batteryChargingState?: HassEntity
) => {
- const battery = Number(batteryState.state);
- const battery_charging =
+ const battery = batteryState.state;
+ const batteryCharging =
batteryChargingState && batteryChargingState.state === "on";
- let icon = "hass:battery";
- if (isNaN(battery)) {
- if (batteryState.state === "off") {
- icon += "-full";
- } else if (batteryState.state === "on") {
- icon += "-alert";
- } else {
- icon += "-unknown";
- }
- return icon;
- }
-
- const batteryRound = Math.round(battery / 10) * 10;
- if (battery_charging && battery > 10) {
- icon += `-charging-${batteryRound}`;
- } else if (battery_charging) {
- icon += "-outline";
- } else if (battery <= 5) {
- icon += "-alert";
- } else if (battery > 5 && battery < 95) {
- icon += `-${batteryRound}`;
- }
- return icon;
+ return batteryIcon(battery, batteryCharging);
+};
+
+export const batteryIcon = (
+ batteryState: number | string,
+ batteryCharging?: boolean
+) => {
+ const batteryValue = Number(batteryState);
+ if (isNaN(batteryValue)) {
+ if (batteryState === "off") {
+ return mdiBattery;
+ }
+ if (batteryState === "on") {
+ return mdiBatteryAlert;
+ }
+ return mdiBatteryUnknown;
+ }
+
+ const batteryRound = Math.round(batteryValue / 10) * 10;
+ if (batteryCharging && batteryValue >= 10) {
+ return BATTERY_CHARGING_ICONS[batteryRound];
+ }
+ if (batteryCharging) {
+ return mdiBatteryChargingOutline;
+ }
+ if (batteryValue <= 5) {
+ return mdiBatteryAlertVariantOutline;
+ }
+ return BATTERY_ICONS[batteryRound];
};
diff --git a/src/common/entity/binary_sensor_icon.ts b/src/common/entity/binary_sensor_icon.ts
index da18e0d01f..0f1dfaeaf2 100644
--- a/src/common/entity/binary_sensor_icon.ts
+++ b/src/common/entity/binary_sensor_icon.ts
@@ -1,3 +1,46 @@
+import {
+ mdiAlertCircle,
+ mdiBattery,
+ mdiBatteryCharging,
+ mdiBatteryOutline,
+ mdiBrightness5,
+ mdiBrightness7,
+ mdiCheckboxMarkedCircle,
+ mdiCheckCircle,
+ mdiCropPortrait,
+ mdiDoorClosed,
+ mdiDoorOpen,
+ mdiFire,
+ mdiGarage,
+ mdiGarageOpen,
+ mdiHome,
+ mdiHomeOutline,
+ mdiLock,
+ mdiLockOpen,
+ mdiMusicNote,
+ mdiMusicNoteOff,
+ mdiPackage,
+ mdiPackageUp,
+ mdiPlay,
+ mdiPowerPlug,
+ mdiPowerPlugOff,
+ mdiRadioboxBlank,
+ mdiRun,
+ mdiServerNetwork,
+ mdiServerNetworkOff,
+ mdiSmoke,
+ mdiSnowflake,
+ mdiSquare,
+ mdiSquareOutline,
+ mdiStop,
+ mdiThermometer,
+ mdiVibrate,
+ mdiWalk,
+ mdiWater,
+ mdiWaterOff,
+ mdiWindowClosed,
+ mdiWindowOpen,
+} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
/** Return an icon representing a binary sensor state. */
@@ -6,52 +49,55 @@ export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => {
const is_off = state === "off";
switch (stateObj?.attributes.device_class) {
case "battery":
- return is_off ? "hass:battery" : "hass:battery-outline";
+ return is_off ? mdiBattery : mdiBatteryOutline;
case "battery_charging":
- return is_off ? "hass:battery" : "hass:battery-charging";
+ return is_off ? mdiBattery : mdiBatteryCharging;
case "cold":
- return is_off ? "hass:thermometer" : "hass:snowflake";
+ return is_off ? mdiThermometer : mdiSnowflake;
case "connectivity":
- return is_off ? "hass:server-network-off" : "hass:server-network";
+ return is_off ? mdiServerNetworkOff : mdiServerNetwork;
case "door":
- return is_off ? "hass:door-closed" : "hass:door-open";
+ return is_off ? mdiDoorClosed : mdiDoorOpen;
case "garage_door":
- return is_off ? "hass:garage" : "hass:garage-open";
+ return is_off ? mdiGarage : mdiGarageOpen;
case "power":
- return is_off ? "hass:power-plug-off" : "hass:power-plug";
+ return is_off ? mdiPowerPlugOff : mdiPowerPlug;
case "gas":
case "problem":
case "safety":
- return is_off ? "hass:check-circle" : "hass:alert-circle";
+ case "tamper":
+ return is_off ? mdiCheckCircle : mdiAlertCircle;
case "smoke":
- return is_off ? "hass:check-circle" : "hass:smoke";
+ return is_off ? mdiCheckCircle : mdiSmoke;
case "heat":
- return is_off ? "hass:thermometer" : "hass:fire";
+ return is_off ? mdiThermometer : mdiFire;
case "light":
- return is_off ? "hass:brightness-5" : "hass:brightness-7";
+ return is_off ? mdiBrightness5 : mdiBrightness7;
case "lock":
- return is_off ? "hass:lock" : "hass:lock-open";
+ return is_off ? mdiLock : mdiLockOpen;
case "moisture":
- return is_off ? "hass:water-off" : "hass:water";
+ return is_off ? mdiWaterOff : mdiWater;
case "motion":
- return is_off ? "hass:walk" : "hass:run";
+ return is_off ? mdiWalk : mdiRun;
case "occupancy":
- return is_off ? "hass:home-outline" : "hass:home";
+ return is_off ? mdiHomeOutline : mdiHome;
case "opening":
- return is_off ? "hass:square" : "hass:square-outline";
+ return is_off ? mdiSquare : mdiSquareOutline;
case "plug":
- return is_off ? "hass:power-plug-off" : "hass:power-plug";
+ return is_off ? mdiPowerPlugOff : mdiPowerPlug;
case "presence":
- return is_off ? "hass:home-outline" : "hass:home";
+ return is_off ? mdiHomeOutline : mdiHome;
+ case "running":
+ return is_off ? mdiStop : mdiPlay;
case "sound":
- return is_off ? "hass:music-note-off" : "hass:music-note";
+ return is_off ? mdiMusicNoteOff : mdiMusicNote;
case "update":
- return is_off ? "mdi:package" : "mdi:package-up";
+ return is_off ? mdiPackage : mdiPackageUp;
case "vibration":
- return is_off ? "hass:crop-portrait" : "hass:vibrate";
+ return is_off ? mdiCropPortrait : mdiVibrate;
case "window":
- return is_off ? "hass:window-closed" : "hass:window-open";
+ return is_off ? mdiWindowClosed : mdiWindowOpen;
default:
- return is_off ? "hass:radiobox-blank" : "hass:checkbox-marked-circle";
+ return is_off ? mdiRadioboxBlank : mdiCheckboxMarkedCircle;
}
};
diff --git a/src/common/entity/compute_state_display.ts b/src/common/entity/compute_state_display.ts
index fddce09d82..fd99a022da 100644
--- a/src/common/entity/compute_state_display.ts
+++ b/src/common/entity/compute_state_display.ts
@@ -39,7 +39,7 @@ export const computeStateDisplay = (
const domain = computeStateDomain(stateObj);
if (domain === "input_datetime") {
- if (state) {
+ if (state !== undefined) {
// If trying to display an explicit state, need to parse the explict state to `Date` then format.
// Attributes aren't available, we have to use `state`.
try {
@@ -63,7 +63,7 @@ export const computeStateDisplay = (
}
}
return state;
- } catch {
+ } catch (_e) {
// Formatting methods may throw error if date parsing doesn't go well,
// just return the state string in that case.
return state;
@@ -71,7 +71,17 @@ export const computeStateDisplay = (
} else {
// If not trying to display an explicit state, create `Date` object from `stateObj`'s attributes then format.
let date: Date;
- if (!stateObj.attributes.has_time) {
+ if (stateObj.attributes.has_date && stateObj.attributes.has_time) {
+ date = new Date(
+ stateObj.attributes.year,
+ stateObj.attributes.month - 1,
+ stateObj.attributes.day,
+ stateObj.attributes.hour,
+ stateObj.attributes.minute
+ );
+ return formatDateTime(date, locale);
+ }
+ if (stateObj.attributes.has_date) {
date = new Date(
stateObj.attributes.year,
stateObj.attributes.month - 1,
@@ -79,20 +89,12 @@ export const computeStateDisplay = (
);
return formatDate(date, locale);
}
- if (!stateObj.attributes.has_date) {
+ if (stateObj.attributes.has_time) {
date = new Date();
date.setHours(stateObj.attributes.hour, stateObj.attributes.minute);
return formatTime(date, locale);
}
-
- date = new Date(
- stateObj.attributes.year,
- stateObj.attributes.month - 1,
- stateObj.attributes.day,
- stateObj.attributes.hour,
- stateObj.attributes.minute
- );
- return formatDateTime(date, locale);
+ return stateObj.state;
}
}
diff --git a/src/common/entity/cover_icon.ts b/src/common/entity/cover_icon.ts
index 011f8e7294..4308306890 100644
--- a/src/common/entity/cover_icon.ts
+++ b/src/common/entity/cover_icon.ts
@@ -1,4 +1,30 @@
/** Return an icon representing a cover state. */
+import {
+ mdiArrowUpBox,
+ mdiArrowDownBox,
+ mdiGarage,
+ mdiGarageOpen,
+ mdiGateArrowRight,
+ mdiGate,
+ mdiGateOpen,
+ mdiDoorOpen,
+ mdiDoorClosed,
+ mdiCircle,
+ mdiWindowShutter,
+ mdiWindowShutterOpen,
+ mdiBlinds,
+ mdiBlindsOpen,
+ mdiWindowClosed,
+ mdiWindowOpen,
+ mdiArrowExpandHorizontal,
+ mdiArrowUp,
+ mdiArrowCollapseHorizontal,
+ mdiArrowDown,
+ mdiCircleSlice8,
+ mdiArrowSplitVertical,
+ mdiCurtains,
+ mdiCurtainsClosed,
+} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
export const coverIcon = (state?: string, stateObj?: HassEntity): string => {
@@ -8,74 +34,84 @@ export const coverIcon = (state?: string, stateObj?: HassEntity): string => {
case "garage":
switch (state) {
case "opening":
- return "hass:arrow-up-box";
+ return mdiArrowUpBox;
case "closing":
- return "hass:arrow-down-box";
+ return mdiArrowDownBox;
case "closed":
- return "hass:garage";
+ return mdiGarage;
default:
- return "hass:garage-open";
+ return mdiGarageOpen;
}
case "gate":
switch (state) {
case "opening":
case "closing":
- return "hass:gate-arrow-right";
+ return mdiGateArrowRight;
case "closed":
- return "hass:gate";
+ return mdiGate;
default:
- return "hass:gate-open";
+ return mdiGateOpen;
}
case "door":
- return open ? "hass:door-open" : "hass:door-closed";
+ return open ? mdiDoorOpen : mdiDoorClosed;
case "damper":
- return open ? "hass:circle" : "hass:circle-slice-8";
+ return open ? mdiCircle : mdiCircleSlice8;
case "shutter":
switch (state) {
case "opening":
- return "hass:arrow-up-box";
+ return mdiArrowUpBox;
case "closing":
- return "hass:arrow-down-box";
+ return mdiArrowDownBox;
case "closed":
- return "hass:window-shutter";
+ return mdiWindowShutter;
default:
- return "hass:window-shutter-open";
+ return mdiWindowShutterOpen;
+ }
+ case "curtain":
+ switch (state) {
+ case "opening":
+ return mdiArrowSplitVertical;
+ case "closing":
+ return mdiArrowCollapseHorizontal;
+ case "closed":
+ return mdiCurtainsClosed;
+ default:
+ return mdiCurtains;
}
case "blind":
- case "curtain":
case "shade":
switch (state) {
case "opening":
- return "hass:arrow-up-box";
+ return mdiArrowUpBox;
case "closing":
- return "hass:arrow-down-box";
+ return mdiArrowDownBox;
case "closed":
- return "hass:blinds";
+ return mdiBlinds;
default:
- return "hass:blinds-open";
+ return mdiBlindsOpen;
}
case "window":
switch (state) {
case "opening":
- return "hass:arrow-up-box";
+ return mdiArrowUpBox;
case "closing":
- return "hass:arrow-down-box";
+ return mdiArrowDownBox;
case "closed":
- return "hass:window-closed";
+ return mdiWindowClosed;
default:
- return "hass:window-open";
+ return mdiWindowOpen;
}
}
switch (state) {
case "opening":
- return "hass:arrow-up-box";
+ return mdiArrowUpBox;
case "closing":
- return "hass:arrow-down-box";
+ return mdiArrowDownBox;
case "closed":
- return "hass:window-closed";
+ return mdiWindowClosed;
default:
- return "hass:window-open";
+ return mdiWindowOpen;
}
};
@@ -84,9 +120,9 @@ export const computeOpenIcon = (stateObj: HassEntity): string => {
case "awning":
case "door":
case "gate":
- return "hass:arrow-expand-horizontal";
+ return mdiArrowExpandHorizontal;
default:
- return "hass:arrow-up";
+ return mdiArrowUp;
}
};
@@ -95,8 +131,8 @@ export const computeCloseIcon = (stateObj: HassEntity): string => {
case "awning":
case "door":
case "gate":
- return "hass:arrow-collapse-horizontal";
+ return mdiArrowCollapseHorizontal;
default:
- return "hass:arrow-down";
+ return mdiArrowDown;
}
};
diff --git a/src/common/entity/domain_icon.ts b/src/common/entity/domain_icon.ts
index 60f27c93aa..3816c925de 100644
--- a/src/common/entity/domain_icon.ts
+++ b/src/common/entity/domain_icon.ts
@@ -1,3 +1,20 @@
+import {
+ mdiAirHumidifierOff,
+ mdiAirHumidifier,
+ mdiLockOpen,
+ mdiLockAlert,
+ mdiLockClock,
+ mdiLock,
+ mdiCastConnected,
+ mdiCast,
+ mdiEmoticonDead,
+ mdiSleep,
+ mdiTimerSand,
+ mdiZWave,
+ mdiClock,
+ mdiCalendar,
+ mdiWeatherNight,
+} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
/**
* Return the icon to be used for a domain.
@@ -28,36 +45,34 @@ export const domainIcon = (
return coverIcon(compareState, stateObj);
case "humidifier":
- return state && state === "off"
- ? "hass:air-humidifier-off"
- : "hass:air-humidifier";
+ return state && state === "off" ? mdiAirHumidifierOff : mdiAirHumidifier;
case "lock":
switch (compareState) {
case "unlocked":
- return "hass:lock-open";
+ return mdiLockOpen;
case "jammed":
- return "hass:lock-alert";
+ return mdiLockAlert;
case "locking":
case "unlocking":
- return "hass:lock-clock";
+ return mdiLockClock;
default:
- return "hass:lock";
+ return mdiLock;
}
case "media_player":
- return compareState === "playing" ? "hass:cast-connected" : "hass:cast";
+ return compareState === "playing" ? mdiCastConnected : mdiCast;
case "zwave":
switch (compareState) {
case "dead":
- return "hass:emoticon-dead";
+ return mdiEmoticonDead;
case "sleeping":
- return "hass:sleep";
+ return mdiSleep;
case "initializing":
- return "hass:timer-sand";
+ return mdiTimerSand;
default:
- return "hass:z-wave";
+ return mdiZWave;
}
case "sensor": {
@@ -71,17 +86,17 @@ export const domainIcon = (
case "input_datetime":
if (!stateObj?.attributes.has_date) {
- return "hass:clock";
+ return mdiClock;
}
if (!stateObj.attributes.has_time) {
- return "hass:calendar";
+ return mdiCalendar;
}
break;
case "sun":
return stateObj?.state === "above_horizon"
? FIXED_DOMAIN_ICONS[domain]
- : "hass:weather-night";
+ : mdiWeatherNight;
}
if (domain in FIXED_DOMAIN_ICONS) {
diff --git a/src/common/entity/sensor_icon.ts b/src/common/entity/sensor_icon.ts
index fa9bed83ce..9af29ceb3e 100644
--- a/src/common/entity/sensor_icon.ts
+++ b/src/common/entity/sensor_icon.ts
@@ -1,8 +1,9 @@
/** Return an icon representing a sensor state. */
+import { mdiBattery, mdiThermometer } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
-import { FIXED_DEVICE_CLASS_ICONS, UNIT_C, UNIT_F } from "../const";
-import { batteryIcon } from "./battery_icon";
import { SENSOR_DEVICE_CLASS_BATTERY } from "../../data/sensor";
+import { FIXED_DEVICE_CLASS_ICONS, UNIT_C, UNIT_F } from "../const";
+import { batteryStateIcon } from "./battery_icon";
export const sensorIcon = (stateObj?: HassEntity): string | undefined => {
const dclass = stateObj?.attributes.device_class;
@@ -12,12 +13,12 @@ export const sensorIcon = (stateObj?: HassEntity): string | undefined => {
}
if (dclass === SENSOR_DEVICE_CLASS_BATTERY) {
- return stateObj ? batteryIcon(stateObj) : "hass:battery";
+ return stateObj ? batteryStateIcon(stateObj) : mdiBattery;
}
const unit = stateObj?.attributes.unit_of_measurement;
if (unit === UNIT_C || unit === UNIT_F) {
- return "hass:thermometer";
+ return mdiThermometer;
}
return undefined;
diff --git a/src/common/entity/state_icon.ts b/src/common/entity/state_icon_path.ts
similarity index 74%
rename from src/common/entity/state_icon.ts
rename to src/common/entity/state_icon_path.ts
index 86bfe50812..24d842caf6 100644
--- a/src/common/entity/state_icon.ts
+++ b/src/common/entity/state_icon_path.ts
@@ -4,13 +4,9 @@ import { DEFAULT_DOMAIN_ICON } from "../const";
import { computeDomain } from "./compute_domain";
import { domainIcon } from "./domain_icon";
-export const stateIcon = (state?: HassEntity) => {
+export const stateIconPath = (state?: HassEntity) => {
if (!state) {
return DEFAULT_DOMAIN_ICON;
}
- if (state.attributes.icon) {
- return state.attributes.icon;
- }
-
return domainIcon(computeDomain(state.entity_id), state);
};
diff --git a/src/common/entity/strip_prefix_from_entity_name.ts b/src/common/entity/strip_prefix_from_entity_name.ts
new file mode 100644
index 0000000000..1efa3704d5
--- /dev/null
+++ b/src/common/entity/strip_prefix_from_entity_name.ts
@@ -0,0 +1,24 @@
+/**
+ * Strips a device name from an entity name.
+ * @param entityName the entity name
+ * @param lowerCasedPrefixWithSpaceSuffix the prefix to strip, lower cased with a space suffix
+ * @returns
+ */
+export const stripPrefixFromEntityName = (
+ entityName: string,
+ lowerCasedPrefixWithSpaceSuffix: string
+) => {
+ if (!entityName.toLowerCase().startsWith(lowerCasedPrefixWithSpaceSuffix)) {
+ return undefined;
+ }
+
+ const newName = entityName.substring(lowerCasedPrefixWithSpaceSuffix.length);
+
+ // If first word already has an upper case letter (e.g. from brand name)
+ // leave as-is, otherwise capitalize the first word.
+ return hasUpperCase(newName.substr(0, newName.indexOf(" ")))
+ ? newName
+ : newName[0].toUpperCase() + newName.slice(1);
+};
+
+const hasUpperCase = (str: string): boolean => str.toLowerCase() !== str;
diff --git a/src/common/search/search-input.ts b/src/common/search/search-input.ts
index b7405a41cf..08084af98a 100644
--- a/src/common/search/search-input.ts
+++ b/src/common/search/search-input.ts
@@ -1,4 +1,3 @@
-import "@material/mwc-icon-button/mwc-icon-button";
import { mdiClose, mdiMagnify } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
@@ -11,11 +10,15 @@ import {
TemplateResult,
} from "lit";
import { customElement, property, query } from "lit/decorators";
+import "../../components/ha-icon-button";
import "../../components/ha-svg-icon";
+import { HomeAssistant } from "../../types";
import { fireEvent } from "../dom/fire_event";
@customElement("search-input")
class SearchInput extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
@property() public filter?: string;
@property({ type: Boolean, attribute: "no-label-float" })
@@ -50,13 +53,12 @@ class SearchInput extends LitElement {
${this.filter &&
html`
-
-
-
+ .label=${this.hass.localize("ui.common.clear")}
+ .path=${mdiClose}
+ >
`}
`;
@@ -90,10 +92,10 @@ class SearchInput extends LitElement {
static get styles(): CSSResultGroup {
return css`
ha-svg-icon,
- mwc-icon-button {
+ ha-icon-button {
color: var(--primary-text-color);
}
- mwc-icon-button {
+ ha-icon-button {
--mdc-icon-button-size: 24px;
}
ha-svg-icon.prefix {
diff --git a/src/common/string/slugify.ts b/src/common/string/slugify.ts
index dc4015ff68..e9901faa76 100644
--- a/src/common/string/slugify.ts
+++ b/src/common/string/slugify.ts
@@ -12,8 +12,8 @@ export const slugify = (value: string, delimiter = "_") => {
.replace(p, (c) => b.charAt(a.indexOf(c))) // Replace special characters
.replace(/&/g, `${delimiter}and${delimiter}`) // Replace & with 'and'
.replace(/[^\w-]+/g, "") // Remove all non-word characters
- .replace(/-/, delimiter) // Replace - with delimiter
- .replace(new RegExp(`/${delimiter}${delimiter}+/`, "g"), delimiter) // Replace multiple delimiters with single delimiter
- .replace(new RegExp(`/^${delimiter}+/`), "") // Trim delimiter from start of text
- .replace(new RegExp(`/-+$/`), ""); // Trim delimiter from end of text
+ .replace(/-/g, delimiter) // Replace - with delimiter
+ .replace(new RegExp(`(${delimiter})\\1+`, "g"), "$1") // Replace multiple delimiters with single delimiter
+ .replace(new RegExp(`^${delimiter}+`), "") // Trim delimiter from start of text
+ .replace(new RegExp(`${delimiter}+$`), ""); // Trim delimiter from end of text
};
diff --git a/src/common/style/icon_color_css.ts b/src/common/style/icon_color_css.ts
index b636ffa89c..a0905c0ea9 100644
--- a/src/common/style/icon_color_css.ts
+++ b/src/common/style/icon_color_css.ts
@@ -1,57 +1,57 @@
import { css } from "lit";
export const iconColorCSS = css`
- ha-icon[data-domain="alert"][data-state="on"],
- ha-icon[data-domain="automation"][data-state="on"],
- ha-icon[data-domain="binary_sensor"][data-state="on"],
- ha-icon[data-domain="calendar"][data-state="on"],
- ha-icon[data-domain="camera"][data-state="streaming"],
- ha-icon[data-domain="cover"][data-state="open"],
- ha-icon[data-domain="fan"][data-state="on"],
- ha-icon[data-domain="humidifier"][data-state="on"],
- ha-icon[data-domain="light"][data-state="on"],
- ha-icon[data-domain="input_boolean"][data-state="on"],
- ha-icon[data-domain="lock"][data-state="unlocked"],
- ha-icon[data-domain="media_player"][data-state="on"],
- ha-icon[data-domain="media_player"][data-state="paused"],
- ha-icon[data-domain="media_player"][data-state="playing"],
- ha-icon[data-domain="script"][data-state="on"],
- ha-icon[data-domain="sun"][data-state="above_horizon"],
- ha-icon[data-domain="switch"][data-state="on"],
- ha-icon[data-domain="timer"][data-state="active"],
- ha-icon[data-domain="vacuum"][data-state="cleaning"],
- ha-icon[data-domain="group"][data-state="on"],
- ha-icon[data-domain="group"][data-state="home"],
- ha-icon[data-domain="group"][data-state="open"],
- ha-icon[data-domain="group"][data-state="locked"],
- ha-icon[data-domain="group"][data-state="problem"] {
+ ha-state-icon[data-domain="alert"][data-state="on"],
+ ha-state-icon[data-domain="automation"][data-state="on"],
+ ha-state-icon[data-domain="binary_sensor"][data-state="on"],
+ ha-state-icon[data-domain="calendar"][data-state="on"],
+ ha-state-icon[data-domain="camera"][data-state="streaming"],
+ ha-state-icon[data-domain="cover"][data-state="open"],
+ ha-state-icon[data-domain="fan"][data-state="on"],
+ ha-state-icon[data-domain="humidifier"][data-state="on"],
+ ha-state-icon[data-domain="light"][data-state="on"],
+ ha-state-icon[data-domain="input_boolean"][data-state="on"],
+ ha-state-icon[data-domain="lock"][data-state="unlocked"],
+ ha-state-icon[data-domain="media_player"][data-state="on"],
+ ha-state-icon[data-domain="media_player"][data-state="paused"],
+ ha-state-icon[data-domain="media_player"][data-state="playing"],
+ ha-state-icon[data-domain="script"][data-state="on"],
+ ha-state-icon[data-domain="sun"][data-state="above_horizon"],
+ ha-state-icon[data-domain="switch"][data-state="on"],
+ ha-state-icon[data-domain="timer"][data-state="active"],
+ ha-state-icon[data-domain="vacuum"][data-state="cleaning"],
+ ha-state-icon[data-domain="group"][data-state="on"],
+ ha-state-icon[data-domain="group"][data-state="home"],
+ ha-state-icon[data-domain="group"][data-state="open"],
+ ha-state-icon[data-domain="group"][data-state="locked"],
+ ha-state-icon[data-domain="group"][data-state="problem"] {
color: var(--paper-item-icon-active-color, #fdd835);
}
- ha-icon[data-domain="climate"][data-state="cooling"] {
+ ha-state-icon[data-domain="climate"][data-state="cooling"] {
color: var(--cool-color, var(--state-climate-cool-color));
}
- ha-icon[data-domain="climate"][data-state="heating"] {
+ ha-state-icon[data-domain="climate"][data-state="heating"] {
color: var(--heat-color, var(--state-climate-heat-color));
}
- ha-icon[data-domain="climate"][data-state="drying"] {
+ ha-state-icon[data-domain="climate"][data-state="drying"] {
color: var(--dry-color, var(--state-climate-dry-color));
}
- ha-icon[data-domain="alarm_control_panel"] {
+ ha-state-icon[data-domain="alarm_control_panel"] {
color: var(--alarm-color-armed, var(--label-badge-red));
}
- ha-icon[data-domain="alarm_control_panel"][data-state="disarmed"] {
+ ha-state-icon[data-domain="alarm_control_panel"][data-state="disarmed"] {
color: var(--alarm-color-disarmed, var(--label-badge-green));
}
- ha-icon[data-domain="alarm_control_panel"][data-state="pending"],
- ha-icon[data-domain="alarm_control_panel"][data-state="arming"] {
+ ha-state-icon[data-domain="alarm_control_panel"][data-state="pending"],
+ ha-state-icon[data-domain="alarm_control_panel"][data-state="arming"] {
color: var(--alarm-color-pending, var(--label-badge-yellow));
animation: pulse 1s infinite;
}
- ha-icon[data-domain="alarm_control_panel"][data-state="triggered"] {
+ ha-state-icon[data-domain="alarm_control_panel"][data-state="triggered"] {
color: var(--alarm-color-triggered, var(--label-badge-red));
animation: pulse 1s infinite;
}
@@ -68,13 +68,13 @@ export const iconColorCSS = css`
}
}
- ha-icon[data-domain="plant"][data-state="problem"],
- ha-icon[data-domain="zwave"][data-state="dead"] {
+ ha-state-icon[data-domain="plant"][data-state="problem"],
+ ha-state-icon[data-domain="zwave"][data-state="dead"] {
color: var(--state-icon-error-color);
}
/* Color the icon if unavailable */
- ha-icon[data-state="unavailable"] {
+ ha-state-icon[data-state="unavailable"] {
color: var(--state-unavailable-color);
}
`;
diff --git a/src/components/chart/state-history-chart-timeline.ts b/src/components/chart/state-history-chart-timeline.ts
index 6aa8c2951b..85de3ea9f7 100644
--- a/src/components/chart/state-history-chart-timeline.ts
+++ b/src/components/chart/state-history-chart-timeline.ts
@@ -12,21 +12,19 @@ import { HomeAssistant } from "../../types";
import "./ha-chart-base";
import type { TimeLineData } from "./timeline-chart/const";
-/** Binary sensor device classes for which the static colors for on/off need to be inverted.
- * List the ones were "off" = good or normal state = should be rendered "green".
+/** Binary sensor device classes for which the static colors for on/off are NOT inverted.
+ * List the ones were "on" = good or normal state => should be rendered "green".
+ * Note: It is now a "not inverted" list (compared to the past) since we now have more inverted ones.
*/
-const BINARY_SENSOR_DEVICE_CLASS_COLOR_INVERTED = new Set([
- "battery",
- "door",
- "garage_door",
- "gas",
- "lock",
- "motion",
- "opening",
- "problem",
- "safety",
- "smoke",
- "window",
+const BINARY_SENSOR_DEVICE_CLASS_COLOR_NOT_INVERTED = new Set([
+ "battery_charging",
+ "connectivity",
+ "light",
+ "moving",
+ "plug",
+ "power",
+ "presence",
+ "update",
]);
const STATIC_STATE_COLORS = new Set([
@@ -47,7 +45,7 @@ const invertOnOff = (entityState?: HassEntity) =>
entityState &&
computeDomain(entityState.entity_id) === "binary_sensor" &&
"device_class" in entityState.attributes &&
- BINARY_SENSOR_DEVICE_CLASS_COLOR_INVERTED.has(
+ !BINARY_SENSOR_DEVICE_CLASS_COLOR_NOT_INVERTED.has(
entityState.attributes.device_class!
);
diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts
index 6419406adf..11e0a50a30 100644
--- a/src/components/data-table/ha-data-table.ts
+++ b/src/components/data-table/ha-data-table.ts
@@ -30,6 +30,7 @@ import "../ha-checkbox";
import type { HaCheckbox } from "../ha-checkbox";
import "../ha-svg-icon";
import { filterData, sortData } from "./sort-filter";
+import { HomeAssistant } from "../../types";
declare global {
// for fire event
@@ -69,7 +70,7 @@ export interface DataTableSortColumnData {
export interface DataTableColumnData extends DataTableSortColumnData {
title: TemplateResult | string;
- type?: "numeric" | "icon" | "icon-button";
+ type?: "numeric" | "icon" | "icon-button" | "overflow-menu";
template?: (data: any, row: T) => TemplateResult | string;
width?: string;
maxWidth?: string;
@@ -93,6 +94,8 @@ export interface SortableColumnContainer {
@customElement("ha-data-table")
export class HaDataTable extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
@property({ type: Object }) public columns: DataTableColumnContainer = {};
@property({ type: Array }) public data: DataTableRowData[] = [];
@@ -232,6 +235,7 @@ export class HaDataTable extends LitElement {
? html`
@@ -64,6 +65,7 @@ class StateInfo extends LitElement {
diff --git a/src/components/ha-alert.ts b/src/components/ha-alert.ts
index ddd3c641e7..25c3975ac6 100644
--- a/src/components/ha-alert.ts
+++ b/src/components/ha-alert.ts
@@ -1,5 +1,4 @@
import "@material/mwc-button/mwc-button";
-import "@material/mwc-icon-button/mwc-icon-button";
import {
mdiAlertCircleOutline,
mdiAlertOutline,
@@ -11,6 +10,7 @@ import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../common/dom/fire_event";
+import "./ha-icon-button";
import "./ha-svg-icon";
const ALERT_ICONS = {
@@ -66,12 +66,11 @@ class HaAlert extends LitElement {
.label=${this.actionText}
>`
: this.dismissable
- ? html`
`
+ label="Dismiss alert"
+ .path=${mdiClose}
+ >`
: ""}