${updatesAvailable > 1
? "Updates Available 🎉"
@@ -76,26 +74,24 @@ export class HassioUpdate extends LitElement {
${this._renderUpdateCard(
"Home Assistant Core",
- this.hassInfo.version,
- this.hassInfo.version_latest,
+ this.hassInfo!,
"hassio/homeassistant/update",
`https://${
- this.hassInfo.version_latest.includes("b") ? "rc" : "www"
- }.home-assistant.io/latest-release-notes/`,
- mdiHomeAssistant
+ this.hassInfo?.version_latest.includes("b") ? "rc" : "www"
+ }.home-assistant.io/latest-release-notes/`
)}
${this._renderUpdateCard(
"Supervisor",
- this.supervisorInfo.version,
- this.supervisorInfo.version_latest,
+ this.supervisorInfo!,
"hassio/supervisor/update",
- `https://github.com//home-assistant/hassio/releases/tag/${this.supervisorInfo.version_latest}`
+ `https://github.com//home-assistant/hassio/releases/tag/${
+ this.supervisorInfo!.version_latest
+ }`
)}
${this.hassOsInfo
? this._renderUpdateCard(
"Operating System",
- this.hassOsInfo.version,
- this.hassOsInfo.version_latest,
+ this.hassOsInfo,
"hassio/os/update",
`https://github.com//home-assistant/hassos/releases/tag/${this.hassOsInfo.version_latest}`
)
@@ -107,28 +103,22 @@ export class HassioUpdate extends LitElement {
private _renderUpdateCard(
name: string,
- curVersion: string,
- lastVersion: string,
+ object: HassioHomeAssistantInfo | HassioSupervisorInfo | HassioHassOSInfo,
apiPath: string,
- releaseNotesUrl: string,
- icon?: string
+ releaseNotesUrl: string
): TemplateResult {
- if (!lastVersion || lastVersion === curVersion) {
+ if (!object.update_available) {
return html``;
}
return html`
- ${icon
- ? html`
-
-
-
- `
- : ""}
- ${name} ${lastVersion}
+
+
+
+ ${name} ${object.version_latest}
- You are currently running version ${curVersion}
+ You are currently running version ${object.version}
@@ -138,7 +128,7 @@ export class HassioUpdate extends LitElement {
Update
diff --git a/hassio/src/dialogs/network/dialog-hassio-network.ts b/hassio/src/dialogs/network/dialog-hassio-network.ts
index 1ab958891b..8569b8bc60 100644
--- a/hassio/src/dialogs/network/dialog-hassio-network.ts
+++ b/hassio/src/dialogs/network/dialog-hassio-network.ts
@@ -39,7 +39,8 @@ import type { HomeAssistant } from "../../../../src/types";
import { HassioNetworkDialogParams } from "./show-dialog-network";
@customElement("dialog-hassio-network")
-export class DialogHassioNetwork extends LitElement implements HassDialog {
+export class DialogHassioNetwork extends LitElement
+ implements HassDialog {
@property({ attribute: false }) public hass!: HomeAssistant;
@internalProperty() private _prosessing = false;
diff --git a/hassio/src/dialogs/registries/dialog-hassio-registries.ts b/hassio/src/dialogs/registries/dialog-hassio-registries.ts
new file mode 100644
index 0000000000..fef6e3e0a8
--- /dev/null
+++ b/hassio/src/dialogs/registries/dialog-hassio-registries.ts
@@ -0,0 +1,245 @@
+import "@material/mwc-button/mwc-button";
+import "@material/mwc-icon-button/mwc-icon-button";
+import "@material/mwc-list/mwc-list-item";
+import { mdiDelete } from "@mdi/js";
+import { PaperInputElement } from "@polymer/paper-input/paper-input";
+import {
+ css,
+ CSSResult,
+ customElement,
+ html,
+ internalProperty,
+ LitElement,
+ property,
+ TemplateResult,
+} from "lit-element";
+import "../../../../src/components/ha-circular-progress";
+import { createCloseHeading } from "../../../../src/components/ha-dialog";
+import "../../../../src/components/ha-svg-icon";
+import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
+import {
+ addHassioDockerRegistry,
+ fetchHassioDockerRegistries,
+ removeHassioDockerRegistry,
+} from "../../../../src/data/hassio/docker";
+import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box";
+import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
+import type { HomeAssistant } from "../../../../src/types";
+
+@customElement("dialog-hassio-registries")
+class HassioRegistriesDialog extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property({ attribute: false }) private _registries?: {
+ registry: string;
+ username: string;
+ }[];
+
+ @internalProperty() private _registry?: string;
+
+ @internalProperty() private _username?: string;
+
+ @internalProperty() private _password?: string;
+
+ @internalProperty() private _opened = false;
+
+ @internalProperty() private _addingRegistry = false;
+
+ protected render(): TemplateResult {
+ return html`
+
+
+ ${this._addingRegistry
+ ? html`
+
+
+
+
+
+ Add registry
+
+ `
+ : html`${this._registries?.length
+ ? this._registries.map((entry) => {
+ return html`
+
+ ${entry.registry}
+ Username: ${entry.username}
+
+
+
+
+ `;
+ })
+ : html`
+
+ No registries configured
+
+ `}
+
+ Add new registry
+ `}
+
+
+ `;
+ }
+
+ private _inputChanged(ev: Event) {
+ const target = ev.currentTarget as PaperInputElement;
+ this[`_${target.name}`] = target.value;
+ }
+
+ public async showDialog(_dialogParams: any): Promise {
+ this._opened = true;
+ await this._loadRegistries();
+ await this.updateComplete;
+ }
+
+ public closeDialog(): void {
+ this._addingRegistry = false;
+ this._opened = false;
+ }
+
+ public focus(): void {
+ this.updateComplete.then(() =>
+ (this.shadowRoot?.querySelector(
+ "[dialogInitialFocus]"
+ ) as HTMLElement)?.focus()
+ );
+ }
+
+ private async _loadRegistries(): Promise {
+ const registries = await fetchHassioDockerRegistries(this.hass);
+ this._registries = Object.keys(registries!.registries).map((key) => ({
+ registry: key,
+ username: registries.registries[key].username,
+ }));
+ }
+
+ private _addRegistry(): void {
+ this._addingRegistry = true;
+ }
+
+ private async _addNewRegistry(): Promise {
+ const data = {};
+ data[this._registry!] = {
+ username: this._username,
+ password: this._password,
+ };
+
+ try {
+ await addHassioDockerRegistry(this.hass, data);
+ await this._loadRegistries();
+ this._addingRegistry = false;
+ } catch (err) {
+ showAlertDialog(this, {
+ title: "Failed to add registry",
+ text: extractApiErrorMessage(err),
+ });
+ }
+ }
+
+ private async _removeRegistry(ev: Event): Promise {
+ const entry = (ev.currentTarget as any).entry;
+
+ try {
+ await removeHassioDockerRegistry(this.hass, entry.registry);
+ await this._loadRegistries();
+ } catch (err) {
+ showAlertDialog(this, {
+ title: "Failed to remove registry",
+ text: extractApiErrorMessage(err),
+ });
+ }
+ }
+
+ static get styles(): CSSResult[] {
+ return [
+ haStyle,
+ haStyleDialog,
+ css`
+ ha-dialog.button-left {
+ --justify-action-buttons: flex-start;
+ }
+ paper-icon-item {
+ cursor: pointer;
+ }
+ .form {
+ color: var(--primary-text-color);
+ }
+ .option {
+ border: 1px solid var(--divider-color);
+ border-radius: 4px;
+ margin-top: 4px;
+ }
+ mwc-button {
+ margin-left: 8px;
+ }
+ mwc-icon-button {
+ color: var(--error-color);
+ margin: -10px;
+ }
+ mwc-list-item {
+ cursor: default;
+ }
+ mwc-list-item span[slot="secondary"] {
+ color: var(--secondary-text-color);
+ }
+ ha-paper-dropdown-menu {
+ display: block;
+ }
+ `,
+ ];
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "dialog-hassio-registries": HassioRegistriesDialog;
+ }
+}
diff --git a/hassio/src/dialogs/registries/show-dialog-registries.ts b/hassio/src/dialogs/registries/show-dialog-registries.ts
new file mode 100644
index 0000000000..a9e871d17e
--- /dev/null
+++ b/hassio/src/dialogs/registries/show-dialog-registries.ts
@@ -0,0 +1,13 @@
+import { fireEvent } from "../../../../src/common/dom/fire_event";
+import "./dialog-hassio-registries";
+
+export const showRegistriesDialog = (element: HTMLElement): void => {
+ fireEvent(element, "show-dialog", {
+ dialogTag: "dialog-hassio-registries",
+ dialogImport: () =>
+ import(
+ /* webpackChunkName: "dialog-hassio-registries" */ "./dialog-hassio-registries"
+ ),
+ dialogParams: {},
+ });
+};
diff --git a/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts b/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts
index 150fc2b181..2d8d028cdf 100644
--- a/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts
+++ b/hassio/src/dialogs/repositories/dialog-hassio-repositories.ts
@@ -39,7 +39,7 @@ class HassioRepositoriesDialog extends LitElement {
@property({ attribute: false })
private _dialogParams?: HassioRepositoryDialogParams;
- @query("#repository_input") private _optionInput?: PaperInputElement;
+ @query("#repository_input", true) private _optionInput?: PaperInputElement;
@internalProperty() private _opened = false;
@@ -91,7 +91,7 @@ class HassioRepositoriesDialog extends LitElement {
title="Remove"
@click=${this._removeRepository}
>
-
+
`;
diff --git a/hassio/src/dialogs/snapshot/dialog-hassio-snapshot-upload.ts b/hassio/src/dialogs/snapshot/dialog-hassio-snapshot-upload.ts
index ed0532d29f..1f657bf0d8 100644
--- a/hassio/src/dialogs/snapshot/dialog-hassio-snapshot-upload.ts
+++ b/hassio/src/dialogs/snapshot/dialog-hassio-snapshot-upload.ts
@@ -19,7 +19,7 @@ import { HassioSnapshotUploadDialogParams } from "./show-dialog-snapshot-upload"
@customElement("dialog-hassio-snapshot-upload")
export class DialogHassioSnapshotUpload extends LitElement
- implements HassDialog {
+ implements HassDialog {
@property({ attribute: false }) public hass!: HomeAssistant;
@internalProperty() private _params?: HassioSnapshotUploadDialogParams;
diff --git a/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts b/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts
index 790ebc467c..d3524bc4ed 100755
--- a/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts
+++ b/hassio/src/dialogs/snapshot/dialog-hassio-snapshot.ts
@@ -1,6 +1,7 @@
import "@material/mwc-button";
import { mdiClose, mdiDelete, mdiDownload, mdiHistory } from "@mdi/js";
-import { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox";
+import "@polymer/paper-checkbox/paper-checkbox";
+import type { PaperCheckboxElement } from "@polymer/paper-checkbox/paper-checkbox";
import "@polymer/paper-input/paper-input";
import {
css,
@@ -196,7 +197,7 @@ class HassioSnapshotDialog extends LitElement {
@click=${this._downloadClicked}
slot="primaryAction"
>
-
+
Download Snapshot
`
: ""}
@@ -205,7 +206,7 @@ class HassioSnapshotDialog extends LitElement {
@click=${this._partialRestoreClicked}
slot="secondaryAction"
>
-
+
Restore Selected
${this._snapshot.type === "full"
@@ -214,7 +215,7 @@ class HassioSnapshotDialog extends LitElement {
@click=${this._fullRestoreClicked}
slot="secondaryAction"
>
-
+
Wipe & restore
`
@@ -224,7 +225,10 @@ class HassioSnapshotDialog extends LitElement {
@click=${this._deleteClicked}
slot="secondaryAction"
>
-
+
Delete Snapshot
`
: ""}
@@ -440,6 +444,19 @@ class HassioSnapshotDialog extends LitElement {
return;
}
+ if (window.location.href.includes("ui.nabu.casa")) {
+ const confirm = await showConfirmationDialog(this, {
+ title: "Potential slow download",
+ text:
+ "Downloading snapshots over the Nabu Casa URL will take some time, it is recomended to use your local URL instead, do you want to continue?",
+ confirmText: "continue",
+ dismissText: "cancel",
+ });
+ if (!confirm) {
+ return;
+ }
+ }
+
const name = this._computeName.replace(/[^a-z0-9]+/gi, "_");
const a = document.createElement("a");
a.href = signedPath.path;
diff --git a/hassio/src/hassio-panel-router.ts b/hassio/src/hassio-panel-router.ts
index 00830423b0..6cad299188 100644
--- a/hassio/src/hassio-panel-router.ts
+++ b/hassio/src/hassio-panel-router.ts
@@ -25,13 +25,13 @@ class HassioPanelRouter extends HassRouterPage {
@property({ type: Boolean }) public narrow!: boolean;
- @property({ attribute: false }) public supervisorInfo: HassioSupervisorInfo;
+ @property({ attribute: false }) public supervisorInfo?: HassioSupervisorInfo;
@property({ attribute: false }) public hassioInfo!: HassioInfo;
- @property({ attribute: false }) public hostInfo: HassioHostInfo;
+ @property({ attribute: false }) public hostInfo?: HassioHostInfo;
- @property({ attribute: false }) public hassInfo: HassioHomeAssistantInfo;
+ @property({ attribute: false }) public hassInfo?: HassioHomeAssistantInfo;
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
diff --git a/hassio/src/hassio-router.ts b/hassio/src/hassio-router.ts
index a3ecdaf784..52168d1b3f 100644
--- a/hassio/src/hassio-router.ts
+++ b/hassio/src/hassio-router.ts
@@ -66,15 +66,15 @@ class HassioRouter extends HassRouterPage {
},
};
- @internalProperty() private _supervisorInfo: HassioSupervisorInfo;
+ @internalProperty() private _supervisorInfo?: HassioSupervisorInfo;
- @internalProperty() private _hostInfo: HassioHostInfo;
+ @internalProperty() private _hostInfo?: HassioHostInfo;
@internalProperty() private _hassioInfo?: HassioInfo;
@internalProperty() private _hassOsInfo?: HassioHassOSInfo;
- @internalProperty() private _hassInfo: HassioHomeAssistantInfo;
+ @internalProperty() private _hassInfo?: HassioHomeAssistantInfo;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
diff --git a/hassio/src/hassio-tabs.ts b/hassio/src/hassio-tabs.ts
index f5dbaf4499..7c7601a6b3 100644
--- a/hassio/src/hassio-tabs.ts
+++ b/hassio/src/hassio-tabs.ts
@@ -8,7 +8,7 @@ export const supervisorTabs: PageNavigation[] = [
iconPath: mdiViewDashboard,
},
{
- name: "Add-on store",
+ name: "Add-on Store",
path: `/hassio/store`,
iconPath: mdiStore,
},
diff --git a/hassio/src/ingress-view/hassio-ingress-view.ts b/hassio/src/ingress-view/hassio-ingress-view.ts
index 8780d9617d..ec899c7094 100644
--- a/hassio/src/ingress-view/hassio-ingress-view.ts
+++ b/hassio/src/ingress-view/hassio-ingress-view.ts
@@ -57,7 +57,7 @@ class HassioIngressView extends LitElement {
aria-label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
@click=${this._toggleMenu}
>
-
+
${this._addon.name}
diff --git a/hassio/src/snapshots/hassio-snapshots.ts b/hassio/src/snapshots/hassio-snapshots.ts
index 8f040c906b..96dd6caecf 100644
--- a/hassio/src/snapshots/hassio-snapshots.ts
+++ b/hassio/src/snapshots/hassio-snapshots.ts
@@ -117,7 +117,7 @@ class HassioSnapshots extends LitElement {
@action=${this._handleAction}
>
-
+
Reload
@@ -131,7 +131,7 @@ class HassioSnapshots extends LitElement {
- Create snapshot
+ Create Snapshot
Snapshots allow you to easily backup and restore all data of your
@@ -219,7 +219,7 @@ class HassioSnapshots extends LitElement {
- Available snapshots
+ Available Snapshots
${this._snapshots === undefined
? undefined
diff --git a/hassio/src/system/hassio-host-info.ts b/hassio/src/system/hassio-host-info.ts
index 94400402ae..5a2c5776bc 100644
--- a/hassio/src/system/hassio-host-info.ts
+++ b/hassio/src/system/hassio-host-info.ts
@@ -87,7 +87,7 @@ class HassioHostInfo extends LitElement {
${this.hostInfo.features.includes("network")
? html`
- IP address
+ IP Address
${primaryIpAddress}
@@ -103,13 +103,13 @@ class HassioHostInfo extends LitElement {
- Operating system
+ Operating System
${this.hostInfo.operating_system}
- ${this.hostInfo.version !== this.hostInfo.version_latest &&
- this.hostInfo.features.includes("hassos")
+ ${this.hostInfo.features.includes("hassos") &&
+ this.hassOsInfo.update_available
? html`
{
const curHostname: string = this.hostInfo.hostname;
const hostname = await showPromptDialog(this, {
- title: "Change hostname",
+ title: "Change Hostname",
inputLabel: "Please enter a new hostname:",
inputType: "string",
defaultValue: curHostname,
diff --git a/hassio/src/system/hassio-supervisor-info.ts b/hassio/src/system/hassio-supervisor-info.ts
index fcec51d607..9a1fa07f5f 100644
--- a/hassio/src/system/hassio-supervisor-info.ts
+++ b/hassio/src/system/hassio-supervisor-info.ts
@@ -7,18 +7,21 @@ import {
property,
TemplateResult,
} from "lit-element";
+import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-switch";
+import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { HassioHostInfo as HassioHostInfoType } from "../../../src/data/hassio/host";
+import { fetchHassioResolution } from "../../../src/data/hassio/resolution";
import {
+ fetchHassioSupervisorInfo,
HassioSupervisorInfo as HassioSupervisorInfoType,
reloadSupervisor,
setSupervisorOption,
SupervisorOptions,
updateSupervisor,
- fetchHassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor";
import {
showAlertDialog,
@@ -26,14 +29,42 @@ import {
} from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
+import { documentationUrl } from "../../../src/util/documentation-url";
import { hassioStyle } from "../resources/hassio-style";
-import { extractApiErrorMessage } from "../../../src/data/hassio/common";
+
+const ISSUES = {
+ container: {
+ title: "Containers known to cause issues",
+ url: "/more-info/unsupported/container",
+ },
+ dbus: { title: "DBUS", url: "/more-info/unsupported/dbus" },
+ docker_configuration: {
+ title: "Docker Configuration",
+ url: "/more-info/unsupported/docker_configuration",
+ },
+ docker_version: {
+ title: "Docker Version",
+ url: "/more-info/unsupported/docker_version",
+ },
+ lxc: { title: "LXC", url: "/more-info/unsupported/lxc" },
+ network_manager: {
+ title: "Network Manager",
+ url: "/more-info/unsupported/network_manager",
+ },
+ os: { title: "Operating System", url: "/more-info/unsupported/os" },
+ privileged: {
+ title: "Supervisor is not privileged",
+ url: "/more-info/unsupported/privileged",
+ },
+ systemd: { title: "Systemd", url: "/more-info/unsupported/systemd" },
+};
@customElement("hassio-supervisor-info")
class HassioSupervisorInfo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
- @property() public supervisorInfo!: HassioSupervisorInfoType;
+ @property({ attribute: false })
+ public supervisorInfo!: HassioSupervisorInfoType;
@property() public hostInfo!: HassioHostInfoType;
@@ -51,12 +82,12 @@ class HassioSupervisorInfo extends LitElement {
- Newest version
+ Newest Version
${this.supervisorInfo.version_latest}
- ${this.supervisorInfo.version !== this.supervisorInfo.version_latest
+ ${this.supervisorInfo.update_available
? html`
- Share diagnostics
+ Share Diagnostics
Share crash reports and diagnostic information.
@@ -118,24 +149,19 @@ class HassioSupervisorInfo extends LitElement {
`
: html`
You are running an unsupported installation.
-
- Learn More
-
+ Learn more
+
`}
Reload
@@ -181,7 +207,7 @@ class HassioSupervisorInfo extends LitElement {
};
await setSupervisorOption(this.hass, data);
await reloadSupervisor(this.hass);
- this.supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
+ fireEvent(this, "hass-api-called", { success: true, response: null });
} catch (err) {
showAlertDialog(this, {
title: "Failed to set supervisor option",
@@ -212,7 +238,7 @@ class HassioSupervisorInfo extends LitElement {
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
- title: "Update supervisor",
+ title: "Update Supervisor",
text: `Are you sure you want to update supervisor to version ${this.supervisorInfo.version_latest}?`,
confirmText: "update",
dismissText: "cancel",
@@ -249,6 +275,32 @@ class HassioSupervisorInfo extends LitElement {
});
}
+ private async _unsupportedDialog(): Promise {
+ const resolution = await fetchHassioResolution(this.hass);
+ await showAlertDialog(this, {
+ title: "You are running an unsupported installation",
+ text: html`Below is a list of issues found with your installation, click
+ on the links to learn how you can resolve the issues.
+
+ ${resolution.unsupported.map(
+ (issue) => html`
+ -
+ ${ISSUES[issue]
+ ? html`
+ ${ISSUES[issue].title}
+ `
+ : issue}
+
+ `
+ )}
+
`,
+ });
+ }
+
private async _toggleDiagnostics(): Promise {
try {
const data: SupervisorOptions = {
diff --git a/hassio/src/system/hassio-supervisor-log.ts b/hassio/src/system/hassio-supervisor-log.ts
index 88528eb431..2e1e2162aa 100644
--- a/hassio/src/system/hassio-supervisor-log.ts
+++ b/hassio/src/system/hassio-supervisor-log.ts
@@ -76,7 +76,7 @@ class HassioSupervisorLog extends LitElement {
${this.hass.userData?.showAdvanced
? html`
+
${metrics.map((metric) =>
- this._renderMetric(metric.description, metric.value ?? 0)
+ this._renderMetric(
+ metric.description,
+ metric.value ?? 0,
+ metric.tooltip
+ )
)}
@@ -77,13 +88,17 @@ class HassioSystemMetrics extends LitElement {
this._loadData();
}
- private _renderMetric(description: string, value: number): TemplateResult {
+ private _renderMetric(
+ description: string,
+ value: number,
+ tooltip?: string
+ ): TemplateResult {
const roundedValue = roundWithOneDecimal(value);
return html`
${description}
-
+
${roundedValue}%
@@ -155,6 +170,7 @@ class HassioSystemMetrics extends LitElement {
}
.value {
width: 42px;
+ padding-right: 4px;
}
`,
];
diff --git a/package.json b/package.json
index e176dd4e88..0a0002bec7 100644
--- a/package.json
+++ b/package.json
@@ -22,28 +22,29 @@
"author": "Paulus Schoutsen (http://paulusschoutsen.nl)",
"license": "Apache-2.0",
"dependencies": {
- "@formatjs/intl-pluralrules": "^1.5.8",
+ "@formatjs/intl-getcanonicallocales": "^1.4.6",
+ "@formatjs/intl-pluralrules": "^3.4.10",
"@fullcalendar/common": "5.1.0",
"@fullcalendar/core": "5.1.0",
"@fullcalendar/daygrid": "5.1.0",
"@fullcalendar/interaction": "5.1.0",
"@fullcalendar/list": "5.1.0",
- "@material/chips": "=8.0.0-canary.096a7a066.0",
- "@material/circular-progress": "=8.0.0-canary.a78ceb112.0",
- "@material/mwc-button": "^0.18.0",
- "@material/mwc-checkbox": "^0.18.0",
- "@material/mwc-dialog": "^0.18.0",
- "@material/mwc-fab": "^0.18.0",
- "@material/mwc-formfield": "^0.18.0",
- "@material/mwc-icon-button": "^0.18.0",
- "@material/mwc-list": "^0.18.0",
- "@material/mwc-menu": "^0.18.0",
- "@material/mwc-radio": "^0.18.0",
- "@material/mwc-ripple": "^0.18.0",
- "@material/mwc-switch": "^0.18.0",
- "@material/mwc-tab": "^0.18.0",
- "@material/mwc-tab-bar": "^0.18.0",
- "@material/top-app-bar": "=8.0.0-canary.096a7a066.0",
+ "@material/chips": "=8.0.0-canary.774dcfc8e.0",
+ "@material/mwc-button": "^0.19.0",
+ "@material/mwc-checkbox": "^0.19.0",
+ "@material/mwc-circular-progress": "^0.19.0",
+ "@material/mwc-dialog": "^0.19.0",
+ "@material/mwc-fab": "^0.19.0",
+ "@material/mwc-formfield": "^0.19.0",
+ "@material/mwc-icon-button": "^0.19.0",
+ "@material/mwc-list": "^0.19.0",
+ "@material/mwc-menu": "^0.19.0",
+ "@material/mwc-radio": "^0.19.0",
+ "@material/mwc-ripple": "^0.19.0",
+ "@material/mwc-switch": "^0.19.0",
+ "@material/mwc-tab": "^0.19.0",
+ "@material/mwc-tab-bar": "^0.19.0",
+ "@material/top-app-bar": "=8.0.0-canary.774dcfc8e.0",
"@mdi/js": "5.6.55",
"@mdi/svg": "5.6.55",
"@polymer/app-layout": "^3.0.2",
@@ -77,7 +78,7 @@
"@polymer/paper-toast": "^3.0.1",
"@polymer/paper-tooltip": "^3.0.1",
"@polymer/polymer": "3.1.0",
- "@thomasloven/round-slider": "0.5.0",
+ "@thomasloven/round-slider": "0.5.2",
"@types/chromecast-caf-sender": "^1.0.3",
"@types/sortablejs": "^1.10.6",
"@vaadin/vaadin-combo-box": "^5.0.10",
@@ -88,11 +89,11 @@
"chartjs-chart-timeline": "^0.3.0",
"codemirror": "^5.49.0",
"comlink": "^4.3.0",
+ "core-js": "^3.6.5",
"cpx": "^1.5.0",
"cropperjs": "^1.5.7",
"deep-clone-simple": "^1.1.1",
"deep-freeze": "^0.0.1",
- "es6-object-assign": "^1.1.0",
"fecha": "^4.2.0",
"fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2",
@@ -103,15 +104,16 @@
"js-yaml": "^3.13.1",
"leaflet": "^1.4.0",
"leaflet-draw": "^1.0.4",
- "lit-element": "^2.3.1",
- "lit-html": "^1.2.1",
+ "lit-element": "^2.4.0",
+ "lit-html": "^1.3.0",
"lit-virtualizer": "^0.4.2",
"marked": "^1.1.1",
"mdn-polyfills": "^5.16.0",
"memoize-one": "^5.0.2",
- "node-vibrant": "^3.1.5",
+ "node-vibrant": "^3.1.6",
"proxy-polyfill": "^0.3.1",
"punycode": "^2.1.1",
+ "qrcode": "^1.4.4",
"regenerator-runtime": "^0.13.2",
"resize-observer-polyfill": "^1.5.1",
"roboto-fontface": "^0.10.0",
@@ -128,16 +130,18 @@
"xss": "^1.0.6"
},
"devDependencies": {
- "@babel/core": "^7.9.0",
- "@babel/plugin-external-helpers": "^7.8.3",
- "@babel/plugin-proposal-class-properties": "^7.8.3",
- "@babel/plugin-proposal-decorators": "^7.8.3",
- "@babel/plugin-proposal-nullish-coalescing-operator": "^7.8.3",
- "@babel/plugin-proposal-object-rest-spread": "^7.9.5",
- "@babel/plugin-proposal-optional-chaining": "^7.9.0",
+ "@babel/core": "^7.11.6",
+ "@babel/plugin-external-helpers": "^7.10.4",
+ "@babel/plugin-proposal-class-properties": "^7.10.4",
+ "@babel/plugin-proposal-decorators": "^7.10.5",
+ "@babel/plugin-proposal-nullish-coalescing-operator": "^7.10.4",
+ "@babel/plugin-proposal-object-rest-spread": "^7.11.0",
+ "@babel/plugin-proposal-optional-chaining": "^7.11.0",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
- "@babel/preset-env": "^7.9.5",
- "@babel/preset-typescript": "^7.9.0",
+ "@babel/plugin-syntax-import-meta": "^7.10.4",
+ "@babel/plugin-syntax-top-level-await": "^7.10.4",
+ "@babel/preset-env": "^7.11.5",
+ "@babel/preset-typescript": "^7.10.4",
"@rollup/plugin-commonjs": "^11.1.0",
"@rollup/plugin-json": "^4.0.3",
"@rollup/plugin-node-resolve": "^7.1.3",
@@ -154,8 +158,8 @@
"@types/mocha": "^7.0.2",
"@types/resize-observer-browser": "^0.1.3",
"@types/webspeechapi": "^0.0.29",
- "@typescript-eslint/eslint-plugin": "^2.28.0",
- "@typescript-eslint/parser": "^2.28.0",
+ "@typescript-eslint/eslint-plugin": "^4.4.0",
+ "@typescript-eslint/parser": "^4.4.0",
"babel-loader": "^8.1.0",
"chai": "^4.2.0",
"del": "^4.0.0",
@@ -180,7 +184,7 @@
"html-minifier": "^4.0.0",
"husky": "^1.3.1",
"lint-staged": "^8.1.5",
- "lit-analyzer": "^1.2.0",
+ "lit-analyzer": "^1.2.1",
"lodash.template": "^4.5.0",
"magic-string": "^0.25.7",
"map-stream": "^0.0.7",
@@ -201,29 +205,24 @@
"source-map-url": "^0.4.0",
"systemjs": "^6.3.2",
"terser-webpack-plugin": "^3.0.6",
- "ts-lit-plugin": "^1.2.0",
+ "ts-lit-plugin": "^1.2.1",
"ts-mocha": "^7.0.0",
- "typescript": "^3.8.3",
+ "typescript": "^4.0.3",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0",
- "webpack": "^4.40.2",
- "webpack-cli": "^3.3.9",
+ "webpack": "5.0.0-rc.3",
+ "webpack-cli": "4.0.0-rc.0",
"webpack-dev-server": "^3.10.3",
- "webpack-manifest-plugin": "^2.0.4",
- "workbox-build": "^5.1.3",
- "worker-plugin": "^4.0.3"
+ "webpack-manifest-plugin": "3.0.0-rc.0",
+ "workbox-build": "^5.1.3"
},
"_comment": "Polymer fixed to 3.1 because 3.2 throws on logbook page",
"_comment_2": "Fix in https://github.com/Polymer/polymer/pull/5569",
"resolutions": {
"@webcomponents/webcomponentsjs": "^2.2.10",
"@polymer/polymer": "3.1.0",
- "lit-html": "1.2.1",
- "lit-element": "2.3.1",
- "@material/animation": "8.0.0-canary.096a7a066.0",
- "@material/base": "8.0.0-canary.096a7a066.0",
- "@material/feature-targeting": "8.0.0-canary.096a7a066.0",
- "@material/theme": "8.0.0-canary.096a7a066.0"
+ "lit-html": "1.3.0",
+ "lit-element": "2.4.0"
},
"main": "src/home-assistant.js",
"husky": {
diff --git a/setup.py b/setup.py
index aeca082009..6077924a32 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
- version="20201001.2",
+ version="20201021.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors",
diff --git a/src/auth/ha-auth-flow.ts b/src/auth/ha-auth-flow.ts
index 4ad4b3330b..a4234c56ca 100644
--- a/src/auth/ha-auth-flow.ts
+++ b/src/auth/ha-auth-flow.ts
@@ -200,7 +200,7 @@ class HaAuthFlow extends litLocalizeLiteMixin(LitElement) {
private _redirect(authCode: string) {
// OAuth 2: 3.1.2 we need to retain query component of a redirect URI
- let url = this.redirectUri!!;
+ let url = this.redirectUri!;
if (!url.includes("?")) {
url += "?";
} else if (!url.endsWith("&")) {
diff --git a/src/common/datetime/relative_time.ts b/src/common/datetime/relative_time.ts
index c2a761394b..ef3b8b50c7 100644
--- a/src/common/datetime/relative_time.ts
+++ b/src/common/datetime/relative_time.ts
@@ -38,13 +38,11 @@ export default function relativeTime(
roundedDelta = Math.round(delta);
}
- const timeDesc = localize(
- `ui.components.relative_time.duration.${unit}`,
+ return localize(
+ options.includeTense === false
+ ? `ui.components.relative_time.duration.${unit}`
+ : `ui.components.relative_time.${tense}_duration.${unit}`,
"count",
roundedDelta
);
-
- return options.includeTense === false
- ? timeDesc
- : localize(`ui.components.relative_time.${tense}`, "time", timeDesc);
}
diff --git a/src/common/dom/dynamic-element-directive.ts b/src/common/dom/dynamic-element-directive.ts
index 8f05f437ef..fe863e48b8 100644
--- a/src/common/dom/dynamic-element-directive.ts
+++ b/src/common/dom/dynamic-element-directive.ts
@@ -10,10 +10,7 @@ export const dynamicElement = directive(
let element = part.value as HTMLElement | undefined;
- if (
- element !== undefined &&
- tag.toUpperCase() === (element as HTMLElement).tagName
- ) {
+ if (tag === element?.localName) {
if (properties) {
Object.entries(properties).forEach(([key, value]) => {
element![key] = value;
diff --git a/src/common/entity/binary_sensor_icon.ts b/src/common/entity/binary_sensor_icon.ts
index 1e0c5c5c7a..1ae025a2df 100644
--- a/src/common/entity/binary_sensor_icon.ts
+++ b/src/common/entity/binary_sensor_icon.ts
@@ -23,7 +23,7 @@ export const binarySensorIcon = (state?: string, stateObj?: HassEntity) => {
case "problem":
case "safety":
case "smoke":
- return is_off ? "hass:shield-check" : "hass:alert";
+ return is_off ? "hass:check-circle" : "hass:alert-circle";
case "heat":
return is_off ? "hass:thermometer" : "hass:fire";
case "light":
diff --git a/src/common/search/search-input.ts b/src/common/search/search-input.ts
index 1d52dbcde6..97a52b1864 100644
--- a/src/common/search/search-input.ts
+++ b/src/common/search/search-input.ts
@@ -51,21 +51,17 @@ class SearchInput extends LitElement {
@value-changed=${this._filterInputChanged}
.noLabelFloat=${this.noLabelFloat}
>
-
+
+
+
${this.filter &&
html`
-
+
`}
diff --git a/src/common/string/filter/char-code.ts b/src/common/string/filter/char-code.ts
new file mode 100644
index 0000000000..faa7210898
--- /dev/null
+++ b/src/common/string/filter/char-code.ts
@@ -0,0 +1,244 @@
+// MIT License
+
+// Copyright (c) 2015 - present Microsoft Corporation
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+// Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/
+
+/**
+ * An inlined enum containing useful character codes (to be used with String.charCodeAt).
+ * Please leave the const keyword such that it gets inlined when compiled to JavaScript!
+ */
+export enum CharCode {
+ Null = 0,
+ /**
+ * The `\b` character.
+ */
+ Backspace = 8,
+ /**
+ * The `\t` character.
+ */
+ Tab = 9,
+ /**
+ * The `\n` character.
+ */
+ LineFeed = 10,
+ /**
+ * The `\r` character.
+ */
+ CarriageReturn = 13,
+ Space = 32,
+ /**
+ * The `!` character.
+ */
+ ExclamationMark = 33,
+ /**
+ * The `"` character.
+ */
+ DoubleQuote = 34,
+ /**
+ * The `#` character.
+ */
+ Hash = 35,
+ /**
+ * The `$` character.
+ */
+ DollarSign = 36,
+ /**
+ * The `%` character.
+ */
+ PercentSign = 37,
+ /**
+ * The `&` character.
+ */
+ Ampersand = 38,
+ /**
+ * The `'` character.
+ */
+ SingleQuote = 39,
+ /**
+ * The `(` character.
+ */
+ OpenParen = 40,
+ /**
+ * The `)` character.
+ */
+ CloseParen = 41,
+ /**
+ * The `*` character.
+ */
+ Asterisk = 42,
+ /**
+ * The `+` character.
+ */
+ Plus = 43,
+ /**
+ * The `,` character.
+ */
+ Comma = 44,
+ /**
+ * The `-` character.
+ */
+ Dash = 45,
+ /**
+ * The `.` character.
+ */
+ Period = 46,
+ /**
+ * The `/` character.
+ */
+ Slash = 47,
+
+ Digit0 = 48,
+ Digit1 = 49,
+ Digit2 = 50,
+ Digit3 = 51,
+ Digit4 = 52,
+ Digit5 = 53,
+ Digit6 = 54,
+ Digit7 = 55,
+ Digit8 = 56,
+ Digit9 = 57,
+
+ /**
+ * The `:` character.
+ */
+ Colon = 58,
+ /**
+ * The `;` character.
+ */
+ Semicolon = 59,
+ /**
+ * The `<` character.
+ */
+ LessThan = 60,
+ /**
+ * The `=` character.
+ */
+ Equals = 61,
+ /**
+ * The `>` character.
+ */
+ GreaterThan = 62,
+ /**
+ * The `?` character.
+ */
+ QuestionMark = 63,
+ /**
+ * The `@` character.
+ */
+ AtSign = 64,
+
+ A = 65,
+ B = 66,
+ C = 67,
+ D = 68,
+ E = 69,
+ F = 70,
+ G = 71,
+ H = 72,
+ I = 73,
+ J = 74,
+ K = 75,
+ L = 76,
+ M = 77,
+ N = 78,
+ O = 79,
+ P = 80,
+ Q = 81,
+ R = 82,
+ S = 83,
+ T = 84,
+ U = 85,
+ V = 86,
+ W = 87,
+ X = 88,
+ Y = 89,
+ Z = 90,
+
+ /**
+ * The `[` character.
+ */
+ OpenSquareBracket = 91,
+ /**
+ * The `\` character.
+ */
+ Backslash = 92,
+ /**
+ * The `]` character.
+ */
+ CloseSquareBracket = 93,
+ /**
+ * The `^` character.
+ */
+ Caret = 94,
+ /**
+ * The `_` character.
+ */
+ Underline = 95,
+ /**
+ * The ``(`)`` character.
+ */
+ BackTick = 96,
+
+ a = 97,
+ b = 98,
+ c = 99,
+ d = 100,
+ e = 101,
+ f = 102,
+ g = 103,
+ h = 104,
+ i = 105,
+ j = 106,
+ k = 107,
+ l = 108,
+ m = 109,
+ n = 110,
+ o = 111,
+ p = 112,
+ q = 113,
+ r = 114,
+ s = 115,
+ t = 116,
+ u = 117,
+ v = 118,
+ w = 119,
+ x = 120,
+ y = 121,
+ z = 122,
+
+ /**
+ * The `{` character.
+ */
+ OpenCurlyBrace = 123,
+ /**
+ * The `|` character.
+ */
+ Pipe = 124,
+ /**
+ * The `}` character.
+ */
+ CloseCurlyBrace = 125,
+ /**
+ * The `~` character.
+ */
+ Tilde = 126,
+}
diff --git a/src/common/string/filter/filter.ts b/src/common/string/filter/filter.ts
new file mode 100644
index 0000000000..d3471433ab
--- /dev/null
+++ b/src/common/string/filter/filter.ts
@@ -0,0 +1,463 @@
+/* eslint-disable no-console */
+// MIT License
+
+// Copyright (c) 2015 - present Microsoft Corporation
+
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import { CharCode } from "./char-code";
+
+const _debug = false;
+
+export interface Match {
+ start: number;
+ end: number;
+}
+
+const _maxLen = 128;
+
+function initTable() {
+ const table: number[][] = [];
+ const row: number[] = [0];
+ for (let i = 1; i <= _maxLen; i++) {
+ row.push(-i);
+ }
+ for (let i = 0; i <= _maxLen; i++) {
+ const thisRow = row.slice(0);
+ thisRow[0] = -i;
+ table.push(thisRow);
+ }
+ return table;
+}
+
+function isSeparatorAtPos(value: string, index: number): boolean {
+ if (index < 0 || index >= value.length) {
+ return false;
+ }
+ const code = value.charCodeAt(index);
+ switch (code) {
+ case CharCode.Underline:
+ case CharCode.Dash:
+ case CharCode.Period:
+ case CharCode.Space:
+ case CharCode.Slash:
+ case CharCode.Backslash:
+ case CharCode.SingleQuote:
+ case CharCode.DoubleQuote:
+ case CharCode.Colon:
+ case CharCode.DollarSign:
+ return true;
+ default:
+ return false;
+ }
+}
+
+function isWhitespaceAtPos(value: string, index: number): boolean {
+ if (index < 0 || index >= value.length) {
+ return false;
+ }
+ const code = value.charCodeAt(index);
+ switch (code) {
+ case CharCode.Space:
+ case CharCode.Tab:
+ return true;
+ default:
+ return false;
+ }
+}
+
+function isUpperCaseAtPos(pos: number, word: string, wordLow: string): boolean {
+ return word[pos] !== wordLow[pos];
+}
+
+function isPatternInWord(
+ patternLow: string,
+ patternPos: number,
+ patternLen: number,
+ wordLow: string,
+ wordPos: number,
+ wordLen: number
+): boolean {
+ while (patternPos < patternLen && wordPos < wordLen) {
+ if (patternLow[patternPos] === wordLow[wordPos]) {
+ patternPos += 1;
+ }
+ wordPos += 1;
+ }
+ return patternPos === patternLen; // pattern must be exhausted
+}
+
+enum Arrow {
+ Top = 0b1,
+ Diag = 0b10,
+ Left = 0b100,
+}
+
+/**
+ * A tuple of three values.
+ * 0. the score
+ * 1. the matches encoded as bitmask (2^53)
+ * 2. the offset at which matching started
+ */
+export type FuzzyScore = [number, number, number];
+
+interface FilterGlobals {
+ _matchesCount: number;
+ _topMatch2: number;
+ _topScore: number;
+ _wordStart: number;
+ _firstMatchCanBeWeak: boolean;
+ _table: number[][];
+ _scores: number[][];
+ _arrows: Arrow[][];
+}
+
+function initGlobals(): FilterGlobals {
+ return {
+ _matchesCount: 0,
+ _topMatch2: 0,
+ _topScore: 0,
+ _wordStart: 0,
+ _firstMatchCanBeWeak: false,
+ _table: initTable(),
+ _scores: initTable(),
+ _arrows: initTable(),
+ };
+}
+
+export function fuzzyScore(
+ pattern: string,
+ patternLow: string,
+ patternStart: number,
+ word: string,
+ wordLow: string,
+ wordStart: number,
+ firstMatchCanBeWeak: boolean
+): FuzzyScore | undefined {
+ const globals = initGlobals();
+ const patternLen = pattern.length > _maxLen ? _maxLen : pattern.length;
+ const wordLen = word.length > _maxLen ? _maxLen : word.length;
+
+ if (
+ patternStart >= patternLen ||
+ wordStart >= wordLen ||
+ patternLen - patternStart > wordLen - wordStart
+ ) {
+ return undefined;
+ }
+
+ // Run a simple check if the characters of pattern occur
+ // (in order) at all in word. If that isn't the case we
+ // stop because no match will be possible
+ if (
+ !isPatternInWord(
+ patternLow,
+ patternStart,
+ patternLen,
+ wordLow,
+ wordStart,
+ wordLen
+ )
+ ) {
+ return undefined;
+ }
+
+ let row = 1;
+ let column = 1;
+ let patternPos = patternStart;
+ let wordPos = wordStart;
+
+ let hasStrongFirstMatch = false;
+
+ // There will be a match, fill in tables
+ for (
+ row = 1, patternPos = patternStart;
+ patternPos < patternLen;
+ row++, patternPos++
+ ) {
+ for (
+ column = 1, wordPos = wordStart;
+ wordPos < wordLen;
+ column++, wordPos++
+ ) {
+ const score = _doScore(
+ pattern,
+ patternLow,
+ patternPos,
+ patternStart,
+ word,
+ wordLow,
+ wordPos
+ );
+
+ if (patternPos === patternStart && score > 1) {
+ hasStrongFirstMatch = true;
+ }
+
+ globals._scores[row][column] = score;
+
+ const diag =
+ globals._table[row - 1][column - 1] + (score > 1 ? 1 : score);
+ const top = globals._table[row - 1][column] + -1;
+ const left = globals._table[row][column - 1] + -1;
+
+ if (left >= top) {
+ // left or diag
+ if (left > diag) {
+ globals._table[row][column] = left;
+ globals._arrows[row][column] = Arrow.Left;
+ } else if (left === diag) {
+ globals._table[row][column] = left;
+ globals._arrows[row][column] = Arrow.Left || Arrow.Diag;
+ } else {
+ globals._table[row][column] = diag;
+ globals._arrows[row][column] = Arrow.Diag;
+ }
+ } else if (top > diag) {
+ globals._table[row][column] = top;
+ globals._arrows[row][column] = Arrow.Top;
+ } else if (top === diag) {
+ globals._table[row][column] = top;
+ globals._arrows[row][column] = Arrow.Top || Arrow.Diag;
+ } else {
+ globals._table[row][column] = diag;
+ globals._arrows[row][column] = Arrow.Diag;
+ }
+ }
+ }
+
+ if (_debug) {
+ printTables(pattern, patternStart, word, wordStart, globals);
+ }
+
+ if (!hasStrongFirstMatch && !firstMatchCanBeWeak) {
+ return undefined;
+ }
+
+ globals._matchesCount = 0;
+ globals._topScore = -100;
+ globals._wordStart = wordStart;
+ globals._firstMatchCanBeWeak = firstMatchCanBeWeak;
+
+ _findAllMatches2(
+ row - 1,
+ column - 1,
+ patternLen === wordLen ? 1 : 0,
+ 0,
+ false,
+ globals
+ );
+ if (globals._matchesCount === 0) {
+ return undefined;
+ }
+
+ return [globals._topScore, globals._topMatch2, wordStart];
+}
+
+function _doScore(
+ pattern: string,
+ patternLow: string,
+ patternPos: number,
+ patternStart: number,
+ word: string,
+ wordLow: string,
+ wordPos: number
+) {
+ if (patternLow[patternPos] !== wordLow[wordPos]) {
+ return -1;
+ }
+ if (wordPos === patternPos - patternStart) {
+ // common prefix: `foobar <-> foobaz`
+ // ^^^^^
+ if (pattern[patternPos] === word[wordPos]) {
+ return 7;
+ }
+ return 5;
+ }
+
+ if (
+ isUpperCaseAtPos(wordPos, word, wordLow) &&
+ (wordPos === 0 || !isUpperCaseAtPos(wordPos - 1, word, wordLow))
+ ) {
+ // hitting upper-case: `foo <-> forOthers`
+ // ^^ ^
+ if (pattern[patternPos] === word[wordPos]) {
+ return 7;
+ }
+ return 5;
+ }
+
+ if (
+ isSeparatorAtPos(wordLow, wordPos) &&
+ (wordPos === 0 || !isSeparatorAtPos(wordLow, wordPos - 1))
+ ) {
+ // hitting a separator: `. <-> foo.bar`
+ // ^
+ return 5;
+ }
+
+ if (
+ isSeparatorAtPos(wordLow, wordPos - 1) ||
+ isWhitespaceAtPos(wordLow, wordPos - 1)
+ ) {
+ // post separator: `foo <-> bar_foo`
+ // ^^^
+ return 5;
+ }
+ return 1;
+}
+
+function printTable(
+ table: number[][],
+ pattern: string,
+ patternLen: number,
+ word: string,
+ wordLen: number
+): string {
+ function pad(s: string, n: number, _pad = " ") {
+ while (s.length < n) {
+ s = _pad + s;
+ }
+ return s;
+ }
+ let ret = ` | |${word
+ .split("")
+ .map((c) => pad(c, 3))
+ .join("|")}\n`;
+
+ for (let i = 0; i <= patternLen; i++) {
+ if (i === 0) {
+ ret += " |";
+ } else {
+ ret += `${pattern[i - 1]}|`;
+ }
+ ret +=
+ table[i]
+ .slice(0, wordLen + 1)
+ .map((n) => pad(n.toString(), 3))
+ .join("|") + "\n";
+ }
+ return ret;
+}
+
+function printTables(
+ pattern: string,
+ patternStart: number,
+ word: string,
+ wordStart: number,
+ globals: FilterGlobals
+): void {
+ pattern = pattern.substr(patternStart);
+ word = word.substr(wordStart);
+ console.log(
+ printTable(globals._table, pattern, pattern.length, word, word.length)
+ );
+ console.log(
+ printTable(globals._arrows, pattern, pattern.length, word, word.length)
+ );
+ console.log(
+ printTable(globals._scores, pattern, pattern.length, word, word.length)
+ );
+}
+
+function _findAllMatches2(
+ row: number,
+ column: number,
+ total: number,
+ matches: number,
+ lastMatched: boolean,
+ globals: FilterGlobals
+): void {
+ if (globals._matchesCount >= 10 || total < -25) {
+ // stop when having already 10 results, or
+ // when a potential alignment as already 5 gaps
+ return;
+ }
+
+ let simpleMatchCount = 0;
+
+ while (row > 0 && column > 0) {
+ const score = globals._scores[row][column];
+ const arrow = globals._arrows[row][column];
+
+ if (arrow === Arrow.Left) {
+ // left -> no match, skip a word character
+ column -= 1;
+ if (lastMatched) {
+ total -= 5; // new gap penalty
+ } else if (matches !== 0) {
+ total -= 1; // gap penalty after first match
+ }
+ lastMatched = false;
+ simpleMatchCount = 0;
+ } else if (arrow && Arrow.Diag) {
+ if (arrow && Arrow.Left) {
+ // left
+ _findAllMatches2(
+ row,
+ column - 1,
+ matches !== 0 ? total - 1 : total, // gap penalty after first match
+ matches,
+ lastMatched,
+ globals
+ );
+ }
+
+ // diag
+ total += score;
+ row -= 1;
+ column -= 1;
+ lastMatched = true;
+
+ // match -> set a 1 at the word pos
+ matches += 2 ** (column + globals._wordStart);
+
+ // count simple matches and boost a row of
+ // simple matches when they yield in a
+ // strong match.
+ if (score === 1) {
+ simpleMatchCount += 1;
+
+ if (row === 0 && !globals._firstMatchCanBeWeak) {
+ // when the first match is a weak
+ // match we discard it
+ return;
+ }
+ } else {
+ // boost
+ total += 1 + simpleMatchCount * (score - 1);
+ simpleMatchCount = 0;
+ }
+ } else {
+ return;
+ }
+ }
+
+ total -= column >= 3 ? 9 : column * 3; // late start penalty
+
+ // dynamically keep track of the current top score
+ // and insert the current best score at head, the rest at tail
+ globals._matchesCount += 1;
+ if (total > globals._topScore) {
+ globals._topScore = total;
+ globals._topMatch2 = matches;
+ }
+}
+
+// #endregion
diff --git a/src/common/string/filter/sequence-matching.ts b/src/common/string/filter/sequence-matching.ts
new file mode 100644
index 0000000000..f8ea1f40cf
--- /dev/null
+++ b/src/common/string/filter/sequence-matching.ts
@@ -0,0 +1,66 @@
+import { fuzzyScore } from "./filter";
+
+/**
+ * Determine whether a sequence of letters exists in another string,
+ * in that order, allowing for skipping. Ex: "chdr" exists in "chandelier")
+ *
+ * @param {string} filter - Sequence of letters to check for
+ * @param {string} word - Word to check for sequence
+ *
+ * @return {number} Score representing how well the word matches the filter. Return of 0 means no match.
+ */
+
+export const fuzzySequentialMatch = (filter: string, ...words: string[]) => {
+ let topScore = 0;
+
+ for (const word of words) {
+ const scores = fuzzyScore(
+ filter,
+ filter.toLowerCase(),
+ 0,
+ word,
+ word.toLowerCase(),
+ 0,
+ true
+ );
+
+ if (!scores) {
+ continue;
+ }
+
+ // The VS Code implementation of filter treats a score of "0" as just barely a match
+ // But we will typically use this matcher in a .filter(), which interprets 0 as a failure.
+ // By shifting all scores up by 1, we allow "0" matches, while retaining score precedence
+ const score = scores[0] + 1;
+
+ if (score > topScore) {
+ topScore = score;
+ }
+ }
+ return topScore;
+};
+
+export interface ScorableTextItem {
+ score?: number;
+ text: string;
+ altText?: string;
+}
+
+type FuzzyFilterSort = (
+ filter: string,
+ items: T[]
+) => T[];
+
+export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => {
+ return items
+ .map((item) => {
+ item.score = item.altText
+ ? fuzzySequentialMatch(filter, item.text, item.altText)
+ : fuzzySequentialMatch(filter, item.text);
+ return item;
+ })
+ .filter((item) => item.score === undefined || item.score > 0)
+ .sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) =>
+ scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0
+ );
+};
diff --git a/src/common/translations/localize.ts b/src/common/translations/localize.ts
index 696a765872..df7b993b1d 100644
--- a/src/common/translations/localize.ts
+++ b/src/common/translations/localize.ts
@@ -1,4 +1,5 @@
import IntlMessageFormat from "intl-messageformat";
+import { shouldPolyfill } from "@formatjs/intl-pluralrules/should-polyfill";
import { Resources } from "../../types";
export type LocalizeFunc = (key: string, ...args: any[]) => string;
@@ -12,8 +13,8 @@ export interface FormatsType {
time: FormatType;
}
-if (!Intl.PluralRules) {
- import("@formatjs/intl-pluralrules/polyfill-locales");
+if (shouldPolyfill()) {
+ await import("@formatjs/intl-pluralrules/polyfill-locales");
}
/**
diff --git a/src/common/util/debounce.ts b/src/common/util/debounce.ts
index fedd679e2e..557d596540 100644
--- a/src/common/util/debounce.ts
+++ b/src/common/util/debounce.ts
@@ -5,7 +5,7 @@
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
// eslint-disable-next-line: ban-types
-export const debounce = (
+export const debounce = unknown>(
func: T,
wait,
immediate = false
diff --git a/src/common/util/subscribe-one.ts b/src/common/util/subscribe-one.ts
index f86bfb7388..51bfa1081d 100644
--- a/src/common/util/subscribe-one.ts
+++ b/src/common/util/subscribe-one.ts
@@ -2,7 +2,10 @@ import { Connection, UnsubscribeFunc } from "home-assistant-js-websocket";
export const subscribeOne = async (
conn: Connection,
- subscribe: (conn: Connection, onChange: (items: T) => void) => UnsubscribeFunc
+ subscribe: (
+ conn2: Connection,
+ onChange: (items: T) => void
+ ) => UnsubscribeFunc
) =>
new Promise((resolve) => {
const unsub = subscribe(conn, (items) => {
diff --git a/src/common/util/throttle.ts b/src/common/util/throttle.ts
index 1cd98ea188..4832a2709b 100644
--- a/src/common/util/throttle.ts
+++ b/src/common/util/throttle.ts
@@ -5,7 +5,7 @@
// as much as it can, without ever going more than once per `wait` duration;
// but if you'd like to disable the execution on the leading edge, pass
// `false for leading`. To disable execution on the trailing edge, ditto.
-export const throttle = (
+export const throttle = unknown>(
func: T,
wait: number,
leading = true,
diff --git a/src/components/buttons/ha-progress-button.ts b/src/components/buttons/ha-progress-button.ts
index a446d456fc..c6c325fdab 100644
--- a/src/components/buttons/ha-progress-button.ts
+++ b/src/components/buttons/ha-progress-button.ts
@@ -21,7 +21,7 @@ class HaProgressButton extends LitElement {
@property({ type: Boolean }) public raised = false;
- @query("mwc-button") private _button?: Button;
+ @query("mwc-button", true) private _button?: Button;
public render(): TemplateResult {
return html`
diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts
index db83db5a6d..1b8530dcec 100644
--- a/src/components/data-table/ha-data-table.ts
+++ b/src/components/data-table/ha-data-table.ts
@@ -73,13 +73,17 @@ export interface DataTableColumnData extends DataTableSortColumnData {
hidden?: boolean;
}
+type ClonedDataTableColumnData = Omit & {
+ title?: string;
+};
+
export interface DataTableRowData {
[key: string]: any;
selectable?: boolean;
}
export interface SortableColumnContainer {
- [key: string]: DataTableSortColumnData;
+ [key: string]: ClonedDataTableColumnData;
}
@customElement("ha-data-table")
@@ -90,6 +94,8 @@ export class HaDataTable extends LitElement {
@property({ type: Boolean }) public selectable = false;
+ @property({ type: Boolean }) public clickable = false;
+
@property({ type: Boolean }) public hasFab = false;
@property({ type: Boolean, attribute: "auto-height" })
@@ -101,6 +107,9 @@ export class HaDataTable extends LitElement {
@property({ type: String }) public searchLabel?: string;
+ @property({ type: Boolean, attribute: "no-label-float" })
+ public noLabelFloat? = false;
+
@property({ type: String }) public filter = "";
@internalProperty() private _filterable = false;
@@ -113,9 +122,9 @@ export class HaDataTable extends LitElement {
@internalProperty() private _filteredData: DataTableRowData[] = [];
- @query("slot[name='header']") private _header!: HTMLSlotElement;
+ @internalProperty() private _headerHeight = 0;
- @query(".mdc-data-table__table") private _table!: HTMLDivElement;
+ @query("slot[name='header']") private _header!: HTMLSlotElement;
private _checkableRowsCount?: number;
@@ -166,11 +175,13 @@ export class HaDataTable extends LitElement {
}
const clonedColumns: DataTableColumnContainer = deepClone(this.columns);
- Object.values(clonedColumns).forEach((column: DataTableColumnData) => {
- delete column.title;
- delete column.type;
- delete column.template;
- });
+ Object.values(clonedColumns).forEach(
+ (column: ClonedDataTableColumnData) => {
+ delete column.title;
+ delete column.type;
+ delete column.template;
+ }
+ );
this._sortColumns = clonedColumns;
}
@@ -206,6 +217,7 @@ export class HaDataTable extends LitElement {
`
@@ -220,7 +232,7 @@ export class HaDataTable extends LitElement {
style=${styleMap({
height: this.autoHeight
? `${(this._filteredData.length || 1) * 53 + 57}px`
- : `calc(100% - ${this._header?.clientHeight}px)`,
+ : `calc(100% - ${this._headerHeight}px)`,
})}
>
@@ -317,12 +329,13 @@ export class HaDataTable extends LitElement {
> => {
if (!worker) {
- worker = wrap(new Worker("./sort_filter_worker", { type: "module" }));
+ worker = wrap(new Worker(new URL("./sort_filter_worker", import.meta.url)));
}
return await worker.filterData(data, columns, filter);
@@ -29,7 +29,7 @@ export const sortData = async (
sortColumn: SortDataParamTypes[3]
): Promise> => {
if (!worker) {
- worker = wrap(new Worker("./sort_filter_worker", { type: "module" }));
+ worker = wrap(new Worker(new URL("./sort_filter_worker", import.meta.url)));
}
return await worker.sortData(data, columns, direction, sortColumn);
diff --git a/src/components/date-range-picker.ts b/src/components/date-range-picker.ts
index cc0e7d3f29..70211da366 100644
--- a/src/components/date-range-picker.ts
+++ b/src/components/date-range-picker.ts
@@ -1,7 +1,7 @@
+// @ts-nocheck
import Vue from "vue";
import wrap from "@vue/web-component-wrapper";
import DateRangePicker from "vue2-daterange-picker";
-// @ts-ignore
import dateRangePickerStyles from "vue2-daterange-picker/dist/vue2-daterange-picker.css";
import { fireEvent } from "../common/dom/fire_event";
import { Constructor } from "../types";
@@ -35,7 +35,6 @@ const Component = Vue.extend({
},
},
render(createElement) {
- // @ts-ignore
return createElement(DateRangePicker, {
props: {
"time-picker": true,
@@ -52,7 +51,6 @@ const Component = Vue.extend({
endDate: this.endDate,
},
callback: (value) => {
- // @ts-ignore
fireEvent(this.$el as HTMLElement, "change", value);
},
expression: "dateRange",
diff --git a/src/components/device/ha-device-action-picker.ts b/src/components/device/ha-device-action-picker.ts
index eaca169438..e8e50f5b47 100644
--- a/src/components/device/ha-device-action-picker.ts
+++ b/src/components/device/ha-device-action-picker.ts
@@ -9,9 +9,17 @@ import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
@customElement("ha-device-action-picker")
class HaDeviceActionPicker extends HaDeviceAutomationPicker {
- protected NO_AUTOMATION_TEXT = "No actions";
+ protected get NO_AUTOMATION_TEXT() {
+ return this.hass.localize(
+ "ui.panel.config.devices.automation.actions.no_actions"
+ );
+ }
- protected UNKNOWN_AUTOMATION_TEXT = "Unknown action";
+ protected get UNKNOWN_AUTOMATION_TEXT() {
+ return this.hass.localize(
+ "ui.panel.config.devices.automation.actions.unknown_action"
+ );
+ }
constructor() {
super(
diff --git a/src/components/device/ha-device-automation-picker.ts b/src/components/device/ha-device-automation-picker.ts
index 540cfc5c9d..fe56884a09 100644
--- a/src/components/device/ha-device-automation-picker.ts
+++ b/src/components/device/ha-device-automation-picker.ts
@@ -33,16 +33,24 @@ export abstract class HaDeviceAutomationPicker<
@property() public value?: T;
- protected NO_AUTOMATION_TEXT = "No automations";
-
- protected UNKNOWN_AUTOMATION_TEXT = "Unknown automation";
-
@internalProperty() private _automations: T[] = [];
// Trigger an empty render so we start with a clean DOM.
// paper-listbox does not like changing things around.
@internalProperty() private _renderEmpty = false;
+ protected get NO_AUTOMATION_TEXT() {
+ return this.hass.localize(
+ "ui.panel.config.devices.automation.actions.no_actions"
+ );
+ }
+
+ protected get UNKNOWN_AUTOMATION_TEXT() {
+ return this.hass.localize(
+ "ui.panel.config.devices.automation.actions.unknown_action"
+ );
+ }
+
private _localizeDeviceAutomation: (
hass: HomeAssistant,
automation: T
diff --git a/src/components/device/ha-device-condition-picker.ts b/src/components/device/ha-device-condition-picker.ts
index dbe95c8f6a..89fefbafaf 100644
--- a/src/components/device/ha-device-condition-picker.ts
+++ b/src/components/device/ha-device-condition-picker.ts
@@ -11,9 +11,17 @@ import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
class HaDeviceConditionPicker extends HaDeviceAutomationPicker<
DeviceCondition
> {
- protected NO_AUTOMATION_TEXT = "No conditions";
+ protected get NO_AUTOMATION_TEXT() {
+ return this.hass.localize(
+ "ui.panel.config.devices.automation.conditions.no_conditions"
+ );
+ }
- protected UNKNOWN_AUTOMATION_TEXT = "Unknown condition";
+ protected get UNKNOWN_AUTOMATION_TEXT() {
+ return this.hass.localize(
+ "ui.panel.config.devices.automation.conditions.unknown_condition"
+ );
+ }
constructor() {
super(
diff --git a/src/components/device/ha-device-trigger-picker.ts b/src/components/device/ha-device-trigger-picker.ts
index ec140426ae..d1ff66d37e 100644
--- a/src/components/device/ha-device-trigger-picker.ts
+++ b/src/components/device/ha-device-trigger-picker.ts
@@ -9,9 +9,17 @@ import { HaDeviceAutomationPicker } from "./ha-device-automation-picker";
@customElement("ha-device-trigger-picker")
class HaDeviceTriggerPicker extends HaDeviceAutomationPicker {
- protected NO_AUTOMATION_TEXT = "No triggers";
+ protected get NO_AUTOMATION_TEXT() {
+ return this.hass.localize(
+ "ui.panel.config.devices.automation.triggers.no_triggers"
+ );
+ }
- protected UNKNOWN_AUTOMATION_TEXT = "Unknown trigger";
+ protected get UNKNOWN_AUTOMATION_TEXT() {
+ return this.hass.localize(
+ "ui.panel.config.devices.automation.triggers.unknown_trigger"
+ );
+ }
constructor() {
super(
diff --git a/src/components/entity/ha-chart-base.js b/src/components/entity/ha-chart-base.js
index 7df570747c..e02470bd08 100644
--- a/src/components/entity/ha-chart-base.js
+++ b/src/components/entity/ha-chart-base.js
@@ -71,14 +71,24 @@ class HaChartBase extends mixinBehaviors(
margin: 5px 0 0 0;
width: 100%;
}
+ .chartTooltip ul {
+ margin: 0 3px;
+ }
.chartTooltip li {
display: block;
white-space: pre-line;
}
+ .chartTooltip li::first-line {
+ line-height: 0;
+ }
.chartTooltip .title {
text-align: center;
font-weight: 500;
}
+ .chartTooltip .beforeBody {
+ text-align: center;
+ font-weight: 300;
+ }
.chartLegend li {
display: inline-block;
padding: 0 6px;
@@ -133,6 +143,9 @@ class HaChartBase extends mixinBehaviors(
style$="opacity:[[tooltip.opacity]]; top:[[tooltip.top]]; left:[[tooltip.left]]; padding:[[tooltip.yPadding]]px [[tooltip.xPadding]]px"
>
[[tooltip.title]]
+
+ [[tooltip.beforeBody]]
+
@@ -264,6 +277,10 @@ class HaChartBase extends mixinBehaviors(
const title = tooltip.title ? tooltip.title[0] || "" : "";
this.set(["tooltip", "title"], title);
+ if (tooltip.beforeBody) {
+ this.set(["tooltip", "beforeBody"], tooltip.beforeBody.join("\n"));
+ }
+
const bodyLines = tooltip.body.map((n) => n.lines);
// Set Text
diff --git a/src/components/entity/ha-entity-attribute-picker.ts b/src/components/entity/ha-entity-attribute-picker.ts
index ee2958323b..e9c3b52e0f 100644
--- a/src/components/entity/ha-entity-attribute-picker.ts
+++ b/src/components/entity/ha-entity-attribute-picker.ts
@@ -1,3 +1,4 @@
+import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
@@ -16,8 +17,9 @@ import {
import { fireEvent } from "../../common/dom/fire_event";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
-import "../ha-icon-button";
+import "../ha-svg-icon";
import "./state-badge";
+import "@material/mwc-icon-button/mwc-icon-button";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
@@ -55,7 +57,7 @@ class HaEntityAttributePicker extends LitElement {
@property({ type: Boolean }) private _opened = false;
- @query("vaadin-combo-box-light") private _comboBox!: HTMLElement;
+ @query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
protected shouldUpdate(changedProps: PropertyValues) {
return !(!changedProps.has("_opened") && this._opened);
@@ -80,6 +82,7 @@ class HaEntityAttributePicker extends LitElement {
.value=${this._value}
.allowCustomValue=${this.allowCustomValue}
.renderer=${rowRenderer}
+ attr-for-value="bind-value"
@opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged}
>
@@ -97,33 +100,35 @@ class HaEntityAttributePicker extends LitElement {
autocorrect="off"
spellcheck="false"
>
- ${this.value
- ? html`
-
- Clear
-
- `
- : ""}
+
+ ${this.value
+ ? html`
+
+
+
+ `
+ : ""}
-
- Toggle
-
+
+
+
+
`;
@@ -159,7 +164,10 @@ class HaEntityAttributePicker extends LitElement {
static get styles(): CSSResult {
return css`
- paper-input > ha-icon-button {
+ .suffix {
+ display: flex;
+ }
+ mwc-icon-button {
--mdc-icon-button-size: 24px;
padding: 0px 2px;
color: var(--secondary-text-color);
diff --git a/src/components/entity/ha-entity-picker.ts b/src/components/entity/ha-entity-picker.ts
index 94ddcf4517..6b06f855c2 100644
--- a/src/components/entity/ha-entity-picker.ts
+++ b/src/components/entity/ha-entity-picker.ts
@@ -1,3 +1,5 @@
+import "@material/mwc-icon-button/mwc-icon-button";
+import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
@@ -20,7 +22,7 @@ import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
-import "../ha-icon-button";
+import "../ha-svg-icon";
import "./state-badge";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
@@ -97,10 +99,12 @@ export class HaEntityPicker extends LitElement {
@property({ type: Boolean }) private _opened = false;
- @query("vaadin-combo-box-light") private _comboBox!: HTMLElement;
+ @query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
private _initedStates = false;
+ private _states: HassEntity[] = [];
+
private _getStates = memoizeOne(
(
_opened: boolean,
@@ -166,7 +170,7 @@ export class HaEntityPicker extends LitElement {
protected updated(changedProps: PropertyValues) {
if (!this._initedStates || (changedProps.has("_opened") && this._opened)) {
- const states = this._getStates(
+ this._states = this._getStates(
this._opened,
this.hass,
this.includeDomains,
@@ -174,7 +178,7 @@ export class HaEntityPicker extends LitElement {
this.entityFilter,
this.includeDeviceClasses
);
- (this._comboBox as any).items = states;
+ (this._comboBox as any).filteredItems = this._states;
this._initedStates = true;
}
}
@@ -192,6 +196,7 @@ export class HaEntityPicker extends LitElement {
.renderer=${rowRenderer}
@opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged}
+ @filter-changed=${this._filterChanged}
>
- ${this.value && !this.hideClearIcon
- ? html`
-
- Clear
-
- `
- : ""}
+
+ ${this.value && !this.hideClearIcon
+ ? html`
+
+
+
+ `
+ : ""}
-
- Toggle
-
+
+
+
+
`;
@@ -258,6 +265,15 @@ export class HaEntityPicker extends LitElement {
}
}
+ private _filterChanged(ev: CustomEvent): void {
+ const filterString = ev.detail.value.toLowerCase();
+ (this._comboBox as any).filteredItems = this._states.filter(
+ (state) =>
+ state.entity_id.toLowerCase().includes(filterString) ||
+ computeStateName(state).toLowerCase().includes(filterString)
+ );
+ }
+
private _setValue(value: string) {
this.value = value;
setTimeout(() => {
@@ -268,7 +284,10 @@ export class HaEntityPicker extends LitElement {
static get styles(): CSSResult {
return css`
- paper-input > ha-icon-button {
+ .suffix {
+ display: flex;
+ }
+ mwc-icon-button {
--mdc-icon-button-size: 24px;
padding: 0px 2px;
color: var(--secondary-text-color);
diff --git a/src/components/entity/ha-state-label-badge.ts b/src/components/entity/ha-state-label-badge.ts
index 675a7b739a..b38259b210 100644
--- a/src/components/entity/ha-state-label-badge.ts
+++ b/src/components/entity/ha-state-label-badge.ts
@@ -110,7 +110,9 @@ export class HaStateLabelBadge extends LitElement {
return null;
case "sensor":
default:
- return state.state === UNKNOWN
+ return state.attributes.device_class === "moon__phase"
+ ? null
+ : state.state === UNKNOWN
? "-"
: state.attributes.unit_of_measurement
? state.state
@@ -162,7 +164,9 @@ export class HaStateLabelBadge extends LitElement {
? "hass:timer-outline"
: "hass:timer-off-outline";
default:
- return null;
+ return state?.attributes.device_class === "moon__phase"
+ ? stateIcon(state)
+ : null;
}
}
diff --git a/src/components/entity/state-badge.ts b/src/components/entity/state-badge.ts
index c39fe4bf83..200b125e90 100644
--- a/src/components/entity/state-badge.ts
+++ b/src/components/entity/state-badge.ts
@@ -11,11 +11,14 @@ import {
} from "lit-element";
import { ifDefined } from "lit-html/directives/if-defined";
import { styleMap } from "lit-html/directives/style-map";
+
import { computeActiveState } from "../../common/entity/compute_active_state";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { stateIcon } from "../../common/entity/state_icon";
import { iconColorCSS } from "../../common/style/icon_color_css";
+
import type { HomeAssistant } from "../../types";
+
import "../ha-icon";
export class StateBadge extends LitElement {
@@ -37,7 +40,13 @@ export class StateBadge extends LitElement {
protected render(): TemplateResult {
const stateObj = this.stateObj;
- if (!stateObj || !this._showIcon) {
+ if (!stateObj) {
+ return html`
+
+ `;
+ }
+
+ if (!this._showIcon) {
return html``;
}
@@ -140,6 +149,9 @@ export class StateBadge extends LitElement {
ha-icon {
transition: color 0.3s ease-in-out, filter 0.3s ease-in-out;
}
+ .missing {
+ color: #fce588;
+ }
${iconColorCSS}
`;
diff --git a/src/components/ha-attributes.ts b/src/components/ha-attributes.ts
index a92d4912b6..933ed41d19 100644
--- a/src/components/ha-attributes.ts
+++ b/src/components/ha-attributes.ts
@@ -33,7 +33,9 @@ class HaAttributes extends LitElement {
).map(
(attribute) => html`
- ${attribute.replace(/_/g, " ")}
+
+ ${attribute.replace(/_/g, " ").replace("id", "ID")}
+
${this.formatAttribute(attribute)}
@@ -62,6 +64,9 @@ class HaAttributes extends LitElement {
max-width: 200px;
overflow-wrap: break-word;
}
+ .key:first-letter {
+ text-transform: capitalize;
+ }
.attribution {
color: var(--secondary-text-color);
text-align: right;
diff --git a/src/components/ha-bar.ts b/src/components/ha-bar.ts
index b9d160bf13..6d449c3306 100644
--- a/src/components/ha-bar.ts
+++ b/src/components/ha-bar.ts
@@ -34,8 +34,8 @@ export class HaBar extends LitElement {
return svg`
`;
@@ -43,6 +43,9 @@ export class HaBar extends LitElement {
static get styles(): CSSResult {
return css`
+ rect {
+ height: 100%;
+ }
rect:first-child {
width: 100%;
fill: var(--ha-bar-background-color, var(--secondary-background-color));
diff --git a/src/components/ha-button-menu.ts b/src/components/ha-button-menu.ts
index 7adc266a56..5aafc86d78 100644
--- a/src/components/ha-button-menu.ts
+++ b/src/components/ha-button-menu.ts
@@ -23,7 +23,7 @@ export class HaButtonMenu extends LitElement {
@property({ type: Boolean }) public disabled = false;
- @query("mwc-menu") private _menu?: Menu;
+ @query("mwc-menu", true) private _menu?: Menu;
public get items() {
return this._menu?.items;
@@ -62,6 +62,9 @@ export class HaButtonMenu extends LitElement {
display: inline-block;
position: relative;
}
+ ::slotted([disabled]) {
+ color: var(--disabled-text-color);
+ }
`;
}
}
diff --git a/src/components/ha-card.ts b/src/components/ha-card.ts
index cf215968c4..157d663bfe 100644
--- a/src/components/ha-card.ts
+++ b/src/components/ha-card.ts
@@ -50,9 +50,12 @@ export class HaCard extends LitElement {
font-family: var(--ha-card-header-font-family, inherit);
font-size: var(--ha-card-header-font-size, 24px);
letter-spacing: -0.012em;
- line-height: 32px;
- padding: 24px 16px 16px;
+ line-height: 48px;
+ padding: 12px 16px 16px;
display: block;
+ margin-block-start: 0px;
+ margin-block-end: 0px;
+ font-weight: normal;
}
:host ::slotted(.card-content:not(:first-child)),
@@ -75,7 +78,7 @@ export class HaCard extends LitElement {
protected render(): TemplateResult {
return html`
${this.header
- ? html` ${this.header} `
+ ? html`${this.header}
`
: html``}
`;
diff --git a/src/components/ha-circular-progress.ts b/src/components/ha-circular-progress.ts
index d28837872e..1e70908291 100644
--- a/src/components/ha-circular-progress.ts
+++ b/src/components/ha-circular-progress.ts
@@ -1,20 +1,9 @@
-// @ts-ignore
-import progressStyles from "@material/circular-progress/dist/mdc.circular-progress.min.css";
-import {
- css,
- customElement,
- html,
- LitElement,
- property,
- svg,
- SVGTemplateResult,
- TemplateResult,
- unsafeCSS,
-} from "lit-element";
-import { classMap } from "lit-html/directives/class-map";
+import { customElement, property } from "lit-element";
+import { CircularProgress } from "@material/mwc-circular-progress";
@customElement("ha-circular-progress")
-export class HaCircularProgress extends LitElement {
+// @ts-ignore
+export class HaCircularProgress extends CircularProgress {
@property({ type: Boolean })
public active = false;
@@ -24,65 +13,31 @@ export class HaCircularProgress extends LitElement {
@property()
public size: "small" | "medium" | "large" = "medium";
- protected render(): TemplateResult {
- let indeterminatePart: SVGTemplateResult;
-
- if (this.size === "small") {
- indeterminatePart = svg`
- `;
- } else if (this.size === "large") {
- indeterminatePart = svg`
- `;
- } else {
- // medium
- indeterminatePart = svg`
- `;
- }
-
- // ignoring prettier as it will introduce unwanted whitespace
- // We have not implemented the determinate support of mdc circular progress.
- // prettier-ignore
- return html`
-
-
-
-
- ${indeterminatePart}
-
- ${indeterminatePart}
-
- ${indeterminatePart}
-
-
-
-
- `;
+ // @ts-ignore
+ public set density(_) {
+ // just a dummy
}
- static get styles() {
- return [
- unsafeCSS(progressStyles),
- css`
- :host {
- text-align: initial;
- }
- `,
- ];
+ public get density() {
+ switch (this.size) {
+ case "small":
+ return -5;
+ case "medium":
+ return 0;
+ case "large":
+ return 5;
+ default:
+ return 0;
+ }
+ }
+
+ // @ts-ignore
+ public set indeterminate(_) {
+ // just a dummy
+ }
+
+ public get indeterminate() {
+ return this.active;
}
}
diff --git a/src/components/ha-date-range-picker.ts b/src/components/ha-date-range-picker.ts
index 884c893d56..895a33f984 100644
--- a/src/components/ha-date-range-picker.ts
+++ b/src/components/ha-date-range-picker.ts
@@ -60,7 +60,7 @@ export class HaDateRangePicker extends LitElement {
?ranges=${this.ranges !== undefined}
>
-
+
html`
class="header_button"
dir=${computeRTLDirection(hass)}
>
-
+
`;
diff --git a/src/components/ha-expansion-panel.ts b/src/components/ha-expansion-panel.ts
new file mode 100644
index 0000000000..6805ecfa82
--- /dev/null
+++ b/src/components/ha-expansion-panel.ts
@@ -0,0 +1,119 @@
+import {
+ css,
+ CSSResult,
+ customElement,
+ html,
+ LitElement,
+ property,
+ query,
+ TemplateResult,
+} from "lit-element";
+import { fireEvent } from "../common/dom/fire_event";
+import "./ha-svg-icon";
+import { mdiChevronDown } from "@mdi/js";
+import { classMap } from "lit-html/directives/class-map";
+
+@customElement("ha-expansion-panel")
+class HaExpansionPanel extends LitElement {
+ @property({ type: Boolean, reflect: true }) expanded = false;
+
+ @property({ type: Boolean, reflect: true }) outlined = false;
+
+ @query(".container") private _container!: HTMLDivElement;
+
+ protected render(): TemplateResult {
+ return html`
+
+
+
+
+
+ `;
+ }
+
+ private _handleTransitionEnd() {
+ this._container.style.removeProperty("height");
+ }
+
+ private _toggleContainer(): void {
+ const scrollHeight = this._container.scrollHeight;
+ this._container.style.height = `${scrollHeight}px`;
+
+ if (this.expanded) {
+ setTimeout(() => {
+ this._container.style.height = "0px";
+ }, 0);
+ }
+
+ this.expanded = !this.expanded;
+ fireEvent(this, "expanded-changed", { expanded: this.expanded });
+ }
+
+ static get styles(): CSSResult {
+ return css`
+ :host {
+ display: block;
+ }
+
+ :host([outlined]) {
+ box-shadow: none;
+ border-width: 1px;
+ border-style: solid;
+ border-color: var(
+ --ha-card-border-color,
+ var(--divider-color, #e0e0e0)
+ );
+ border-radius: var(--ha-card-border-radius, 4px);
+ }
+
+ .summary {
+ display: flex;
+ padding: 0px 16px;
+ min-height: 48px;
+ align-items: center;
+ cursor: pointer;
+ overflow: hidden;
+ }
+
+ .summary-icon {
+ transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
+ margin-left: auto;
+ }
+
+ .summary-icon.expanded {
+ transform: rotate(180deg);
+ }
+
+ .container {
+ overflow: hidden;
+ transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1);
+ height: 0px;
+ }
+
+ .container.expanded {
+ height: auto;
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-expansion-panel": HaExpansionPanel;
+ }
+
+ // for fire event
+ interface HASSDomEvents {
+ "expanded-changed": {
+ expanded: boolean;
+ };
+ }
+}
diff --git a/src/components/ha-form/ha-form-boolean.ts b/src/components/ha-form/ha-form-boolean.ts
index c8c956ceca..454a01e718 100644
--- a/src/components/ha-form/ha-form-boolean.ts
+++ b/src/components/ha-form/ha-form-boolean.ts
@@ -27,7 +27,7 @@ export class HaFormBoolean extends LitElement implements HaFormElement {
@property() public suffix!: string;
- @query("paper-checkbox") private _input?: HTMLElement;
+ @query("paper-checkbox", true) private _input?: HTMLElement;
public focus() {
if (this._input) {
diff --git a/src/components/ha-form/ha-form-float.ts b/src/components/ha-form/ha-form-float.ts
index d915434a3d..9720f0ffd2 100644
--- a/src/components/ha-form/ha-form-float.ts
+++ b/src/components/ha-form/ha-form-float.ts
@@ -21,7 +21,7 @@ export class HaFormFloat extends LitElement implements HaFormElement {
@property() public suffix!: string;
- @query("paper-input") private _input?: HTMLElement;
+ @query("paper-input", true) private _input?: HTMLElement;
public focus() {
if (this._input) {
diff --git a/src/components/ha-form/ha-form-multi_select.ts b/src/components/ha-form/ha-form-multi_select.ts
index b46218ae6f..29f174a160 100644
--- a/src/components/ha-form/ha-form-multi_select.ts
+++ b/src/components/ha-form/ha-form-multi_select.ts
@@ -35,7 +35,7 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
@internalProperty() private _init = false;
- @query("paper-menu-button") private _input?: HTMLElement;
+ @query("paper-menu-button", true) private _input?: HTMLElement;
public focus(): void {
if (this._input) {
diff --git a/src/components/ha-form/ha-form-positive_time_period_dict.ts b/src/components/ha-form/ha-form-positive_time_period_dict.ts
index b43bbd4ffa..d5a0db5975 100644
--- a/src/components/ha-form/ha-form-positive_time_period_dict.ts
+++ b/src/components/ha-form/ha-form-positive_time_period_dict.ts
@@ -20,7 +20,7 @@ export class HaFormTimePeriod extends LitElement implements HaFormElement {
@property() public suffix!: string;
- @query("paper-time-input") private _input?: HTMLElement;
+ @query("paper-time-input", true) private _input?: HTMLElement;
public focus() {
if (this._input) {
diff --git a/src/components/ha-form/ha-form-select.ts b/src/components/ha-form/ha-form-select.ts
index 359a06c5a8..bac4f8d963 100644
--- a/src/components/ha-form/ha-form-select.ts
+++ b/src/components/ha-form/ha-form-select.ts
@@ -24,7 +24,7 @@ export class HaFormSelect extends LitElement implements HaFormElement {
@property() public suffix!: string;
- @query("ha-paper-dropdown-menu") private _input?: HTMLElement;
+ @query("ha-paper-dropdown-menu", true) private _input?: HTMLElement;
public focus() {
if (this._input) {
diff --git a/src/components/ha-form/ha-form-string.ts b/src/components/ha-form/ha-form-string.ts
index 46bdbee332..3f62f9de31 100644
--- a/src/components/ha-form/ha-form-string.ts
+++ b/src/components/ha-form/ha-form-string.ts
@@ -55,6 +55,7 @@ export class HaFormString extends LitElement implements HaFormElement {
id="iconButton"
title="Click to toggle between masked and clear password"
@click=${this._toggleUnmaskedPassword}
+ tabindex="-1"
>
diff --git a/src/components/ha-hls-player.ts b/src/components/ha-hls-player.ts
index 61f189713c..5b84c8d4e2 100644
--- a/src/components/ha-hls-player.ts
+++ b/src/components/ha-hls-player.ts
@@ -38,6 +38,7 @@ class HaHLSPlayer extends LitElement {
@property({ type: Boolean, attribute: "allow-exoplayer" })
public allowExoPlayer = false;
+ // don't cache this, as we remove it on disconnects
@query("video") private _videoEl!: HTMLVideoElement;
@internalProperty() private _attached = false;
@@ -154,6 +155,9 @@ class HaHLSPlayer extends LitElement {
}
private _resizeExoPlayer = () => {
+ if (!this._videoEl) {
+ return;
+ }
const rect = this._videoEl.getBoundingClientRect();
this.hass!.auth.external!.fireMessage({
type: "exoplayer/resize",
diff --git a/src/components/ha-labeled-slider.js b/src/components/ha-labeled-slider.js
index a620ec0204..147fd5b1b1 100644
--- a/src/components/ha-labeled-slider.js
+++ b/src/components/ha-labeled-slider.js
@@ -14,8 +14,8 @@ class HaLabeledSlider extends PolymerElement {
}
.title {
- margin-bottom: 16px;
- color: var(--secondary-text-color);
+ margin-bottom: 8px;
+ color: var(--primary-text-color);
}
.slider-container {
@@ -43,7 +43,6 @@ class HaLabeledSlider extends PolymerElement {
step="[[step]]"
pin="[[pin]]"
disabled="[[disabled]]"
- disabled="[[disabled]]"
value="{{value}}"
>
diff --git a/src/components/ha-menu-button.ts b/src/components/ha-menu-button.ts
index 7531c989d9..b95d012938 100644
--- a/src/components/ha-menu-button.ts
+++ b/src/components/ha-menu-button.ts
@@ -6,9 +6,9 @@ import {
CSSResult,
customElement,
html,
+ internalProperty,
LitElement,
property,
- internalProperty,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../common/dom/fire_event";
@@ -62,7 +62,7 @@ class HaMenuButton extends LitElement {
aria-label=${this.hass.localize("ui.sidebar.sidebar_toggle")}
@click=${this._toggleMenu}
>
-
+
${hasNotifications ? html` ` : ""}
`;
@@ -98,8 +98,7 @@ class HaMenuButton extends LitElement {
return;
}
- this.style.visibility =
- newNarrow || this._alwaysVisible ? "initial" : "hidden";
+ this.style.display = newNarrow || this._alwaysVisible ? "initial" : "none";
if (!newNarrow) {
this._hasNotifications = false;
diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts
index 12ac252e73..086b0bfdba 100644
--- a/src/components/ha-sidebar.ts
+++ b/src/components/ha-sidebar.ts
@@ -728,6 +728,7 @@ class HaSidebar extends LitElement {
width: 64px;
}
:host([expanded]) {
+ width: 256px;
width: calc(256px + env(safe-area-inset-left));
}
:host([rtl]) {
@@ -735,8 +736,7 @@ class HaSidebar extends LitElement {
border-left: 1px solid var(--divider-color);
}
.menu {
- box-sizing: border-box;
- height: 65px;
+ height: var(--header-height);
display: flex;
padding: 0 8.5px;
border-bottom: 1px solid transparent;
@@ -793,7 +793,10 @@ class HaSidebar extends LitElement {
display: flex;
flex-direction: column;
box-sizing: border-box;
- height: calc(100% - 196px - env(safe-area-inset-bottom));
+ height: calc(100% - var(--header-height) - 132px);
+ height: calc(
+ 100% - var(--header-height) - 132px - env(safe-area-inset-bottom)
+ );
overflow-x: hidden;
background: none;
margin-left: env(safe-area-inset-left);
diff --git a/src/components/ha-slider.js b/src/components/ha-slider.js
index 9a2a9a09c9..f520fa20ef 100644
--- a/src/components/ha-slider.js
+++ b/src/components/ha-slider.js
@@ -21,6 +21,7 @@ class HaSlider extends PaperSliderClass {
.pin > .slider-knob > .slider-knob-inner {
font-size: var(--ha-slider-pin-font-size, 10px);
line-height: normal;
+ cursor: pointer;
}
.disabled.ring > .slider-knob > .slider-knob-inner {
@@ -69,9 +70,9 @@ class HaSlider extends PaperSliderClass {
transform: scale(1) translate(0, -10px);
}
- .slider-input {
- width: 54px;
- }
+ .slider-input {
+ width: 54px;
+ }
`)
);
}
diff --git a/src/components/ha-tabs.ts b/src/components/ha-tabs.ts
new file mode 100644
index 0000000000..b6dc45dcf1
--- /dev/null
+++ b/src/components/ha-tabs.ts
@@ -0,0 +1,100 @@
+import "@polymer/paper-tabs/paper-tabs";
+import type { PaperIconButtonElement } from "@polymer/paper-icon-button/paper-icon-button";
+import type { PaperTabElement } from "@polymer/paper-tabs/paper-tab";
+import type { PaperTabsElement } from "@polymer/paper-tabs/paper-tabs";
+import { customElement } from "lit-element";
+import { Constructor } from "../types";
+
+const PaperTabs = customElements.get("paper-tabs") as Constructor<
+ PaperTabsElement
+>;
+
+let subTemplate: HTMLTemplateElement;
+
+@customElement("ha-tabs")
+export class HaTabs extends PaperTabs {
+ private _firstTabWidth = 0;
+
+ private _lastTabWidth = 0;
+
+ private _lastLeftHiddenState = false;
+
+ static get template(): HTMLTemplateElement {
+ if (!subTemplate) {
+ subTemplate = (PaperTabs as any).template.cloneNode(true);
+
+ const superStyle = subTemplate.content.querySelector("style");
+
+ // Add "noink" attribute for scroll buttons to disable animation.
+ subTemplate.content
+ .querySelectorAll("paper-icon-button")
+ .forEach((arrow: PaperIconButtonElement) => {
+ arrow.setAttribute("noink", "");
+ });
+
+ superStyle!.appendChild(
+ document.createTextNode(`
+ :host {
+ padding-top: .5px;
+ }
+ .not-visible {
+ display: none;
+ }
+ paper-icon-button {
+ width: 24px;
+ height: 48px;
+ padding: 0;
+ margin: 0;
+ }
+ `)
+ );
+ }
+ return subTemplate;
+ }
+
+ // Get first and last tab's width for _affectScroll
+ public _tabChanged(tab: PaperTabElement, old: PaperTabElement): void {
+ super._tabChanged(tab, old);
+ const tabs = this.querySelectorAll("paper-tab:not(.hide-tab)");
+ if (tabs.length > 0) {
+ this._firstTabWidth = tabs[0].clientWidth;
+ this._lastTabWidth = tabs[tabs.length - 1].clientWidth;
+ }
+
+ // Scroll active tab into view if needed.
+ const selected = this.querySelector(".iron-selected");
+ if (selected) {
+ selected.scrollIntoView();
+ }
+ }
+
+ /**
+ * Modify _affectScroll so that when the scroll arrows appear
+ * while scrolling and the tab container shrinks we can counteract
+ * the jump in tab position so that the scroll still appears smooth.
+ */
+ public _affectScroll(dx: number): void {
+ if (this._firstTabWidth === 0 || this._lastTabWidth === 0) {
+ return;
+ }
+
+ this.$.tabsContainer.scrollLeft += dx;
+
+ const scrollLeft = this.$.tabsContainer.scrollLeft;
+
+ this._leftHidden = scrollLeft - this._firstTabWidth < 0;
+ this._rightHidden =
+ scrollLeft + this._lastTabWidth > this._tabContainerScrollSize;
+
+ if (this._lastLeftHiddenState !== this._leftHidden) {
+ this._lastLeftHiddenState = this._leftHidden;
+ this.$.tabsContainer.scrollLeft += this._leftHidden ? -23 : 23;
+ }
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-tabs": HaTabs;
+ }
+}
diff --git a/src/components/ha-yaml-editor.ts b/src/components/ha-yaml-editor.ts
index 596e878b51..4d2f4c2c59 100644
--- a/src/components/ha-yaml-editor.ts
+++ b/src/components/ha-yaml-editor.ts
@@ -20,7 +20,7 @@ declare global {
}
}
-const isEmpty = (obj: object): boolean => {
+const isEmpty = (obj: Record): boolean => {
if (typeof obj !== "object") {
return false;
}
@@ -44,7 +44,7 @@ export class HaYamlEditor extends LitElement {
@internalProperty() private _yaml = "";
- @query("ha-code-editor") private _editor?: HaCodeEditor;
+ @query("ha-code-editor", true) private _editor?: HaCodeEditor;
public setValue(value): void {
try {
@@ -105,6 +105,10 @@ export class HaYamlEditor extends LitElement {
fireEvent(this, "value-changed", { value: parsed, isValid } as any);
}
+
+ get yaml() {
+ return this._editor?.value;
+ }
}
declare global {
diff --git a/src/components/map/ha-location-editor.ts b/src/components/map/ha-location-editor.ts
index 3b42fd38cc..8ba1ec3c2b 100644
--- a/src/components/map/ha-location-editor.ts
+++ b/src/components/map/ha-location-editor.ts
@@ -61,8 +61,8 @@ class LocationEditor extends LitElement {
if (!this._leafletMap || !this.location) {
return;
}
- if ((this._locationMarker as Circle).getBounds) {
- this._leafletMap.fitBounds((this._locationMarker as Circle).getBounds());
+ if (this._locationMarker && "getBounds" in this._locationMarker) {
+ this._leafletMap.fitBounds(this._locationMarker.getBounds());
} else {
this._leafletMap.setView(this.location, this.fitZoom);
}
diff --git a/src/components/map/ha-locations-editor.ts b/src/components/map/ha-locations-editor.ts
index 06547974fe..a12021c11f 100644
--- a/src/components/map/ha-locations-editor.ts
+++ b/src/components/map/ha-locations-editor.ts
@@ -90,8 +90,8 @@ export class HaLocationsEditor extends LitElement {
if (!marker) {
return;
}
- if ((marker as Circle).getBounds) {
- this._leafletMap.fitBounds((marker as Circle).getBounds());
+ if ("getBounds" in marker) {
+ this._leafletMap.fitBounds(marker.getBounds());
(marker as Circle).bringToFront();
} else {
const circle = this._circles[id];
@@ -296,8 +296,8 @@ export class HaLocationsEditor extends LitElement {
// @ts-ignore
(ev: MouseEvent) => this._markerClicked(ev)
)
- .addTo(this._leafletMap);
- marker.id = location.id;
+ .addTo(this._leafletMap!);
+ (marker as any).id = location.id;
this._locationMarkers![location.id] = marker;
}
diff --git a/src/components/media-player/ha-media-player-browse.ts b/src/components/media-player/ha-media-player-browse.ts
index 32146ec822..8c17267fc4 100644
--- a/src/components/media-player/ha-media-player-browse.ts
+++ b/src/components/media-player/ha-media-player-browse.ts
@@ -378,6 +378,7 @@ export class HaMediaPlayerBrowse extends LitElement {
: html`
${this.hass.localize("ui.components.media-browser.no_items")}
+
${currentItem.media_content_id ===
"media-source://media_source/local/."
? html`
${this.hass.localize(
@@ -398,7 +399,7 @@ export class HaMediaPlayerBrowse extends LitElement {
${this.hass.localize(
"ui.components.media-browser.local_media_files"
- )}.`
+ )}`
: ""}
`}
diff --git a/src/components/state-history-chart-timeline.js b/src/components/state-history-chart-timeline.js
index 3bbd6054ce..50c234f35f 100644
--- a/src/components/state-history-chart-timeline.js
+++ b/src/components/state-history-chart-timeline.js
@@ -159,7 +159,7 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
if (prevState !== null) {
dataRow.push([prevLastChanged, endTime, locState, prevState]);
}
- datasets.push({ data: dataRow });
+ datasets.push({ data: dataRow, entity_id: stateInfo.entity_id });
labels.push(entityDisplay);
});
@@ -173,12 +173,22 @@ class StateHistoryChartTimeline extends LocalizeMixin(PolymerElement) {
return [state, start, end];
};
+ const formatTooltipBeforeBody = (item, data) => {
+ if (!this.hass.userData || !this.hass.userData.showAdvanced || !item[0]) {
+ return "";
+ }
+ // Extract the entity ID from the dataset.
+ const values = data.datasets[item[0].datasetIndex];
+ return values.entity_id || "";
+ };
+
const chartOptions = {
type: "timeline",
options: {
tooltips: {
callbacks: {
label: formatTooltipLabel,
+ beforeBody: formatTooltipBeforeBody,
},
},
scales: {
diff --git a/src/components/user/ha-user-picker.ts b/src/components/user/ha-user-picker.ts
index ecd99f188f..6b3e865c05 100644
--- a/src/components/user/ha-user-picker.ts
+++ b/src/components/user/ha-user-picker.ts
@@ -24,10 +24,14 @@ class HaUserPicker extends LitElement {
@property() public label?: string;
- @property() public value?: string;
+ @property() public noUserLabel?: string;
+
+ @property() public value = "";
@property() public users?: User[];
+ @property({ type: Boolean }) public disabled = false;
+
private _sortedUsers = memoizeOne((users?: User[]) => {
if (!users) {
return [];
@@ -40,15 +44,19 @@ class HaUserPicker extends LitElement {
protected render(): TemplateResult {
return html`
-
+
- No user
+ ${this.noUserLabel ||
+ this.hass?.localize("ui.components.user-picker.no_user")}
${this._sortedUsers(this.users).map(
(user) => html`
@@ -67,10 +75,6 @@ class HaUserPicker extends LitElement {
`;
}
- private get _value() {
- return this.value || "";
- }
-
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
if (this.users === undefined) {
@@ -83,7 +87,7 @@ class HaUserPicker extends LitElement {
private _userChanged(ev) {
const newValue = ev.detail.item.dataset.userId;
- if (newValue !== this._value) {
+ if (newValue !== this.value) {
this.value = ev.detail.value;
setTimeout(() => {
fireEvent(this, "value-changed", { value: newValue });
@@ -111,3 +115,9 @@ class HaUserPicker extends LitElement {
}
customElements.define("ha-user-picker", HaUserPicker);
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-user-picker": HaUserPicker;
+ }
+}
diff --git a/src/components/user/ha-users-picker.ts b/src/components/user/ha-users-picker.ts
new file mode 100644
index 0000000000..d4fac81d48
--- /dev/null
+++ b/src/components/user/ha-users-picker.ts
@@ -0,0 +1,169 @@
+import {
+ css,
+ CSSResult,
+ customElement,
+ html,
+ LitElement,
+ property,
+ TemplateResult,
+} from "lit-element";
+import { fireEvent } from "../../common/dom/fire_event";
+import type { PolymerChangedEvent } from "../../polymer-types";
+import type { HomeAssistant } from "../../types";
+import { fetchUsers, User } from "../../data/user";
+import "./ha-user-picker";
+import { mdiClose } from "@mdi/js";
+import memoizeOne from "memoize-one";
+import { guard } from "lit-html/directives/guard";
+
+@customElement("ha-users-picker")
+class HaUsersPickerLight extends LitElement {
+ @property({ attribute: false }) public hass?: HomeAssistant;
+
+ @property() public value?: string[];
+
+ @property({ attribute: "picked-user-label" })
+ public pickedUserLabel?: string;
+
+ @property({ attribute: "pick-user-label" })
+ public pickUserLabel?: string;
+
+ @property({ attribute: false })
+ public users?: User[];
+
+ protected firstUpdated(changedProps) {
+ super.firstUpdated(changedProps);
+ if (this.users === undefined) {
+ fetchUsers(this.hass!).then((users) => {
+ this.users = users;
+ });
+ }
+ }
+
+ protected render(): TemplateResult {
+ if (!this.hass || !this.users) {
+ return html``;
+ }
+
+ const notSelectedUsers = this._notSelectedUsers(this.users, this.value);
+ return html`
+ ${guard([notSelectedUsers], () =>
+ this.value?.map(
+ (user_id, idx) => html`
+
+
+
+
+
+
+ `
+ )
+ )}
+
+ `;
+ }
+
+ private _notSelectedUsers = memoizeOne(
+ (users?: User[], currentUsers?: string[]) =>
+ currentUsers
+ ? users?.filter(
+ (user) => !user.system_generated && !currentUsers.includes(user.id)
+ )
+ : users?.filter((user) => !user.system_generated)
+ );
+
+ private _notSelectedUsersAndSelected = (
+ userId: string,
+ users?: User[],
+ notSelected?: User[]
+ ) => {
+ const selectedUser = users?.find((user) => user.id === userId);
+ if (selectedUser) {
+ return notSelected ? [...notSelected, selectedUser] : [selectedUser];
+ }
+ return notSelected;
+ };
+
+ private get _currentUsers() {
+ return this.value || [];
+ }
+
+ private async _updateUsers(users) {
+ this.value = users;
+ fireEvent(this, "value-changed", {
+ value: users,
+ });
+ }
+
+ private _userChanged(event: PolymerChangedEvent) {
+ event.stopPropagation();
+ const index = (event.currentTarget as any).index;
+ const newValue = event.detail.value;
+ const newUsers = [...this._currentUsers];
+ if (newValue === "") {
+ newUsers.splice(index, 1);
+ } else {
+ newUsers.splice(index, 1, newValue);
+ }
+ this._updateUsers(newUsers);
+ }
+
+ private async _addUser(event: PolymerChangedEvent) {
+ event.stopPropagation();
+ const toAdd = event.detail.value;
+ (event.currentTarget as any).value = "";
+ if (!toAdd) {
+ return;
+ }
+ const currentUsers = this._currentUsers;
+ if (currentUsers.includes(toAdd)) {
+ return;
+ }
+
+ this._updateUsers([...currentUsers, toAdd]);
+ }
+
+ private _removeUser(event) {
+ const userId = (event.currentTarget as any).userId;
+ this._updateUsers(this._currentUsers.filter((user) => user !== userId));
+ }
+
+ static get styles(): CSSResult {
+ return css`
+ :host {
+ display: block;
+ }
+ div {
+ display: flex;
+ align-items: center;
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-users-picker": HaUsersPickerLight;
+ }
+}
diff --git a/src/data/automation.ts b/src/data/automation.ts
index 4b06d4af63..bb35448575 100644
--- a/src/data/automation.ts
+++ b/src/data/automation.ts
@@ -109,10 +109,17 @@ export interface TemplateTrigger {
value_template: string;
}
+export interface ContextConstraint {
+ context_id?: string;
+ parent_id?: string;
+ user_id?: string | string[];
+}
+
export interface EventTrigger {
platform: "event";
event_type: string;
- event_data: any;
+ event_data?: any;
+ context?: ContextConstraint;
}
export type Trigger =
@@ -217,12 +224,12 @@ export const subscribeTrigger = (
hass: HomeAssistant,
onChange: (result: {
variables: {
- trigger: {};
+ trigger: Record;
};
context: Context;
}) => void,
trigger: Trigger | Trigger[],
- variables?: {}
+ variables?: Record
) =>
hass.connection.subscribeMessage(onChange, {
type: "subscribe_trigger",
@@ -233,7 +240,7 @@ export const subscribeTrigger = (
export const testCondition = (
hass: HomeAssistant,
condition: Condition | Condition[],
- variables?: {}
+ variables?: Record
) =>
hass.callWS<{ result: boolean }>({
type: "test_condition",
diff --git a/src/data/cached-history.ts b/src/data/cached-history.ts
index f1fee4ec98..6c97c20baa 100644
--- a/src/data/cached-history.ts
+++ b/src/data/cached-history.ts
@@ -10,7 +10,6 @@ import {
} from "./history";
export interface CacheConfig {
- refresh: number;
cacheKey: string;
hoursToShow: number;
}
diff --git a/src/data/collection.ts b/src/data/collection.ts
index a565d371f0..43931acd31 100644
--- a/src/data/collection.ts
+++ b/src/data/collection.ts
@@ -17,12 +17,12 @@ interface OptimisticCollection extends Collection {
*/
export const getOptimisticCollection = (
- saveCollection: (conn: Connection, data: StateType) => Promise,
+ saveCollection: (conn2: Connection, data: StateType) => Promise,
conn: Connection,
key: string,
- fetchCollection: (conn: Connection) => Promise,
+ fetchCollection: (conn2: Connection) => Promise,
subscribeUpdates?: (
- conn: Connection,
+ conn2: Connection,
store: Store
) => Promise
): OptimisticCollection => {
diff --git a/src/data/counter.ts b/src/data/counter.ts
new file mode 100644
index 0000000000..b01bf65869
--- /dev/null
+++ b/src/data/counter.ts
@@ -0,0 +1,51 @@
+import { HomeAssistant } from "../types";
+
+export interface Counter {
+ id: string;
+ name: string;
+ icon?: string;
+ initial?: number;
+ restore?: boolean;
+ minimum?: number;
+ maximum?: number;
+ step?: number;
+}
+
+export interface CounterMutableParams {
+ name: string;
+ icon: string;
+ initial: number;
+ restore: boolean;
+ minimum: number;
+ maximum: number;
+ step: number;
+}
+
+export const fetchCounter = (hass: HomeAssistant) =>
+ hass.callWS({ type: "counter/list" });
+
+export const createCounter = (
+ hass: HomeAssistant,
+ values: CounterMutableParams
+) =>
+ hass.callWS({
+ type: "counter/create",
+ ...values,
+ });
+
+export const updateCounter = (
+ hass: HomeAssistant,
+ id: string,
+ updates: Partial
+) =>
+ hass.callWS({
+ type: "counter/update",
+ counter_id: id,
+ ...updates,
+ });
+
+export const deleteCounter = (hass: HomeAssistant, id: string) =>
+ hass.callWS({
+ type: "counter/delete",
+ counter_id: id,
+ });
diff --git a/src/data/entity_registry.ts b/src/data/entity_registry.ts
index a5d897ca8b..4c5271e82d 100644
--- a/src/data/entity_registry.ts
+++ b/src/data/entity_registry.ts
@@ -15,7 +15,7 @@ export interface EntityRegistryEntry {
export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
unique_id: string;
- capabilities: object;
+ capabilities: Record;
original_name?: string;
original_icon?: string;
}
diff --git a/src/data/hassio/addon.ts b/src/data/hassio/addon.ts
index d500ad4311..902548f174 100644
--- a/src/data/hassio/addon.ts
+++ b/src/data/hassio/addon.ts
@@ -2,78 +2,71 @@ import { HomeAssistant } from "../../types";
import { hassioApiResultExtractor, HassioResponse } from "./common";
export interface HassioAddonInfo {
- name: string;
- slug: string;
- description: string;
- repository: "core" | "local" | string;
- version: string;
- state: "none" | "started" | "stopped";
- installed: string | undefined;
- detached: boolean;
+ advanced: boolean;
available: boolean;
build: boolean;
- advanced: boolean;
- url: string | null;
+ description: string;
+ detached: boolean;
icon: boolean;
+ installed: boolean;
logo: boolean;
+ name: string;
+ repository: "core" | "local" | string;
+ slug: string;
+ stage: "stable" | "experimental" | "deprecated";
+ state: "started" | "stopped" | null;
+ update_available: boolean;
+ url: string | null;
+ version_latest: string;
+ version: string;
}
export interface HassioAddonDetails extends HassioAddonInfo {
- name: string;
- slug: string;
- description: string;
- long_description: null | string;
- auto_update: boolean;
- url: null | string;
- detached: boolean;
- documentation: boolean;
- available: boolean;
- arch: "armhf" | "aarch64" | "i386" | "amd64";
- machine: any;
- homeassistant: string;
- version_latest: string;
- boot: "auto" | "manual";
- build: boolean;
- options: object;
- network: null | object;
- network_description: null | object;
- host_network: boolean;
- host_pid: boolean;
- host_ipc: boolean;
- host_dbus: boolean;
- privileged: any;
apparmor: "disable" | "default" | "profile";
- devices: string[];
- auto_uart: boolean;
- icon: boolean;
- logo: boolean;
- stage: "stable" | "experimental" | "deprecated";
- changelog: boolean;
- hassio_api: boolean;
- hassio_role: "default" | "homeassistant" | "manager" | "admin";
- startup: "initialize" | "system" | "services" | "application" | "once";
- homeassistant_api: boolean;
- auth_api: boolean;
- full_access: boolean;
- protected: boolean;
- rating: "1-6";
- stdin: boolean;
- webui: null | string;
- gpio: boolean;
- kernel_modules: boolean;
- devicetree: boolean;
- docker_api: boolean;
- audio: boolean;
+ arch: "armhf" | "aarch64" | "i386" | "amd64";
audio_input: null | string;
audio_output: null | string;
- services_role: string[];
+ audio: boolean;
+ auth_api: boolean;
+ auto_uart: boolean;
+ auto_update: boolean;
+ boot: "auto" | "manual";
+ changelog: boolean;
+ devices: string[];
+ devicetree: boolean;
discovery: string[];
- ip_address: string;
- ingress: boolean;
- ingress_panel: boolean;
+ docker_api: boolean;
+ documentation: boolean;
+ full_access: boolean;
+ gpio: boolean;
+ hassio_api: boolean;
+ hassio_role: "default" | "homeassistant" | "manager" | "admin";
+ homeassistant_api: boolean;
+ homeassistant: string;
+ host_dbus: boolean;
+ host_ipc: boolean;
+ host_network: boolean;
+ host_pid: boolean;
ingress_entry: null | string;
+ ingress_panel: boolean;
ingress_url: null | string;
+ ingress: boolean;
+ ip_address: string;
+ kernel_modules: boolean;
+ long_description: null | string;
+ machine: any;
+ network_description: null | Record;
+ network: null | Record;
+ options: Record;
+ privileged: any;
+ protected: boolean;
+ rating: "1-6";
+ services_role: string[];
+ slug: string;
+ startup: "initialize" | "system" | "services" | "application" | "once";
+ stdin: boolean;
watchdog: null | boolean;
+ webui: null | string;
}
export interface HassioAddonsInfo {
@@ -96,11 +89,11 @@ export interface HassioAddonRepository {
export interface HassioAddonSetOptionParams {
audio_input?: string | null;
audio_output?: string | null;
- options?: object | null;
+ options?: Record | null;
boot?: "auto" | "manual";
auto_update?: boolean;
ingress_panel?: boolean;
- network?: object | null;
+ network?: Record | null;
watchdog?: boolean;
}
diff --git a/src/data/hassio/common.ts b/src/data/hassio/common.ts
index 103f61c35a..57a0afa59a 100644
--- a/src/data/hassio/common.ts
+++ b/src/data/hassio/common.ts
@@ -22,8 +22,8 @@ export const hassioApiResultExtractor = (response: HassioResponse) =>
export const extractApiErrorMessage = (error: any): string => {
return typeof error === "object"
? typeof error.body === "object"
- ? error.body.message || "Unknown error, see logs"
- : error.body || "Unknown error, see logs"
+ ? error.body.message || "Unknown error, see supervisor logs"
+ : error.body || error.message || "Unknown error, see supervisor logs"
: error;
};
diff --git a/src/data/hassio/docker.ts b/src/data/hassio/docker.ts
new file mode 100644
index 0000000000..4bc9a194c5
--- /dev/null
+++ b/src/data/hassio/docker.ts
@@ -0,0 +1,36 @@
+import { HomeAssistant } from "../../types";
+import { hassioApiResultExtractor, HassioResponse } from "./common";
+
+interface HassioDockerRegistries {
+ [key: string]: { username: string; password?: string };
+}
+
+export const fetchHassioDockerRegistries = async (hass: HomeAssistant) => {
+ return hassioApiResultExtractor(
+ await hass.callApi>(
+ "GET",
+ "hassio/docker/registries"
+ )
+ );
+};
+
+export const addHassioDockerRegistry = async (
+ hass: HomeAssistant,
+ data: HassioDockerRegistries
+) => {
+ await hass.callApi>(
+ "POST",
+ "hassio/docker/registries",
+ data
+ );
+};
+
+export const removeHassioDockerRegistry = async (
+ hass: HomeAssistant,
+ registry: string
+) => {
+ await hass.callApi>(
+ "DELETE",
+ `hassio/docker/registries/${registry}`
+ );
+};
diff --git a/src/data/hassio/hardware.ts b/src/data/hassio/hardware.ts
index 345bf0d1ba..863a16d70d 100644
--- a/src/data/hassio/hardware.ts
+++ b/src/data/hassio/hardware.ts
@@ -18,7 +18,7 @@ export interface HassioHardwareInfo {
input: string[];
disk: string[];
gpio: string[];
- audio: object;
+ audio: Record;
}
export const fetchHassioHardwareAudio = async (hass: HomeAssistant) => {
diff --git a/src/data/hassio/host.ts b/src/data/hassio/host.ts
index a8db6dbc71..ebdc606832 100644
--- a/src/data/hassio/host.ts
+++ b/src/data/hassio/host.ts
@@ -1,14 +1,25 @@
import { HomeAssistant } from "../../types";
import { hassioApiResultExtractor, HassioResponse } from "./common";
-export type HassioHostInfo = any;
+export type HassioHostInfo = {
+ chassis: string;
+ cpe: string;
+ deployment: string;
+ disk_free: number;
+ disk_total: number;
+ disk_used: number;
+ features: string[];
+ hostname: string;
+ kernel: string;
+ operating_system: string;
+};
export interface HassioHassOSInfo {
- version: string;
- version_cli: string;
+ board: string;
+ boot: string;
+ update_available: boolean;
version_latest: string;
- version_cli_latest: string;
- board: "ova" | "rpi";
+ version: string;
}
export const fetchHassioHostInfo = async (hass: HomeAssistant) => {
diff --git a/src/data/hassio/resolution.ts b/src/data/hassio/resolution.ts
new file mode 100644
index 0000000000..8f85943de3
--- /dev/null
+++ b/src/data/hassio/resolution.ts
@@ -0,0 +1,15 @@
+import { HomeAssistant } from "../../types";
+import { hassioApiResultExtractor, HassioResponse } from "./common";
+
+export interface HassioResolution {
+ unsupported: string[];
+}
+
+export const fetchHassioResolution = async (hass: HomeAssistant) => {
+ return hassioApiResultExtractor(
+ await hass.callApi>(
+ "GET",
+ "hassio/resolution/info"
+ )
+ );
+};
diff --git a/src/data/hassio/supervisor.ts b/src/data/hassio/supervisor.ts
index 992425b8bc..9d2845f776 100644
--- a/src/data/hassio/supervisor.ts
+++ b/src/data/hassio/supervisor.ts
@@ -1,19 +1,56 @@
import { HomeAssistant, PanelInfo } from "../../types";
+import { HassioAddonInfo, HassioAddonRepository } from "./addon";
import { hassioApiResultExtractor, HassioResponse } from "./common";
-export type HassioHomeAssistantInfo = any;
-export type HassioSupervisorInfo = any;
+export type HassioHomeAssistantInfo = {
+ arch: string;
+ audio_input: string | null;
+ audio_output: string | null;
+ boot: boolean;
+ image: string;
+ ip_address: string;
+ machine: string;
+ port: number;
+ ssl: boolean;
+ update_available: boolean;
+ version_latest: string;
+ version: string;
+ wait_boot: number;
+ watchdog: boolean;
+};
+
+export type HassioSupervisorInfo = {
+ addons: HassioAddonInfo[];
+ addons_repositories: HassioAddonRepository[];
+ arch: string;
+ channel: string;
+ debug: boolean;
+ debug_block: boolean;
+ diagnostics: boolean | null;
+ healthy: boolean;
+ ip_address: string;
+ logging: string;
+ supported: boolean;
+ timezone: string;
+ update_available: boolean;
+ version: string;
+ version_latest: string;
+ wait_boot: number;
+};
export type HassioInfo = {
arch: string;
channel: string;
docker: string;
- hassos?: string;
+ features: string[];
+ hassos: null;
homeassistant: string;
hostname: string;
logging: string;
- maching: string;
+ machine: string;
+ operating_system: string;
supervisor: string;
+ supported: boolean;
supported_arch: string[];
timezone: string;
};
diff --git a/src/data/history.ts b/src/data/history.ts
index c5df270404..16bb07c782 100644
--- a/src/data/history.ts
+++ b/src/data/history.ts
@@ -85,11 +85,14 @@ export const fetchRecent = (
export const fetchDate = (
hass: HomeAssistant,
startTime: Date,
- endTime: Date
+ endTime: Date,
+ entityId
): Promise => {
return hass.callApi(
"GET",
- `history/period/${startTime.toISOString()}?end_time=${endTime.toISOString()}&minimal_response`
+ `history/period/${startTime.toISOString()}?end_time=${endTime.toISOString()}&minimal_response${
+ entityId ? `&filter_entity_id=${entityId}` : ``
+ }`
);
};
diff --git a/src/data/logbook.ts b/src/data/logbook.ts
index 74d2231f4a..d5a916c400 100644
--- a/src/data/logbook.ts
+++ b/src/data/logbook.ts
@@ -6,6 +6,7 @@ import { HomeAssistant } from "../types";
import { UNAVAILABLE_STATES } from "./entity";
const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages";
+export const CONTINUOUS_DOMAINS = ["proximity", "sensor"];
export interface LogbookEntry {
when: string;
diff --git a/src/data/lovelace.ts b/src/data/lovelace.ts
index d0a19bf20c..608dbb9fb7 100644
--- a/src/data/lovelace.ts
+++ b/src/data/lovelace.ts
@@ -33,7 +33,7 @@ export interface LovelaceResource {
}
export interface LovelaceResourcesMutableParams {
- res_type: "css" | "js" | "module" | "html";
+ res_type: LovelaceResource["type"];
url: string;
}
diff --git a/src/data/ozw.ts b/src/data/ozw.ts
index 80ad9882b7..0e5d73565a 100644
--- a/src/data/ozw.ts
+++ b/src/data/ozw.ts
@@ -63,6 +63,16 @@ export interface OZWNetworkStatistics {
retries: number;
}
+export interface OZWDeviceConfig {
+ label: string;
+ type: string;
+ value: string | number;
+ parameter: number;
+ min: number;
+ max: number;
+ help: string;
+}
+
export const nodeQueryStages = [
"ProtocolInfo",
"Probe",
@@ -180,6 +190,17 @@ export const fetchOZWNodeMetadata = (
node_id: node_id,
});
+export const fetchOZWNodeConfig = (
+ hass: HomeAssistant,
+ ozw_instance: number,
+ node_id: number
+): Promise =>
+ hass.callWS({
+ type: "ozw/get_config_parameters",
+ ozw_instance: ozw_instance,
+ node_id: node_id,
+ });
+
export const refreshNodeInfo = (
hass: HomeAssistant,
ozw_instance: number,
diff --git a/src/data/panel_custom.ts b/src/data/panel_custom.ts
index ce09a24408..0b6472c6d1 100644
--- a/src/data/panel_custom.ts
+++ b/src/data/panel_custom.ts
@@ -9,6 +9,6 @@ export interface CustomPanelConfig {
html_url?: string;
}
-export type CustomPanelInfo = PanelInfo<
+export type CustomPanelInfo> = PanelInfo<
T & { _panel_custom: CustomPanelConfig }
>;
diff --git a/src/data/script.ts b/src/data/script.ts
index 240472f743..3becd56e0a 100644
--- a/src/data/script.ts
+++ b/src/data/script.ts
@@ -105,7 +105,7 @@ export type Action =
export const triggerScript = (
hass: HomeAssistant,
entityId: string,
- variables?: {}
+ variables?: Record
) => hass.callService("script", computeObjectId(entityId), variables);
export const canExcecute = (state: ScriptEntity) => {
diff --git a/src/data/timer.ts b/src/data/timer.ts
index 8b54020a7d..a963d64ea3 100644
--- a/src/data/timer.ts
+++ b/src/data/timer.ts
@@ -2,6 +2,7 @@ import {
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
+import { HomeAssistant } from "../types";
export type TimerEntity = HassEntityBase & {
attributes: HassEntityAttributeBase & {
@@ -9,3 +10,48 @@ export type TimerEntity = HassEntityBase & {
remaining: string;
};
};
+
+export interface DurationDict {
+ hours?: number | string;
+ minutes?: number | string;
+ seconds?: number | string;
+}
+
+export interface Timer {
+ id: string;
+ name: string;
+ icon?: string;
+ duration?: string | number | DurationDict;
+}
+
+export interface TimerMutableParams {
+ name: string;
+ icon: string;
+ duration: string | number | DurationDict;
+}
+
+export const fetchTimer = (hass: HomeAssistant) =>
+ hass.callWS({ type: "timer/list" });
+
+export const createTimer = (hass: HomeAssistant, values: TimerMutableParams) =>
+ hass.callWS({
+ type: "timer/create",
+ ...values,
+ });
+
+export const updateTimer = (
+ hass: HomeAssistant,
+ id: string,
+ updates: Partial
+) =>
+ hass.callWS({
+ type: "timer/update",
+ timer_id: id,
+ ...updates,
+ });
+
+export const deleteTimer = (hass: HomeAssistant, id: string) =>
+ hass.callWS({
+ type: "timer/delete",
+ timer_id: id,
+ });
diff --git a/src/data/translation.ts b/src/data/translation.ts
index a713ccd256..8a11515458 100644
--- a/src/data/translation.ts
+++ b/src/data/translation.ts
@@ -33,8 +33,8 @@ export const getHassTranslations = async (
category: TranslationCategory,
integration?: string,
config_flow?: boolean
-): Promise<{}> => {
- const result = await hass.callWS<{ resources: {} }>({
+): Promise> => {
+ const result = await hass.callWS<{ resources: Record }>({
type: "frontend/get_translations",
language,
category,
@@ -47,8 +47,8 @@ export const getHassTranslations = async (
export const getHassTranslationsPre109 = async (
hass: HomeAssistant,
language: string
-): Promise<{}> => {
- const result = await hass.callWS<{ resources: {} }>({
+): Promise> => {
+ const result = await hass.callWS<{ resources: Record }>({
type: "frontend/get_translations",
language,
});
diff --git a/src/data/weather.ts b/src/data/weather.ts
index 8c0f7cb5f7..16333d2d1e 100644
--- a/src/data/weather.ts
+++ b/src/data/weather.ts
@@ -1,6 +1,14 @@
-import { SVGTemplateResult, svg, html, TemplateResult, css } from "lit-element";
+import {
+ mdiGauge,
+ mdiWaterPercent,
+ mdiWeatherFog,
+ mdiWeatherRainy,
+ mdiWeatherWindy,
+} from "@mdi/js";
+import { css, html, svg, SVGTemplateResult, TemplateResult } from "lit-element";
import { styleMap } from "lit-html/directives/style-map";
-
+import "../components/ha-icon";
+import "../components/ha-svg-icon";
import type { HomeAssistant, WeatherEntity } from "../types";
import { roundWithOneDecimal } from "../util/calculate";
@@ -25,6 +33,15 @@ export const weatherIcons = {
exceptional: "hass:alert-circle-outline",
};
+export const weatherAttrIcons = {
+ humidity: mdiWaterPercent,
+ wind_bearing: mdiWeatherWindy,
+ wind_speed: mdiWeatherWindy,
+ pressure: mdiGauge,
+ visibility: mdiWeatherFog,
+ precipitation: mdiWeatherRainy,
+};
+
const cloudyStates = new Set([
"partlycloudy",
"cloudy",
@@ -48,7 +65,7 @@ const snowyStates = new Set(["snowy", "snowy-rainy"]);
const lightningStates = new Set(["lightning", "lightning-rainy"]);
-export const cardinalDirections = [
+const cardinalDirections = [
"N",
"NNE",
"NE",
@@ -77,13 +94,29 @@ const getWindBearingText = (degree: string): string => {
return degree;
};
-export const getWindBearing = (bearing: string): string => {
+const getWindBearing = (bearing: string): string => {
if (bearing != null) {
return getWindBearingText(bearing);
}
return "";
};
+export const getWind = (
+ hass: HomeAssistant,
+ speed: string,
+ bearing: string
+): string => {
+ if (bearing !== null) {
+ const cardinalDirection = getWindBearing(bearing);
+ return `${speed} ${getWeatherUnit(hass!, "wind_speed")} (${
+ hass.localize(
+ `ui.card.weather.cardinal_direction.${cardinalDirection.toLowerCase()}`
+ ) || cardinalDirection
+ })`;
+ }
+ return `${speed} ${getWeatherUnit(hass!, "wind_speed")}`;
+};
+
export const getWeatherUnit = (
hass: HomeAssistant,
measure: string
@@ -94,6 +127,7 @@ export const getWeatherUnit = (
return lengthUnit === "km" ? "hPa" : "inHg";
case "wind_speed":
return `${lengthUnit}/h`;
+ case "visibility":
case "length":
return lengthUnit;
case "precipitation":
@@ -109,7 +143,7 @@ export const getWeatherUnit = (
export const getSecondaryWeatherAttribute = (
hass: HomeAssistant,
stateObj: WeatherEntity
-): string | undefined => {
+): TemplateResult | undefined => {
const extrema = getWeatherExtrema(hass, stateObj);
if (extrema) {
@@ -133,17 +167,22 @@ export const getSecondaryWeatherAttribute = (
return undefined;
}
- return `
- ${hass!.localize(
- `ui.card.weather.attributes.${attribute}`
- )} ${roundWithOneDecimal(value)} ${getWeatherUnit(hass!, attribute)}
+ const weatherAttrIcon = weatherAttrIcons[attribute];
+
+ return html`
+ ${weatherAttrIcon
+ ? html`
+
+ `
+ : hass!.localize(`ui.card.weather.attributes.${attribute}`)}
+ ${roundWithOneDecimal(value)} ${getWeatherUnit(hass!, attribute)}
`;
};
const getWeatherExtrema = (
hass: HomeAssistant,
stateObj: WeatherEntity
-): string | undefined => {
+): TemplateResult | undefined => {
if (!stateObj.attributes.forecast?.length) {
return undefined;
}
@@ -173,22 +212,18 @@ const getWeatherExtrema = (
const unit = getWeatherUnit(hass!, "temperature");
- return `
- ${
- tempHigh
- ? `
+ return html`
+ ${tempHigh
+ ? `
${tempHigh} ${unit}
`
- : ""
- }
+ : ""}
${tempLow && tempHigh ? " / " : ""}
- ${
- tempLow
- ? `
+ ${tempLow
+ ? `
${tempLow} ${unit}
`
- : ""
- }
+ : ""}
`;
};
@@ -210,7 +245,7 @@ export const weatherSVGStyles = css`
}
`;
-export const getWeatherStateSVG = (
+const getWeatherStateSVG = (
state: string,
nightTime?: boolean
): SVGTemplateResult => {
diff --git a/src/data/ws-templates.ts b/src/data/ws-templates.ts
index 0cac4d4606..6d65e6ccbb 100644
--- a/src/data/ws-templates.ts
+++ b/src/data/ws-templates.ts
@@ -9,6 +9,7 @@ interface TemplateListeners {
all: boolean;
domains: string[];
entities: string[];
+ time: boolean;
}
export const subscribeRenderTemplate = (
@@ -17,7 +18,7 @@ export const subscribeRenderTemplate = (
params: {
template: string;
entity_ids?: string | string[];
- variables?: object;
+ variables?: Record;
timeout?: number;
}
): Promise => {
diff --git a/src/dialogs/domain-toggler/dialog-domain-toggler.ts b/src/dialogs/domain-toggler/dialog-domain-toggler.ts
index 4fb23c79de..4540edfcc7 100644
--- a/src/dialogs/domain-toggler/dialog-domain-toggler.ts
+++ b/src/dialogs/domain-toggler/dialog-domain-toggler.ts
@@ -19,7 +19,8 @@ import { HassDialog } from "../make-dialog-manager";
import { HaDomainTogglerDialogParams } from "./show-dialog-domain-toggler";
@customElement("dialog-domain-toggler")
-class DomainTogglerDialog extends LitElement implements HassDialog {
+class DomainTogglerDialog extends LitElement
+ implements HassDialog {
public hass!: HomeAssistant;
@internalProperty() private _params?: HaDomainTogglerDialogParams;
diff --git a/src/dialogs/generic/dialog-box.ts b/src/dialogs/generic/dialog-box.ts
index 5df7cb4f1e..e905f0c3ff 100644
--- a/src/dialogs/generic/dialog-box.ts
+++ b/src/dialogs/generic/dialog-box.ts
@@ -70,6 +70,7 @@ class DialogBox extends LitElement {
${this._params.text}
@@ -180,6 +181,9 @@ class DialogBox extends LitElement {
/* Place above other dialogs */
--dialog-z-index: 104;
}
+ .warning {
+ color: var(--warning-color);
+ }
`,
];
}
diff --git a/src/dialogs/generic/show-dialog-box.ts b/src/dialogs/generic/show-dialog-box.ts
index 86c5cfee8b..5e965bab2f 100644
--- a/src/dialogs/generic/show-dialog-box.ts
+++ b/src/dialogs/generic/show-dialog-box.ts
@@ -5,6 +5,7 @@ interface BaseDialogParams {
confirmText?: string;
text?: string | TemplateResult;
title?: string;
+ warning?: boolean;
}
export interface AlertDialogParams extends BaseDialogParams {
diff --git a/src/dialogs/image-cropper-dialog/image-cropper-dialog.ts b/src/dialogs/image-cropper-dialog/image-cropper-dialog.ts
index ed9e3a05aa..c2ab0ca148 100644
--- a/src/dialogs/image-cropper-dialog/image-cropper-dialog.ts
+++ b/src/dialogs/image-cropper-dialog/image-cropper-dialog.ts
@@ -29,7 +29,7 @@ export class HaImagecropperDialog extends LitElement {
@internalProperty() private _open = false;
- @query("img") private _image!: HTMLImageElement;
+ @query("img", true) private _image!: HTMLImageElement;
private _cropper?: Cropper;
diff --git a/src/dialogs/more-info/controls/more-info-group.js b/src/dialogs/more-info/controls/more-info-group.js
deleted file mode 100644
index fabab470cc..0000000000
--- a/src/dialogs/more-info/controls/more-info-group.js
+++ /dev/null
@@ -1,111 +0,0 @@
-import { dom } from "@polymer/polymer/lib/legacy/polymer.dom";
-import { html } from "@polymer/polymer/lib/utils/html-tag";
-/* eslint-plugin-disable lit */
-import { PolymerElement } from "@polymer/polymer/polymer-element";
-import dynamicContentUpdater from "../../../common/dom/dynamic_content_updater";
-import { computeStateDomain } from "../../../common/entity/compute_state_domain";
-import "../../../state-summary/state-card-content";
-
-class MoreInfoGroup extends PolymerElement {
- static get template() {
- return html`
-
-
-
-
-
-
-
-
- `;
- }
-
- static get properties() {
- return {
- hass: {
- type: Object,
- },
-
- stateObj: {
- type: Object,
- },
-
- states: {
- type: Array,
- computed: "computeStates(stateObj, hass)",
- },
- };
- }
-
- static get observers() {
- return ["statesChanged(stateObj, states)"];
- }
-
- computeStates(stateObj, hass) {
- const states = [];
- const entIds = stateObj.attributes.entity_id || [];
-
- for (let i = 0; i < entIds.length; i++) {
- const state = hass.states[entIds[i]];
-
- if (state) {
- states.push(state);
- }
- }
-
- return states;
- }
-
- statesChanged(stateObj, states) {
- let groupDomainStateObj = false;
- let groupDomain = false;
-
- if (states && states.length > 0) {
- const baseStateObj = states.find((s) => s.state === "on") || states[0];
- groupDomain = computeStateDomain(baseStateObj);
-
- // Groups need to be filtered out or we'll show content of
- // first child above the children of the current group
- if (groupDomain !== "group") {
- groupDomainStateObj = {
- ...baseStateObj,
- entity_id: stateObj.entity_id,
- attributes: { ...baseStateObj.attributes },
- };
-
- for (let i = 0; i < states.length; i++) {
- if (groupDomain !== computeStateDomain(states[i])) {
- groupDomainStateObj = false;
- break;
- }
- }
- }
- }
-
- if (!groupDomainStateObj) {
- const el = dom(this.$.groupedControlDetails);
- if (el.lastChild) {
- el.removeChild(el.lastChild);
- }
- } else {
- dynamicContentUpdater(
- this.$.groupedControlDetails,
- "MORE-INFO-" + groupDomain.toUpperCase(),
- { stateObj: groupDomainStateObj, hass: this.hass }
- );
- }
- }
-}
-
-customElements.define("more-info-group", MoreInfoGroup);
diff --git a/src/dialogs/more-info/controls/more-info-group.ts b/src/dialogs/more-info/controls/more-info-group.ts
new file mode 100644
index 0000000000..788d1a2f6b
--- /dev/null
+++ b/src/dialogs/more-info/controls/more-info-group.ts
@@ -0,0 +1,111 @@
+import { HassEntity } from "home-assistant-js-websocket";
+import {
+ LitElement,
+ property,
+ CSSResult,
+ css,
+ internalProperty,
+ PropertyValues,
+} from "lit-element";
+import { html, TemplateResult } from "lit-html";
+import { dynamicElement } from "../../../common/dom/dynamic-element-directive";
+import { computeStateDomain } from "../../../common/entity/compute_state_domain";
+import "../../../state-summary/state-card-content";
+import { GroupEntity, HomeAssistant } from "../../../types";
+import {
+ importMoreInfoControl,
+ domainMoreInfoType,
+} from "../state_more_info_control";
+
+class MoreInfoGroup extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property() public stateObj?: GroupEntity;
+
+ @internalProperty() private _groupDomainStateObj?: HassEntity;
+
+ @internalProperty() private _moreInfoType?: string;
+
+ protected updated(changedProperties: PropertyValues) {
+ if (
+ !this.hass ||
+ !this.stateObj ||
+ (!changedProperties.has("hass") && !changedProperties.has("stateObj"))
+ ) {
+ return;
+ }
+
+ const states = this.stateObj.attributes.entity_id
+ .map((entity_id) => this.hass.states[entity_id])
+ .filter((state) => state);
+
+ if (!states.length) {
+ this._groupDomainStateObj = undefined;
+ this._moreInfoType = undefined;
+ return;
+ }
+
+ const baseStateObj = states.find((s) => s.state === "on") || states[0];
+ const groupDomain = computeStateDomain(baseStateObj);
+
+ // Groups need to be filtered out or we'll show content of
+ // first child above the children of the current group
+ if (
+ groupDomain !== "group" &&
+ states.every((state) => groupDomain === computeStateDomain(state))
+ ) {
+ this._groupDomainStateObj = {
+ ...baseStateObj,
+ entity_id: this.stateObj.entity_id,
+ attributes: { ...baseStateObj.attributes },
+ };
+ const type = domainMoreInfoType(groupDomain);
+ importMoreInfoControl(type);
+ this._moreInfoType = type === "hidden" ? undefined : `more-info-${type}`;
+ } else {
+ this._groupDomainStateObj = undefined;
+ this._moreInfoType = undefined;
+ }
+ }
+
+ protected render(): TemplateResult {
+ if (!this.hass || !this.stateObj) {
+ return html``;
+ }
+ return html`${this._moreInfoType
+ ? dynamicElement(this._moreInfoType, {
+ hass: this.hass,
+ stateObj: this._groupDomainStateObj,
+ })
+ : ""}
+ ${this.stateObj.attributes.entity_id.map((entity_id) => {
+ const state = this.hass!.states[entity_id];
+ if (!state) {
+ return "";
+ }
+ return html`
+
+ `;
+ })}`;
+ }
+
+ static get styles(): CSSResult {
+ return css`
+ state-card-content {
+ display: block;
+ margin-top: 8px;
+ }
+ `;
+ }
+}
+
+customElements.define("more-info-group", MoreInfoGroup);
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "more-info-group": MoreInfoGroup;
+ }
+}
diff --git a/src/dialogs/more-info/controls/more-info-light.ts b/src/dialogs/more-info/controls/more-info-light.ts
index 7ce5896ad6..a0ae76b03b 100644
--- a/src/dialogs/more-info/controls/more-info-light.ts
+++ b/src/dialogs/more-info/controls/more-info-light.ts
@@ -67,9 +67,10 @@ class MoreInfoLight extends LitElement {
caption=${this.hass.localize("ui.card.light.brightness")}
icon="hass:brightness-5"
min="1"
- max="255"
+ max="100"
value=${this._brightnessSliderValue}
@change=${this._brightnessSliderChanged}
+ pin
>
`
: ""}
@@ -87,6 +88,7 @@ class MoreInfoLight extends LitElement {
.max=${this.stateObj.attributes.max_mireds}
.value=${this._ctSliderValue}
@change=${this._ctSliderChanged}
+ pin
>
`
: ""}
@@ -98,6 +100,7 @@ class MoreInfoLight extends LitElement {
max="255"
.value=${this._wvSliderValue}
@change=${this._wvSliderChanged}
+ pin
>
`
: ""}
@@ -155,16 +158,22 @@ class MoreInfoLight extends LitElement {
protected updated(changedProps: PropertyValues): void {
const stateObj = this.stateObj! as LightEntity;
- if (changedProps.has("stateObj") && stateObj.state === "on") {
- this._brightnessSliderValue = stateObj.attributes.brightness;
- this._ctSliderValue = stateObj.attributes.color_temp;
- this._wvSliderValue = stateObj.attributes.white_value;
+ if (changedProps.has("stateObj")) {
+ if (stateObj.state === "on") {
+ this._brightnessSliderValue = Math.round(
+ (stateObj.attributes.brightness * 100) / 255
+ );
+ this._ctSliderValue = stateObj.attributes.color_temp;
+ this._wvSliderValue = stateObj.attributes.white_value;
- if (stateObj.attributes.hs_color) {
- this._colorPickerColor = {
- h: stateObj.attributes.hs_color[0],
- s: stateObj.attributes.hs_color[1] / 100,
- };
+ if (stateObj.attributes.hs_color) {
+ this._colorPickerColor = {
+ h: stateObj.attributes.hs_color[0],
+ s: stateObj.attributes.hs_color[1] / 100,
+ };
+ }
+ } else {
+ this._brightnessSliderValue = 0;
}
}
}
@@ -191,7 +200,7 @@ class MoreInfoLight extends LitElement {
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj!.entity_id,
- brightness: bri,
+ brightness_pct: bri,
});
}
@@ -250,15 +259,10 @@ class MoreInfoLight extends LitElement {
align-items: center;
}
- .content.is-on {
- margin-top: -16px;
- }
-
.content > * {
width: 100%;
max-height: 84px;
overflow: hidden;
- padding-top: 16px;
}
.color_temp {
diff --git a/src/dialogs/more-info/controls/more-info-media_player.ts b/src/dialogs/more-info/controls/more-info-media_player.ts
index 85ac9db862..1e47d7f943 100644
--- a/src/dialogs/more-info/controls/more-info-media_player.ts
+++ b/src/dialogs/more-info/controls/more-info-media_player.ts
@@ -104,18 +104,7 @@ class MoreInfoMediaPlayer extends LitElement {
>
`
: ""}
- ${supportsFeature(stateObj, SUPPORT_VOLUME_SET)
- ? html`
-
- `
- : supportsFeature(stateObj, SUPPORT_VOLUME_BUTTONS)
+ ${supportsFeature(stateObj, SUPPORT_VOLUME_BUTTONS)
? html`
`
: ""}
+ ${supportsFeature(stateObj, SUPPORT_VOLUME_SET)
+ ? html`
+
+ `
+ : ""}
`
: ""}
@@ -196,8 +197,8 @@ class MoreInfoMediaPlayer extends LitElement {
)}
@keydown=${this._ttsCheckForEnter}
>
-
diff --git a/src/dialogs/more-info/controls/more-info-weather.ts b/src/dialogs/more-info/controls/more-info-weather.ts
index 8d7a4ca5f3..7e962071a5 100644
--- a/src/dialogs/more-info/controls/more-info-weather.ts
+++ b/src/dialogs/more-info/controls/more-info-weather.ts
@@ -1,3 +1,4 @@
+import "../../../components/ha-svg-icon";
import { HassEntity } from "home-assistant-js-websocket";
import {
css,
@@ -9,44 +10,47 @@ import {
} from "lit-element";
import { html, TemplateResult } from "lit-html";
import { HomeAssistant } from "../../../types";
-import "../../../components/ha-icon";
-const cardinalDirections = [
- "N",
- "NNE",
- "NE",
- "ENE",
- "E",
- "ESE",
- "SE",
- "SSE",
- "S",
- "SSW",
- "SW",
- "WSW",
- "W",
- "WNW",
- "NW",
- "NNW",
- "N",
-];
+import { getWind, getWeatherUnit } from "../../../data/weather";
+
+import {
+ mdiAlertCircleOutline,
+ mdiEye,
+ mdiGauge,
+ mdiThermometer,
+ mdiWaterPercent,
+ mdiWeatherCloudy,
+ mdiWeatherFog,
+ mdiWeatherHail,
+ mdiWeatherLightning,
+ mdiWeatherLightningRainy,
+ mdiWeatherNight,
+ mdiWeatherPartlyCloudy,
+ mdiWeatherPouring,
+ mdiWeatherRainy,
+ mdiWeatherSnowy,
+ mdiWeatherSnowyRainy,
+ mdiWeatherSunny,
+ mdiWeatherWindy,
+ mdiWeatherWindyVariant,
+} from "@mdi/js";
const weatherIcons = {
- "clear-night": "hass:weather-night",
- cloudy: "hass:weather-cloudy",
- exceptional: "hass:alert-circle-outline",
- fog: "hass:weather-fog",
- hail: "hass:weather-hail",
- lightning: "hass:weather-lightning",
- "lightning-rainy": "hass:weather-lightning-rainy",
- partlycloudy: "hass:weather-partly-cloudy",
- pouring: "hass:weather-pouring",
- rainy: "hass:weather-rainy",
- snowy: "hass:weather-snowy",
- "snowy-rainy": "hass:weather-snowy-rainy",
- sunny: "hass:weather-sunny",
- windy: "hass:weather-windy",
- "windy-variant": "hass:weather-windy-variant",
+ "clear-night": mdiWeatherNight,
+ cloudy: mdiWeatherCloudy,
+ exceptional: mdiAlertCircleOutline,
+ fog: mdiWeatherFog,
+ hail: mdiWeatherHail,
+ lightning: mdiWeatherLightning,
+ "lightning-rainy": mdiWeatherLightningRainy,
+ partlycloudy: mdiWeatherPartlyCloudy,
+ pouring: mdiWeatherPouring,
+ rainy: mdiWeatherRainy,
+ snowy: mdiWeatherSnowy,
+ "snowy-rainy": mdiWeatherSnowyRainy,
+ sunny: mdiWeatherSunny,
+ windy: mdiWeatherWindy,
+ "windy-variant": mdiWeatherWindyVariant,
};
@customElement("more-info-weather")
@@ -79,24 +83,25 @@ class MoreInfoWeather extends LitElement {
return html`
-
+
${this.hass.localize("ui.card.weather.attributes.temperature")}
- ${this.stateObj.attributes.temperature} ${this.getUnit("temperature")}
+ ${this.stateObj.attributes.temperature}
+ ${getWeatherUnit(this.hass, "temperature")}
${this._showValue(this.stateObj.attributes.pressure)
? html`
-
+
${this.hass.localize("ui.card.weather.attributes.air_pressure")}
${this.stateObj.attributes.pressure}
- ${this.getUnit("air_pressure")}
+ ${getWeatherUnit(this.hass, "air_pressure")}
`
@@ -104,7 +109,7 @@ class MoreInfoWeather extends LitElement {
${this._showValue(this.stateObj.attributes.humidity)
? html`
-
+
${this.hass.localize("ui.card.weather.attributes.humidity")}
@@ -115,12 +120,13 @@ class MoreInfoWeather extends LitElement {
${this._showValue(this.stateObj.attributes.wind_speed)
? html`
-
+
${this.hass.localize("ui.card.weather.attributes.wind_speed")}
- ${this.getWind(
+ ${getWind(
+ this.hass,
this.stateObj.attributes.wind_speed,
this.stateObj.attributes.wind_bearing
)}
@@ -131,12 +137,13 @@ class MoreInfoWeather extends LitElement {
${this._showValue(this.stateObj.attributes.visibility)
? html`
-
+
${this.hass.localize("ui.card.weather.attributes.visibility")}
- ${this.stateObj.attributes.visibility} ${this.getUnit("length")}
+ ${this.stateObj.attributes.visibility}
+ ${getWeatherUnit(this.hass, "length")}
`
@@ -151,9 +158,9 @@ class MoreInfoWeather extends LitElement {
${item.condition
? html`
-
+
`
: ""}
${!this._showValue(item.templow)
@@ -169,12 +176,14 @@ class MoreInfoWeather extends LitElement {
${this.computeDate(item.datetime)}
- ${item.templow} ${this.getUnit("temperature")}
+ ${item.templow}
+ ${getWeatherUnit(this.hass, "temperature")}
`
: ""}
- ${item.temperature} ${this.getUnit("temperature")}
+ ${item.temperature}
+ ${getWeatherUnit(this.hass, "temperature")}
`;
@@ -193,7 +202,7 @@ class MoreInfoWeather extends LitElement {
static get styles(): CSSResult {
return css`
- ha-icon {
+ ha-svg-icon {
color: var(--paper-item-icon-color);
}
.section {
@@ -247,41 +256,6 @@ class MoreInfoWeather extends LitElement {
});
}
- private getUnit(measure: string): string {
- const lengthUnit = this.hass.config.unit_system.length || "";
- switch (measure) {
- case "air_pressure":
- return lengthUnit === "km" ? "hPa" : "inHg";
- case "length":
- return lengthUnit;
- case "precipitation":
- return lengthUnit === "km" ? "mm" : "in";
- default:
- return this.hass.config.unit_system[measure] || "";
- }
- }
-
- private windBearingToText(degree: string): string {
- const degreenum = parseInt(degree, 10);
- if (isFinite(degreenum)) {
- // eslint-disable-next-line no-bitwise
- return cardinalDirections[(((degreenum + 11.25) / 22.5) | 0) % 16];
- }
- return degree;
- }
-
- private getWind(speed: string, bearing: string) {
- if (bearing != null) {
- const cardinalDirection = this.windBearingToText(bearing);
- return `${speed} ${this.getUnit("length")}/h (${
- this.hass.localize(
- `ui.card.weather.cardinal_direction.${cardinalDirection.toLowerCase()}`
- ) || cardinalDirection
- })`;
- }
- return `${speed} ${this.getUnit("length")}/h`;
- }
-
private _showValue(item: string): boolean {
return typeof item !== "undefined" && item !== null;
}
diff --git a/src/dialogs/more-info/ha-more-info-dialog.ts b/src/dialogs/more-info/ha-more-info-dialog.ts
index 9851cf2b5b..a0c3378af6 100644
--- a/src/dialogs/more-info/ha-more-info-dialog.ts
+++ b/src/dialogs/more-info/ha-more-info-dialog.ts
@@ -40,7 +40,14 @@ import "./ha-more-info-logbook";
import "./controls/more-info-default";
const DOMAINS_NO_INFO = ["camera", "configurator"];
+/**
+ * Entity domains that should be editable *if* they have an id present;
+ * {@see shouldShowEditIcon}.
+ * */
const EDITABLE_DOMAINS_WITH_ID = ["scene", "automation"];
+/**
+ * Entity Domains that should always be editable; {@see shouldShowEditIcon}.
+ * */
const EDITABLE_DOMAINS = ["script"];
export interface MoreInfoDialogParams {
@@ -63,20 +70,10 @@ export class MoreInfoDialog extends LitElement {
this._entityId = params.entityId;
if (!this._entityId) {
this.closeDialog();
- }
- this.large = false;
- }
-
- public closeDialog() {
- this._entityId = undefined;
- this._currTabIndex = 0;
- fireEvent(this, "dialog-closed", { dialog: this.localName });
- }
-
- protected updated(changedProperties) {
- if (!this.hass || !this._entityId || !changedProperties.has("_entityId")) {
return;
}
+ this.large = false;
+
const stateObj = this.hass.states[this._entityId];
if (!stateObj) {
return;
@@ -90,6 +87,26 @@ export class MoreInfoDialog extends LitElement {
}
}
+ public closeDialog() {
+ this._entityId = undefined;
+ this._currTabIndex = 0;
+ fireEvent(this, "dialog-closed", { dialog: this.localName });
+ }
+
+ protected shouldShowEditIcon(domain, stateObj): boolean {
+ if (EDITABLE_DOMAINS_WITH_ID.includes(domain) && stateObj.attributes.id) {
+ return true;
+ }
+ if (EDITABLE_DOMAINS.includes(domain)) {
+ return true;
+ }
+ if (domain === "person" && stateObj.attributes.editable !== "false") {
+ return true;
+ }
+
+ return false;
+ }
+
protected render() {
if (!this._entityId) {
return html``;
@@ -137,10 +154,7 @@ export class MoreInfoDialog extends LitElement {
`
: ""}
- ${this.hass.user!.is_admin &&
- ((EDITABLE_DOMAINS_WITH_ID.includes(domain) &&
- stateObj.attributes.id) ||
- EDITABLE_DOMAINS.includes(domain))
+ ${this.shouldShowEditIcon(domain, stateObj)
? html`
{
const domain = computeStateDomain(stateObj);
+ return domainMoreInfoType(domain);
+};
+
+export const domainMoreInfoType = (domain: string): string => {
if (DOMAINS_WITH_MORE_INFO.includes(domain)) {
return domain;
}
diff --git a/src/dialogs/notifications/notification-drawer.js b/src/dialogs/notifications/notification-drawer.js
index 927af2f3a9..0ad149e9f0 100644
--- a/src/dialogs/notifications/notification-drawer.js
+++ b/src/dialogs/notifications/notification-drawer.js
@@ -40,7 +40,7 @@ export class HuiNotificationDrawer extends EventsMixin(
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom);
- height: calc(100% - 65px);
+ height: calc(100% - 1px - var(--header-height));
box-sizing: border-box;
background-color: var(--primary-background-color);
color: var(--primary-text-color);
@@ -50,6 +50,11 @@ export class HuiNotificationDrawer extends EventsMixin(
padding: 0 16px 16px;
}
+ .notification-actions {
+ padding: 0 16px 16px;
+ text-align: center;
+ }
+
.empty {
padding: 16px;
text-align: center;
@@ -69,6 +74,13 @@ export class HuiNotificationDrawer extends EventsMixin(
+
+
+
+ [[localize('ui.notification_drawer.dismiss_all')]]
+
+
+
[[localize('ui.notification_drawer.empty')]]
@@ -88,6 +100,7 @@ export class HuiNotificationDrawer extends EventsMixin(
notifications: {
type: Array,
computed: "_computeNotifications(open, hass, _notificationsBackend)",
+ observer: "_notificationsChanged",
},
_notificationsBackend: {
type: Array,
@@ -111,10 +124,23 @@ export class HuiNotificationDrawer extends EventsMixin(
this.open = false;
}
+ _dismissAll() {
+ this.notifications.forEach((notification) => {
+ this.hass.callService("persistent_notification", "dismiss", {
+ notification_id: notification.notification_id,
+ });
+ });
+ this.open = false;
+ }
+
_empty(notifications) {
return notifications.length === 0;
}
+ _moreThanOne(notifications) {
+ return notifications.length > 1;
+ }
+
_openChanged(open) {
if (open) {
// Render closed then animate open
@@ -130,6 +156,17 @@ export class HuiNotificationDrawer extends EventsMixin(
}
}
+ _notificationsChanged(newNotifications, oldNotifications) {
+ // automatically close drawer when last notification has been dismissed
+ if (
+ this.open &&
+ oldNotifications.length > 0 &&
+ !newNotifications.length === 0
+ ) {
+ this.open = false;
+ }
+ }
+
_computeNotifications(open, hass, notificationsBackend) {
if (!open) {
return [];
@@ -139,7 +176,22 @@ export class HuiNotificationDrawer extends EventsMixin(
.filter((entityId) => computeDomain(entityId) === "configurator")
.map((entityId) => hass.states[entityId]);
- return notificationsBackend.concat(configuratorEntities);
+ const notifications = notificationsBackend.concat(configuratorEntities);
+
+ notifications.sort(function (n1, n2) {
+ const d1 = new Date(n1.created_at);
+ const d2 = new Date(n2.created_at);
+
+ if (d1 < d2) {
+ return 1;
+ }
+ if (d1 > d2) {
+ return -1;
+ }
+ return 0;
+ });
+
+ return notifications;
}
showDialog({ narrow }) {
diff --git a/src/dialogs/quick-bar/ha-quick-bar.ts b/src/dialogs/quick-bar/ha-quick-bar.ts
new file mode 100644
index 0000000000..ce8ace0a88
--- /dev/null
+++ b/src/dialogs/quick-bar/ha-quick-bar.ts
@@ -0,0 +1,376 @@
+import "@material/mwc-list/mwc-list";
+import type { List } from "@material/mwc-list/mwc-list";
+import { SingleSelectedEvent } from "@material/mwc-list/mwc-list-foundation";
+import "@material/mwc-list/mwc-list-item";
+import type { ListItem } from "@material/mwc-list/mwc-list-item";
+import { mdiConsoleLine } from "@mdi/js";
+import {
+ css,
+ customElement,
+ html,
+ internalProperty,
+ LitElement,
+ property,
+ PropertyValues,
+ query,
+} from "lit-element";
+import { ifDefined } from "lit-html/directives/if-defined";
+import { styleMap } from "lit-html/directives/style-map";
+import { scroll } from "lit-virtualizer";
+import memoizeOne from "memoize-one";
+import { componentsWithService } from "../../common/config/components_with_service";
+import { fireEvent } from "../../common/dom/fire_event";
+import { computeDomain } from "../../common/entity/compute_domain";
+import { computeStateName } from "../../common/entity/compute_state_name";
+import { domainIcon } from "../../common/entity/domain_icon";
+import "../../common/search/search-input";
+import { compare } from "../../common/string/compare";
+import {
+ fuzzyFilterSort,
+ ScorableTextItem,
+} from "../../common/string/filter/sequence-matching";
+import { debounce } from "../../common/util/debounce";
+import "../../components/ha-circular-progress";
+import "../../components/ha-dialog";
+import "../../components/ha-header-bar";
+import { domainToName } from "../../data/integration";
+import { haStyleDialog } from "../../resources/styles";
+import { HomeAssistant } from "../../types";
+import { QuickBarParams } from "./show-dialog-quick-bar";
+
+interface QuickBarItem extends ScorableTextItem {
+ icon: string;
+ action(data?: any): void;
+}
+
+@customElement("ha-quick-bar")
+export class QuickBar extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @internalProperty() private _commandItems: QuickBarItem[] = [];
+
+ @internalProperty() private _entityItems: QuickBarItem[] = [];
+
+ @internalProperty() private _items?: QuickBarItem[] = [];
+
+ @internalProperty() private _filter = "";
+
+ @internalProperty() private _opened = false;
+
+ @internalProperty() private _commandMode = false;
+
+ @internalProperty() private _commandTriggered = -1;
+
+ @internalProperty() private _done = false;
+
+ @query("search-input", false) private _filterInputField?: HTMLElement;
+
+ private _focusSet = false;
+
+ public async showDialog(params: QuickBarParams) {
+ this._commandMode = params.commandMode || this._toggleIfAlreadyOpened();
+ this._commandItems = this._generateCommandItems();
+ this._entityItems = this._generateEntityItems();
+ this._opened = true;
+ }
+
+ public closeDialog() {
+ this._opened = false;
+ this._done = false;
+ this._focusSet = false;
+ this._filter = "";
+ this._commandTriggered = -1;
+ this._items = [];
+ fireEvent(this, "dialog-closed", { dialog: this.localName });
+ }
+
+ protected updated(changedProperties: PropertyValues) {
+ if (
+ this._opened &&
+ (changedProperties.has("_filter") ||
+ changedProperties.has("_commandMode"))
+ ) {
+ this._setFilteredItems();
+ }
+ }
+
+ protected render() {
+ if (!this._opened) {
+ return html``;
+ }
+
+ return html`
+
+ ${this._filter}` : this._filter}
+ @keydown=${this._handleInputKeyDown}
+ @focus=${this._setFocusFirstListItem}
+ >
+ ${this._commandMode
+ ? html` `
+ : ""}
+
+ ${!this._items
+ ? html` `
+ : html`
+ ${scroll({
+ items: this._items,
+ renderItem: (item: QuickBarItem, index?: number) =>
+ this._renderItem(item, index),
+ })}
+ `}
+
+ `;
+ }
+
+ private _handleOpened() {
+ this._setFilteredItems();
+ this.updateComplete.then(() => {
+ this._done = true;
+ });
+ }
+
+ private async _handleRangeChanged(e) {
+ if (this._focusSet) {
+ return;
+ }
+ if (e.firstVisible > -1) {
+ this._focusSet = true;
+ await this.updateComplete;
+ this._setFocusFirstListItem();
+ }
+ }
+
+ private _renderItem(item: QuickBarItem, index?: number) {
+ return html`
+
+
+ ${item.text}
+ ${item.altText
+ ? html`
+ ${item.altText}
+ `
+ : null}
+ ${this._commandTriggered === index
+ ? html` `
+ : null}
+
+ `;
+ }
+
+ private async processItemAndCloseDialog(item: QuickBarItem, index: number) {
+ this._commandTriggered = index;
+
+ await item.action();
+ this.closeDialog();
+ }
+
+ private _handleSelected(ev: SingleSelectedEvent) {
+ const index = ev.detail.index;
+ const item = ((ev.target as List).items[index] as any).item;
+ this.processItemAndCloseDialog(item, index);
+ }
+
+ private _handleInputKeyDown(ev: KeyboardEvent) {
+ if (ev.code === "Enter") {
+ if (!this._items?.length) {
+ return;
+ }
+
+ this.processItemAndCloseDialog(this._items[0], 0);
+ } else if (ev.code === "ArrowDown") {
+ ev.preventDefault();
+ this._getItemAtIndex(0)?.focus();
+ this._getItemAtIndex(1)?.focus();
+ }
+ }
+
+ private _getItemAtIndex(index: number): ListItem | null {
+ return this.renderRoot.querySelector(`mwc-list-item[index="${index}"]`);
+ }
+
+ private _handleSearchChange(ev: CustomEvent): void {
+ const newFilter = ev.detail.value;
+ const oldCommandMode = this._commandMode;
+
+ if (newFilter.startsWith(">")) {
+ this._commandMode = true;
+ this._debouncedSetFilter(newFilter.substring(1));
+ } else {
+ this._commandMode = false;
+ this._debouncedSetFilter(newFilter);
+ }
+
+ if (oldCommandMode !== this._commandMode) {
+ this._items = undefined;
+ this._focusSet = false;
+ }
+ }
+
+ private _debouncedSetFilter = debounce((filter: string) => {
+ this._filter = filter;
+ }, 100);
+
+ private _setFocusFirstListItem() {
+ // @ts-ignore
+ this._getItemAtIndex(0)?.rippleHandlers.startFocus();
+ }
+
+ private _handleListItemKeyDown(ev: KeyboardEvent) {
+ const isSingleCharacter = ev.key.length === 1;
+ const isFirstListItem =
+ (ev.target as HTMLElement).getAttribute("index") === "0";
+ if (ev.key === "ArrowUp") {
+ if (isFirstListItem) {
+ this._filterInputField?.focus();
+ }
+ }
+ if (ev.key === "Backspace" || isSingleCharacter) {
+ (ev.currentTarget as List).scrollTop = 0;
+ this._filterInputField?.focus();
+ }
+ }
+
+ private _generateCommandItems(): QuickBarItem[] {
+ return [...this._generateReloadCommands()].sort((a, b) =>
+ compare(a.text.toLowerCase(), b.text.toLowerCase())
+ );
+ }
+
+ private _generateReloadCommands(): QuickBarItem[] {
+ const reloadableDomains = componentsWithService(this.hass, "reload").sort();
+
+ return reloadableDomains.map((domain) => ({
+ text:
+ this.hass.localize(`ui.dialogs.quick-bar.commands.reload.${domain}`) ||
+ this.hass.localize(
+ "ui.dialogs.quick-bar.commands.reload.reload",
+ "domain",
+ domainToName(this.hass.localize, domain)
+ ),
+ icon: domainIcon(domain),
+ action: () => this.hass.callService(domain, "reload"),
+ }));
+ }
+
+ private _generateEntityItems(): QuickBarItem[] {
+ return Object.keys(this.hass.states)
+ .map((entityId) => ({
+ text: computeStateName(this.hass.states[entityId]),
+ altText: entityId,
+ icon: domainIcon(computeDomain(entityId), this.hass.states[entityId]),
+ action: () => fireEvent(this, "hass-more-info", { entityId }),
+ }))
+ .sort((a, b) => compare(a.text.toLowerCase(), b.text.toLowerCase()));
+ }
+
+ private _toggleIfAlreadyOpened() {
+ return this._opened ? !this._commandMode : false;
+ }
+
+ private _setFilteredItems() {
+ const items = this._commandMode ? this._commandItems : this._entityItems;
+ this._items = this._filter
+ ? this._filterItems(items || [], this._filter)
+ : items;
+ }
+
+ private _filterItems = memoizeOne(
+ (items: QuickBarItem[], filter: string): QuickBarItem[] =>
+ fuzzyFilterSort(filter.trimLeft(), items)
+ );
+
+ static get styles() {
+ return [
+ haStyleDialog,
+ css`
+ .heading {
+ padding: 8px 20px 0px;
+ }
+
+ ha-dialog {
+ --dialog-z-index: 8;
+ --dialog-content-padding: 0;
+ }
+
+ @media (min-width: 800px) {
+ ha-dialog {
+ --mdc-dialog-max-width: 800px;
+ --mdc-dialog-min-width: 500px;
+ --dialog-surface-position: fixed;
+ --dialog-surface-top: 40px;
+ --mdc-dialog-max-height: calc(100% - 72px);
+ }
+ }
+
+ ha-svg-icon.prefix {
+ margin: 8px;
+ }
+
+ .uni-virtualizer-host {
+ display: block;
+ position: relative;
+ contain: strict;
+ overflow: auto;
+ height: 100%;
+ }
+
+ .uni-virtualizer-host > * {
+ box-sizing: border-box;
+ }
+
+ mwc-list-item {
+ width: 100%;
+ }
+ `,
+ ];
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-quick-bar": QuickBar;
+ }
+}
diff --git a/src/dialogs/quick-bar/show-dialog-quick-bar.ts b/src/dialogs/quick-bar/show-dialog-quick-bar.ts
new file mode 100644
index 0000000000..b003af2cfa
--- /dev/null
+++ b/src/dialogs/quick-bar/show-dialog-quick-bar.ts
@@ -0,0 +1,20 @@
+import { fireEvent } from "../../common/dom/fire_event";
+
+export interface QuickBarParams {
+ entityFilter?: string;
+ commandMode?: boolean;
+}
+
+export const loadQuickBar = () =>
+ import(/* webpackChunkName: "quick-bar-dialog" */ "./ha-quick-bar");
+
+export const showQuickBar = (
+ element: HTMLElement,
+ dialogParams: QuickBarParams
+): void => {
+ fireEvent(element, "show-dialog", {
+ dialogTag: "ha-quick-bar",
+ dialogImport: loadQuickBar,
+ dialogParams,
+ });
+};
diff --git a/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts b/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts
index aafdb13ee0..305d3206c8 100644
--- a/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts
+++ b/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts
@@ -57,7 +57,7 @@ export class HaVoiceCommandDialog extends LitElement {
@internalProperty() private _agentInfo?: AgentInfo;
- @query("#messages") private messages!: PaperDialogScrollableElement;
+ @query("#messages", true) private messages!: PaperDialogScrollableElement;
private recognition!: SpeechRecognition;
diff --git a/src/entrypoints/custom-panel.ts b/src/entrypoints/custom-panel.ts
index 4ff9160025..bbb9320c41 100644
--- a/src/entrypoints/custom-panel.ts
+++ b/src/entrypoints/custom-panel.ts
@@ -35,7 +35,10 @@ function setProperties(properties) {
setCustomPanelProperties(panelEl, properties);
}
-function initialize(panel: CustomPanelInfo, properties: {}) {
+function initialize(
+ panel: CustomPanelInfo,
+ properties: Record
+) {
const style = document.createElement("style");
style.innerHTML = "body{margin:0}";
document.head.appendChild(style);
diff --git a/src/external_app/external_auth.ts b/src/external_app/external_auth.ts
index c3c6f0fe42..f1624e77d6 100644
--- a/src/external_app/external_auth.ts
+++ b/src/external_app/external_auth.ts
@@ -64,7 +64,13 @@ export class ExternalAuth extends Auth {
});
}
+ private _tokenCallbackPromise?: Promise;
+
public async refreshAccessToken(force?: boolean) {
+ if (this._tokenCallbackPromise && !force) {
+ await this._tokenCallbackPromise;
+ return;
+ }
const payload: GetExternalAuthPayload = {
callback: CALLBACK_SET_TOKEN,
};
@@ -72,14 +78,15 @@ export class ExternalAuth extends Auth {
payload.force = true;
}
- const callbackPromise = new Promise(
+ this._tokenCallbackPromise = new Promise(
(resolve, reject) => {
window[CALLBACK_SET_TOKEN] = (success, data) =>
success ? resolve(data) : reject(data);
}
);
- await 0;
+ // we sleep 1 microtask to get the promise to actually set it on the window object.
+ await Promise.resolve();
if (window.externalApp) {
window.externalApp.getExternalAuth(JSON.stringify(payload));
@@ -87,10 +94,11 @@ export class ExternalAuth extends Auth {
window.webkit!.messageHandlers.getExternalAuth.postMessage(payload);
}
- const tokens = await callbackPromise;
+ const tokens = await this._tokenCallbackPromise;
this.data.access_token = tokens.access_token;
this.data.expires = tokens.expires_in * 1000 + Date.now();
+ this._tokenCallbackPromise = undefined;
}
public async revoke() {
@@ -101,7 +109,8 @@ export class ExternalAuth extends Auth {
success ? resolve(data) : reject(data);
});
- await 0;
+ // we sleep 1 microtask to get the promise to actually set it on the window object.
+ await Promise.resolve();
if (window.externalApp) {
window.externalApp.revokeExternalAuth(JSON.stringify(payload));
diff --git a/src/html/_js_base.html.template b/src/html/_js_base.html.template
index 2864581ce2..08cc3ce65a 100644
--- a/src/html/_js_base.html.template
+++ b/src/html/_js_base.html.template
@@ -20,5 +20,4 @@
"content" in document.createElement("template"))) {
document.write("
diff --git a/src/html/_preload_roboto.html.template b/src/html/_preload_roboto.html.template
index 53431bb914..382beadfc0 100644
--- a/src/html/_preload_roboto.html.template
+++ b/src/html/_preload_roboto.html.template
@@ -8,7 +8,7 @@
el.type = "font/woff2";
el.href = src;
el.crossOrigin = "anonymous";
- document.head.append(el);
+ document.head.appendChild(el);
}
_pf("/static/fonts/roboto/Roboto-Regular.woff2");
_pf("/static/fonts/roboto/Roboto-Medium.woff2");
diff --git a/src/html/_style_base.html.template b/src/html/_style_base.html.template
index 21d047c1b8..0777efeb57 100644
--- a/src/html/_style_base.html.template
+++ b/src/html/_style_base.html.template
@@ -1,4 +1,4 @@
-
+
[[localize('ui.panel.config.core.section.core.header')]] {
- this.validating = false;
- this.isValid = result.result === "valid";
-
- if (!this.isValid) {
- this.validateLog = result.errors;
- }
- });
- }
}
customElements.define("ha-config-section-core", HaConfigSectionCore);
diff --git a/src/panels/config/customize/ha-config-customize.js b/src/panels/config/customize/ha-config-customize.js
index 2f2b3eb37f..d008085d2b 100644
--- a/src/panels/config/customize/ha-config-customize.js
+++ b/src/panels/config/customize/ha-config-customize.js
@@ -11,6 +11,7 @@ import "../ha-config-section";
import "../ha-entity-config";
import { configSections } from "../ha-panel-config";
import "./ha-form-customize";
+import { documentationUrl } from "../../../util/documentation-url";
/*
* @appliesMixin LocalizeMixin
@@ -34,6 +35,14 @@ class HaConfigCustomize extends LocalizeMixin(PolymerElement) {
[[localize('ui.panel.config.customize.picker.introduction')]]
+
+
+ [[localize("ui.panel.config.customize.picker.documentation")]]
+
hass.states[key])
.sort(sortStatesByName);
}
+
+ _computeDocumentationUrl(hass) {
+ return documentationUrl(
+ hass,
+ "/docs/configuration/customizing-devices/#customization-using-the-ui"
+ );
+ }
}
customElements.define("ha-config-customize", HaConfigCustomize);
diff --git a/src/panels/config/customize/ha-form-customize.js b/src/panels/config/customize/ha-form-customize.js
index 2ad523125d..4df81153df 100644
--- a/src/panels/config/customize/ha-form-customize.js
+++ b/src/panels/config/customize/ha-form-customize.js
@@ -40,35 +40,35 @@ class HaFormCustomize extends LocalizeMixin(PolymerElement) {
-
+
[[localize('ui.panel.config.customize.attributes_customize')]]
-
+
-
+
[[localize('ui.panel.config.customize.attributes_outside')]]
[[localize('ui.panel.config.customize.different_include')]]
-
+
-
+
[[localize('ui.panel.config.customize.attributes_set')]]
[[localize('ui.panel.config.customize.attributes_override')]]
-
+
-
+
[[localize('ui.panel.config.customize.attributes_not_set')]]
-
+
diff --git a/src/panels/config/devices/device-detail/integration-elements/mqtt/mqtt-discovery-payload.ts b/src/panels/config/devices/device-detail/integration-elements/mqtt/mqtt-discovery-payload.ts
index 140d53c4de..4adc561007 100644
--- a/src/panels/config/devices/device-detail/integration-elements/mqtt/mqtt-discovery-payload.ts
+++ b/src/panels/config/devices/device-detail/integration-elements/mqtt/mqtt-discovery-payload.ts
@@ -13,7 +13,7 @@ import { classMap } from "lit-html/directives/class-map";
@customElement("mqtt-discovery-payload")
class MQTTDiscoveryPayload extends LitElement {
- @property() public payload!: object;
+ @property() public payload!: Record;
@property() public showAsYaml = false;
diff --git a/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-actions-ozw.ts b/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-actions-ozw.ts
index 7f0cac03f2..6773c324a9 100644
--- a/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-actions-ozw.ts
+++ b/src/panels/config/devices/device-detail/integration-elements/ozw/ha-device-actions-ozw.ts
@@ -31,7 +31,6 @@ export class HaDeviceActionsOzw extends LitElement {
@property()
private ozw_instance = 1;
-
protected updated(changedProperties: PropertyValues) {
if (changedProperties.has("device")) {
const identifiers:
diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts
index 22c2ea190a..c1072e6b29 100644
--- a/src/panels/config/devices/ha-config-device-page.ts
+++ b/src/panels/config/devices/ha-config-device-page.ts
@@ -259,7 +259,7 @@ export class HaConfigDevicePage extends LitElement {
isComponentLoaded(this.hass, "automation")
? html`
-
+
${this.hass.localize(
"ui.panel.config.devices.automation.automations"
)}
@@ -270,7 +270,7 @@ export class HaConfigDevicePage extends LitElement {
)}
icon="hass:plus-circle"
>
-
+
${this._related?.automation?.length
? this._related.automation.map((automation) => {
const state = this.hass.states[automation];
@@ -328,7 +328,7 @@ export class HaConfigDevicePage extends LitElement {
isComponentLoaded(this.hass, "scene") && entities.length
? html`
-
+
${this.hass.localize(
"ui.panel.config.devices.scene.scenes"
)}
@@ -340,7 +340,7 @@ export class HaConfigDevicePage extends LitElement {
)}
icon="hass:plus-circle"
>
-
+
${
this._related?.scene?.length
@@ -402,7 +402,7 @@ export class HaConfigDevicePage extends LitElement {
isComponentLoaded(this.hass, "script")
? html`
-
+
${this.hass.localize(
"ui.panel.config.devices.script.scripts"
)}
@@ -413,18 +413,14 @@ export class HaConfigDevicePage extends LitElement {
)}
icon="hass:plus-circle"
>
-
+
${this._related?.script?.length
? this._related.script.map((script) => {
const state = this.hass.states[script];
return state
? html`
@@ -578,6 +574,9 @@ export class HaConfigDevicePage extends LitElement {
text: this.hass.localize(
"ui.panel.config.devices.confirm_rename_entity_ids_warning"
),
+ confirmText: this.hass.localize("ui.common.yes"),
+ dismissText: this.hass.localize("ui.common.no"),
+ warning: true,
}));
const updateProms = entities.map((entity) => {
@@ -716,6 +715,7 @@ export class HaConfigDevicePage extends LitElement {
paper-item {
cursor: pointer;
+ font-size: var(--paper-font-body1_-_font-size);
}
paper-item.no-link {
diff --git a/src/panels/config/devices/ha-config-devices-dashboard.ts b/src/panels/config/devices/ha-config-devices-dashboard.ts
index 274eecf5e0..7e954b5dbc 100644
--- a/src/panels/config/devices/ha-config-devices-dashboard.ts
+++ b/src/panels/config/devices/ha-config-devices-dashboard.ts
@@ -82,11 +82,11 @@ export class HaConfigDeviceDashboard extends LitElement {
filterTexts.push(
`${this.hass.localize(
"ui.panel.config.integrations.integration"
- )} ${integrationName}${
+ )} "${integrationName}${
integrationName !== configEntry.title
? `: ${configEntry.title}`
: ""
- }`
+ }"`
);
break;
}
@@ -310,6 +310,7 @@ export class HaConfigDeviceDashboard extends LitElement {
this.hass.localize
)}
@row-click=${this._handleRowClicked}
+ clickable
>
`;
diff --git a/src/panels/config/entities/const.ts b/src/panels/config/entities/const.ts
index bf9b0c5ad6..1c5e4b4f59 100644
--- a/src/panels/config/entities/const.ts
+++ b/src/panels/config/entities/const.ts
@@ -5,4 +5,6 @@ export const PLATFORMS_WITH_SETTINGS_TAB = {
input_text: "entity-settings-helper-tab",
input_boolean: "entity-settings-helper-tab",
input_datetime: "entity-settings-helper-tab",
+ counter: "entity-settings-helper-tab",
+ timer: "entity-settings-helper-tab",
};
diff --git a/src/panels/config/entities/dialog-entity-editor.ts b/src/panels/config/entities/dialog-entity-editor.ts
index 87e3cb87da..c12c79d28e 100644
--- a/src/panels/config/entities/dialog-entity-editor.ts
+++ b/src/panels/config/entities/dialog-entity-editor.ts
@@ -175,16 +175,11 @@ export class DialogEntityEditor extends LitElement {
"ui.dialogs.entity_registry.no_unique_id",
"faq_link",
html`${this.hass.localize(
- "ui.dialogs.entity_registry.faq"
- )}`
+ >${this.hass.localize("ui.dialogs.entity_registry.faq")} `
)}
`;
diff --git a/src/panels/config/entities/editor-tabs/settings/entity-settings-helper-tab.ts b/src/panels/config/entities/editor-tabs/settings/entity-settings-helper-tab.ts
index fbaa723ec8..8f7ecfff2f 100644
--- a/src/panels/config/entities/editor-tabs/settings/entity-settings-helper-tab.ts
+++ b/src/panels/config/entities/editor-tabs/settings/entity-settings-helper-tab.ts
@@ -42,6 +42,16 @@ import {
fetchInputText,
updateInputText,
} from "../../../../../data/input_text";
+import {
+ deleteCounter,
+ fetchCounter,
+ updateCounter,
+} from "../../../../../data/counter";
+import {
+ deleteTimer,
+ fetchTimer,
+ updateTimer,
+} from "../../../../../data/timer";
import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../../../types";
import type { Helper } from "../../../helpers/const";
@@ -50,6 +60,8 @@ import "../../../helpers/forms/ha-input_datetime-form";
import "../../../helpers/forms/ha-input_number-form";
import "../../../helpers/forms/ha-input_select-form";
import "../../../helpers/forms/ha-input_text-form";
+import "../../../helpers/forms/ha-counter-form";
+import "../../../helpers/forms/ha-timer-form";
import "../../entity-registry-basic-editor";
import type { HaEntityRegistryBasicEditor } from "../../entity-registry-basic-editor";
import { haStyle } from "../../../../../resources/styles";
@@ -80,6 +92,16 @@ const HELPERS = {
update: updateInputSelect,
delete: deleteInputSelect,
},
+ counter: {
+ fetch: fetchCounter,
+ update: updateCounter,
+ delete: deleteCounter,
+ },
+ timer: {
+ fetch: fetchTimer,
+ update: updateTimer,
+ delete: deleteTimer,
+ },
};
@customElement("entity-settings-helper-tab")
diff --git a/src/panels/config/entities/entity-registry-settings.ts b/src/panels/config/entities/entity-registry-settings.ts
index 5ac897dc78..e6b7ea577d 100644
--- a/src/panels/config/entities/entity-registry-settings.ts
+++ b/src/panels/config/entities/entity-registry-settings.ts
@@ -27,6 +27,7 @@ import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box
import type { PolymerChangedEvent } from "../../../polymer-types";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
+import { domainIcon } from "../../../common/entity/domain_icon";
@customElement("entity-registry-settings")
export class EntityRegistrySettings extends LitElement {
@@ -93,7 +94,11 @@ export class EntityRegistrySettings extends LitElement {
.value=${this._icon}
@value-changed=${this._iconChanged}
.label=${this.hass.localize("ui.dialogs.entity_registry.editor.icon")}
- .placeholder=${this.entry.original_icon}
+ .placeholder=${this.entry.original_icon ||
+ domainIcon(
+ computeDomain(this.entry.entity_id),
+ this.hass.states[this.entry.entity_id]
+ )}
.disabled=${this._submitting}
.errorMessage=${this.hass.localize(
"ui.dialogs.entity_registry.editor.icon_error"
diff --git a/src/panels/config/entities/ha-config-entities.ts b/src/panels/config/entities/ha-config-entities.ts
index 366c852fa9..85ffc4a26a 100644
--- a/src/panels/config/entities/ha-config-entities.ts
+++ b/src/panels/config/entities/ha-config-entities.ts
@@ -97,13 +97,15 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
@internalProperty() private _filter = "";
+ @internalProperty() private _numHiddenEntities = 0;
+
@internalProperty() private _searchParms = new URLSearchParams(
window.location.search
);
@internalProperty() private _selectedEntities: string[] = [];
- @query("hass-tabs-subpage-data-table")
+ @query("hass-tabs-subpage-data-table", true)
private _dataTable!: HaTabsSubpageDataTable;
private getDialog?: () => DialogEntityEditor | undefined;
@@ -118,6 +120,10 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
filters.forEach((value, key) => {
switch (key) {
case "config_entry": {
+ // If we are requested to show the entities for a given config entry,
+ // also show the disabled ones by default.
+ this._showDisabled = true;
+
if (!entries) {
this._loadConfigEntries();
break;
@@ -132,11 +138,11 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
filterTexts.push(
`${this.hass.localize(
"ui.panel.config.integrations.integration"
- )} ${integrationName}${
+ )} "${integrationName}${
integrationName !== configEntry.title
? `: ${configEntry.title}`
: ""
- }`
+ }"`
);
break;
}
@@ -262,11 +268,9 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
showUnavailable: boolean,
showReadOnly: boolean
): EntityRow[] => {
- if (!showDisabled) {
- entities = entities.filter((entity) => !entity.disabled_by);
- }
-
const result: EntityRow[] = [];
+ // If nothing gets filtered, this is our correct count of entities
+ let startLength = entities.length + stateEntities.length;
entities = showReadOnly ? entities.concat(stateEntities) : entities;
@@ -276,10 +280,23 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
entities = entities.filter(
(entity) => entity.config_entry_id === value
);
+ // If we have an active filter and `showReadOnly` is true, the length of `entities` is correct.
+ // If however, the read-only entities were not added before, we need to check how many would
+ // have matched the active filter and add that number to the count.
+ startLength = entities.length;
+ if (!showReadOnly) {
+ startLength += stateEntities.filter(
+ (entity) => entity.config_entry_id === value
+ ).length;
+ }
break;
}
});
+ if (!showDisabled) {
+ entities = entities.filter((entity) => !entity.disabled_by);
+ }
+
for (const entry of entities) {
const entity = this.hass.states[entry.entity_id];
const unavailable = entity?.state === UNAVAILABLE;
@@ -315,6 +332,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
});
}
+ this._numHiddenEntities = startLength - result.length;
return result;
}
);
@@ -358,6 +376,16 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
this.hass.localize,
this._entries
);
+
+ const entityData = this._filteredEntities(
+ this._entities,
+ this._stateEntities,
+ this._searchParms,
+ this._showDisabled,
+ this._showUnavailable,
+ this._showReadOnly
+ );
+
const headerToolbar = this._selectedEntities.length
? html`
@@ -441,11 +469,32 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
"ui.panel.config.filtering.filtering_by"
)}
${activeFilters.join(", ")}
+ ${this._numHiddenEntities
+ ? "(" +
+ this.hass.localize(
+ "ui.panel.config.entities.picker.filter.hidden_entities",
+ "number",
+ this._numHiddenEntities
+ ) +
+ ")"
+ : ""}
`
: `${this.hass.localize(
"ui.panel.config.filtering.filtering_by"
- )} ${activeFilters.join(", ")}`}
+ )} ${activeFilters.join(", ")}
+ ${
+ this._numHiddenEntities
+ ? "(" +
+ this.hass.localize(
+ "ui.panel.config.entities.picker.filter.hidden_entities",
+ "number",
+ this._numHiddenEntities
+ ) +
+ ")"
+ : ""
+ }
+ `}
${this.hass.localize(
"ui.panel.config.filtering.clear"
@@ -453,6 +502,31 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
>
`
: ""}
+ ${this._numHiddenEntities && !activeFilters
+ ? html`
+ ${this.narrow
+ ? html`
+
+
+ ${this.hass.localize(
+ "ui.panel.config.entities.picker.filter.hidden_entities",
+ "number",
+ this._numHiddenEntities
+ )}
+
+ `
+ : `${this.hass.localize(
+ "ui.panel.config.entities.picker.filter.hidden_entities",
+ "number",
+ this._numHiddenEntities
+ )}`}
+ ${this.hass.localize(
+ "ui.panel.config.entities.picker.filter.show_all"
+ )}
+ `
+ : ""}
-
+
;
+
+ @internalProperty() private _name!: string;
+
+ @internalProperty() private _icon!: string;
+
+ @internalProperty() private _maximum?: number;
+
+ @internalProperty() private _minimum?: number;
+
+ @internalProperty() private _restore?: boolean;
+
+ @internalProperty() private _initial?: number;
+
+ @internalProperty() private _step?: number;
+
+ set item(item: Counter) {
+ this._item = item;
+ if (item) {
+ this._name = item.name || "";
+ this._icon = item.icon || "";
+ this._maximum = item.maximum;
+ this._minimum = item.minimum;
+ this._restore = item.restore ?? true;
+ this._step = item.step ?? 1;
+ this._initial = item.initial ?? 0;
+ } else {
+ this._name = "";
+ this._icon = "";
+ this._maximum = undefined;
+ this._minimum = undefined;
+ this._restore = true;
+ this._step = 1;
+ this._initial = 0;
+ }
+ }
+
+ public focus() {
+ this.updateComplete.then(() =>
+ (this.shadowRoot?.querySelector(
+ "[dialogInitialFocus]"
+ ) as HTMLElement)?.focus()
+ );
+ }
+
+ protected render(): TemplateResult {
+ if (!this.hass) {
+ return html``;
+ }
+ const nameInvalid = !this._name || this._name.trim() === "";
+
+ return html`
+
+
+
+
+
+
+ ${this.hass.userData?.showAdvanced
+ ? html`
+
+
+
+
+
+ ${this.hass.localize(
+ "ui.dialogs.helper_settings.counter.restore"
+ )}
+
+
+ `
+ : ""}
+
+ `;
+ }
+
+ private _valueChanged(ev: CustomEvent) {
+ if (!this.new && !this._item) {
+ return;
+ }
+ ev.stopPropagation();
+ const target = ev.target as any;
+ const configValue = target.configValue;
+ const value =
+ target.type === "number"
+ ? Number(ev.detail.value)
+ : target.localName === "ha-switch"
+ ? (ev.target as HaSwitch).checked
+ : ev.detail.value;
+ if (this[`_${configValue}`] === value) {
+ return;
+ }
+ const newValue = { ...this._item };
+ if (value === undefined || value === "") {
+ delete newValue[configValue];
+ } else {
+ newValue[configValue] = value;
+ }
+ fireEvent(this, "value-changed", {
+ value: newValue,
+ });
+ }
+
+ static get styles(): CSSResult[] {
+ return [
+ haStyle,
+ css`
+ .form {
+ color: var(--primary-text-color);
+ }
+ .row {
+ margin-top: 12px;
+ margin-bottom: 12px;
+ color: var(--primary-text-color);
+ display: flex;
+ align-items: center;
+ }
+ .row div {
+ margin-left: 16px;
+ }
+ `,
+ ];
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-counter-form": HaCounterForm;
+ }
+}
diff --git a/src/panels/config/helpers/forms/ha-input_select-form.ts b/src/panels/config/helpers/forms/ha-input_select-form.ts
index eef74de859..657df02826 100644
--- a/src/panels/config/helpers/forms/ha-input_select-form.ts
+++ b/src/panels/config/helpers/forms/ha-input_select-form.ts
@@ -36,7 +36,7 @@ class HaInputSelectForm extends LitElement {
@internalProperty() private _options: string[] = [];
- @query("#option_input") private _optionInput?: PaperInputElement;
+ @query("#option_input", true) private _optionInput?: PaperInputElement;
set item(item: InputSelect) {
this._item = item;
diff --git a/src/panels/config/helpers/forms/ha-timer-form.ts b/src/panels/config/helpers/forms/ha-timer-form.ts
new file mode 100644
index 0000000000..9757b76b94
--- /dev/null
+++ b/src/panels/config/helpers/forms/ha-timer-form.ts
@@ -0,0 +1,130 @@
+import {
+ css,
+ CSSResult,
+ customElement,
+ html,
+ LitElement,
+ property,
+ internalProperty,
+ TemplateResult,
+} from "lit-element";
+import { fireEvent } from "../../../../common/dom/fire_event";
+import "../../../../components/ha-icon-input";
+import { Timer, DurationDict } from "../../../../data/timer";
+import { haStyle } from "../../../../resources/styles";
+import { HomeAssistant } from "../../../../types";
+
+@customElement("ha-timer-form")
+class HaTimerForm extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property() public new?: boolean;
+
+ private _item?: Timer;
+
+ @internalProperty() private _name!: string;
+
+ @internalProperty() private _icon!: string;
+
+ @internalProperty() private _duration!: string | number | DurationDict;
+
+ set item(item: Timer) {
+ this._item = item;
+ if (item) {
+ this._name = item.name || "";
+ this._icon = item.icon || "";
+ this._duration = item.duration || "";
+ } else {
+ this._name = "";
+ this._icon = "";
+ this._duration = "00:00:00";
+ }
+ }
+
+ public focus() {
+ this.updateComplete.then(() =>
+ (this.shadowRoot?.querySelector(
+ "[dialogInitialFocus]"
+ ) as HTMLElement)?.focus()
+ );
+ }
+
+ protected render(): TemplateResult {
+ if (!this.hass) {
+ return html``;
+ }
+ const nameInvalid = !this._name || this._name.trim() === "";
+
+ return html`
+
+
+
+
+
+ `;
+ }
+
+ private _valueChanged(ev: CustomEvent) {
+ if (!this.new && !this._item) {
+ return;
+ }
+ ev.stopPropagation();
+ const configValue = (ev.target as any).configValue;
+ const value = ev.detail.value;
+ if (this[`_${configValue}`] === value) {
+ return;
+ }
+ const newValue = { ...this._item };
+ if (!value) {
+ delete newValue[configValue];
+ } else {
+ newValue[configValue] = ev.detail.value;
+ }
+ fireEvent(this, "value-changed", {
+ value: newValue,
+ });
+ }
+
+ static get styles(): CSSResult[] {
+ return [
+ haStyle,
+ css`
+ .form {
+ color: var(--primary-text-color);
+ }
+ `,
+ ];
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-timer-form": HaTimerForm;
+ }
+}
diff --git a/src/panels/config/helpers/ha-config-helpers.ts b/src/panels/config/helpers/ha-config-helpers.ts
index fe96b3389e..8179474639 100644
--- a/src/panels/config/helpers/ha-config-helpers.ts
+++ b/src/panels/config/helpers/ha-config-helpers.ts
@@ -153,6 +153,7 @@ export class HaConfigHelpers extends LitElement {
.data=${this._getItems(this._stateItems)}
@row-click=${this._openEditDialog}
hasFab
+ clickable
.noDataText=${this.hass.localize(
"ui.panel.config.helpers.picker.no_helpers"
)}
@@ -164,7 +165,7 @@ export class HaConfigHelpers extends LitElement {
)}"
@click=${this._createHelpler}
>
-
+
`;
diff --git a/src/panels/config/info/system-health-card.ts b/src/panels/config/info/system-health-card.ts
index eafbf533db..b3433a0831 100644
--- a/src/panels/config/info/system-health-card.ts
+++ b/src/panels/config/info/system-health-card.ts
@@ -1,14 +1,18 @@
import "../../../components/ha-circular-progress";
+import { mdiContentCopy } from "@mdi/js";
import {
css,
CSSResult,
html,
+ internalProperty,
LitElement,
property,
- internalProperty,
+ query,
TemplateResult,
} from "lit-element";
import "../../../components/ha-card";
+import "@polymer/paper-tooltip/paper-tooltip";
+import type { PaperTooltipElement } from "@polymer/paper-tooltip/paper-tooltip";
import { domainToName } from "../../../data/integration";
import {
fetchSystemHealthInfo,
@@ -37,6 +41,8 @@ class SystemHealthCard extends LitElement {
@internalProperty() private _info?: SystemHealthInfo;
+ @query("paper-tooltip", true) private _toolTip?: PaperTooltipElement;
+
protected render(): TemplateResult {
if (!this.hass) {
return html``;
@@ -78,7 +84,24 @@ class SystemHealthCard extends LitElement {
}
return html`
-
+
+
+
+ ${domainToName(this.hass.localize, "system_health")}
+
+
+
+
+
+ ${this.hass.localize("ui.common.copied")}
+
+
${sections}
`;
@@ -104,6 +127,25 @@ class SystemHealthCard extends LitElement {
}
}
+ private _copyInfo(): void {
+ const selection = window.getSelection()!;
+ selection.removeAllRanges();
+
+ const copyElement = this.shadowRoot?.querySelector(
+ "ha-card"
+ ) as HTMLElement;
+
+ const range = document.createRange();
+ range.selectNodeContents(copyElement);
+ selection.addRange(range);
+
+ document.execCommand("copy");
+ window.getSelection()!.removeAllRanges();
+
+ this._toolTip!.show();
+ setTimeout(() => this._toolTip?.hide(), 3000);
+ }
+
static get styles(): CSSResult {
return css`
table {
@@ -119,6 +161,11 @@ class SystemHealthCard extends LitElement {
align-items: center;
justify-content: center;
}
+
+ .card-header {
+ justify-content: space-between;
+ display: flex;
+ }
`;
}
}
diff --git a/src/panels/config/integrations/ha-config-integrations.ts b/src/panels/config/integrations/ha-config-integrations.ts
index 99d7f6b266..744b3d0e8e 100644
--- a/src/panels/config/integrations/ha-config-integrations.ts
+++ b/src/panels/config/integrations/ha-config-integrations.ts
@@ -287,7 +287,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
.label=${this.hass.localize("ui.common.overflow_menu")}
slot="trigger"
>
-
+
${this.hass.localize(
@@ -480,7 +480,7 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
title=${this.hass.localize("ui.panel.config.integrations.new")}
@click=${this._createFlow}
>
-
+
`;
diff --git a/src/panels/config/integrations/ha-integration-card.ts b/src/panels/config/integrations/ha-integration-card.ts
index a1625c7c2c..a4d9e67ab5 100644
--- a/src/panels/config/integrations/ha-integration-card.ts
+++ b/src/panels/config/integrations/ha-integration-card.ts
@@ -249,7 +249,7 @@ export class HaIntegrationCard extends LitElement {
.label=${this.hass.localize("ui.common.overflow_menu")}
slot="trigger"
>
-
+
${this.hass.localize(
@@ -475,7 +475,7 @@ export class HaIntegrationCard extends LitElement {
align-items: center;
height: 40px;
padding: 16px 16px 8px 16px;
- vertical-align: middle;
+ justify-content: center;
}
.group-header h1 {
margin: 0;
@@ -495,7 +495,6 @@ export class HaIntegrationCard extends LitElement {
max-height: 100%;
max-width: 90%;
}
-
.none-found {
margin: auto;
text-align: center;
@@ -507,9 +506,16 @@ export class HaIntegrationCard extends LitElement {
margin-bottom: 0;
}
h2 {
- margin-top: 0;
min-height: 24px;
}
+ h3 {
+ word-wrap: break-word;
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 3;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
ha-button-menu {
color: var(--secondary-text-color);
--mdc-menu-min-width: 200px;
diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-network-nodes.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-network-nodes.ts
index f492cf579f..bd796519a4 100644
--- a/src/panels/config/integrations/integration-panels/ozw/ozw-network-nodes.ts
+++ b/src/panels/config/integrations/integration-panels/ozw/ozw-network-nodes.ts
@@ -117,6 +117,7 @@ class OZWNetworkNodes extends LitElement {
.data=${this._nodes}
id="node_id"
@row-click=${this._handleRowClicked}
+ clickable
>
`;
diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-node-config.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-node-config.ts
new file mode 100644
index 0000000000..b1017aaaa8
--- /dev/null
+++ b/src/panels/config/integrations/integration-panels/ozw/ozw-node-config.ts
@@ -0,0 +1,274 @@
+import "@material/mwc-button/mwc-button";
+import "@material/mwc-fab";
+import {
+ css,
+ CSSResultArray,
+ customElement,
+ html,
+ LitElement,
+ internalProperty,
+ property,
+ TemplateResult,
+} from "lit-element";
+import { navigate } from "../../../../../common/navigate";
+import "../../../../../components/buttons/ha-call-service-button";
+import "../../../../../components/ha-card";
+import "../../../../../components/ha-icon-next";
+import "../../../../../layouts/hass-tabs-subpage";
+import { haStyle } from "../../../../../resources/styles";
+import type { HomeAssistant, Route } from "../../../../../types";
+import "../../../ha-config-section";
+import {
+ fetchOZWNodeStatus,
+ fetchOZWNodeMetadata,
+ fetchOZWNodeConfig,
+ OZWDevice,
+ OZWDeviceMetaDataResponse,
+ OZWDeviceConfig,
+} from "../../../../../data/ozw";
+import { ERR_NOT_FOUND } from "../../../../../data/websocket_api";
+import { showOZWRefreshNodeDialog } from "./show-dialog-ozw-refresh-node";
+import { ozwNodeTabs } from "./ozw-node-router";
+
+@customElement("ozw-node-config")
+class OZWNodeConfig extends LitElement {
+ @property({ type: Object }) public hass!: HomeAssistant;
+
+ @property({ type: Object }) public route!: Route;
+
+ @property({ type: Boolean }) public narrow!: boolean;
+
+ @property({ type: Boolean }) public isWide!: boolean;
+
+ @property() public configEntryId?: string;
+
+ @property() public ozwInstance?;
+
+ @property() public nodeId?;
+
+ @internalProperty() private _node?: OZWDevice;
+
+ @internalProperty() private _metadata?: OZWDeviceMetaDataResponse;
+
+ @internalProperty() private _config?: OZWDeviceConfig[];
+
+ @internalProperty() private _error?: string;
+
+ protected firstUpdated() {
+ if (!this.ozwInstance) {
+ navigate(this, "/config/ozw/dashboard", true);
+ } else if (!this.nodeId) {
+ navigate(this, `/config/ozw/network/${this.ozwInstance}/nodes`, true);
+ } else {
+ this._fetchData();
+ }
+ }
+
+ protected render(): TemplateResult {
+ if (this._error) {
+ return html`
+
+ `;
+ }
+
+ return html`
+
+
+
+ ${this.hass.localize("ui.panel.config.ozw.node_config.header")}
+
+
+
+ ${this.hass.localize(
+ "ui.panel.config.ozw.node_config.introduction"
+ )}
+
+
+ ${this.hass.localize(
+ "ui.panel.config.ozw.node_config.help_source"
+ )}
+
+
+
+ Note: This panel is currently read-only. The ability to change
+ values will come in a later update.
+
+
+ ${this._node
+ ? html`
+
+
+
+ ${this._node.node_manufacturer_name}
+ ${this._node.node_product_name}
+ ${this.hass.localize("ui.panel.config.ozw.common.node_id")}:
+ ${this._node.node_id}
+ ${this.hass.localize(
+ "ui.panel.config.ozw.common.query_stage"
+ )}:
+ ${this._node.node_query_stage}
+ ${this._metadata?.metadata.ProductManualURL
+ ? html`
+
+ ${this.hass.localize(
+ "ui.panel.config.ozw.node_metadata.product_manual"
+ )}
+
+ `
+ : ``}
+
+
+
+ ${this.hass.localize(
+ "ui.panel.config.ozw.refresh_node.button"
+ )}
+
+
+
+
+ ${this._metadata?.metadata.WakeupHelp
+ ? html`
+
+
+
+ ${this.hass.localize(
+ "ui.panel.config.ozw.node_config.wakeup_help"
+ )}
+
+
+ ${this._metadata.metadata.WakeupHelp}
+
+
+
+ `
+ : ``}
+ ${this._config
+ ? html`
+ ${this._config.map(
+ (item) => html`
+
+
+ ${item.label}
+ ${item.help}
+ ${item.value}
+
+
+ `
+ )}
+ `
+ : ``}
+ `
+ : ``}
+
+
+ `;
+ }
+
+ private async _fetchData() {
+ if (!this.ozwInstance || !this.nodeId) {
+ return;
+ }
+
+ try {
+ const nodeProm = fetchOZWNodeStatus(
+ this.hass!,
+ this.ozwInstance,
+ this.nodeId
+ );
+ const metadataProm = fetchOZWNodeMetadata(
+ this.hass!,
+ this.ozwInstance,
+ this.nodeId
+ );
+ const configProm = fetchOZWNodeConfig(
+ this.hass!,
+ this.ozwInstance,
+ this.nodeId
+ );
+ [this._node, this._metadata, this._config] = await Promise.all([
+ nodeProm,
+ metadataProm,
+ configProm,
+ ]);
+ } catch (err) {
+ if (err.code === ERR_NOT_FOUND) {
+ this._error = ERR_NOT_FOUND;
+ return;
+ }
+ throw err;
+ }
+ }
+
+ private async _refreshNodeClicked() {
+ showOZWRefreshNodeDialog(this, {
+ node_id: this.nodeId,
+ ozw_instance: this.ozwInstance,
+ });
+ }
+
+ static get styles(): CSSResultArray {
+ return [
+ haStyle,
+ css`
+ .secondary {
+ color: var(--secondary-text-color);
+ font-size: 0.9em;
+ }
+
+ .content {
+ margin-top: 24px;
+ }
+
+ .sectionHeader {
+ position: relative;
+ padding-right: 40px;
+ }
+
+ ha-card {
+ margin: 0 auto;
+ max-width: 600px;
+ }
+
+ [hidden] {
+ display: none;
+ }
+
+ blockquote {
+ display: block;
+ background-color: #ddd;
+ padding: 8px;
+ margin: 8px 0;
+ font-size: 0.9em;
+ }
+
+ blockquote em {
+ font-size: 0.9em;
+ margin-top: 6px;
+ }
+ `,
+ ];
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ozw-node-config": OZWNodeConfig;
+ }
+}
diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-node-dashboard.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-node-dashboard.ts
index bd2fa6af3e..c860027551 100644
--- a/src/panels/config/integrations/integration-panels/ozw/ozw-node-dashboard.ts
+++ b/src/panels/config/integrations/integration-panels/ozw/ozw-node-dashboard.ts
@@ -26,7 +26,7 @@ import {
} from "../../../../../data/ozw";
import { ERR_NOT_FOUND } from "../../../../../data/websocket_api";
import { showOZWRefreshNodeDialog } from "./show-dialog-ozw-refresh-node";
-import { ozwNetworkTabs } from "./ozw-network-router";
+import { ozwNodeTabs } from "./ozw-node-router";
@customElement("ozw-node-dashboard")
class OZWNodeDashboard extends LitElement {
@@ -74,7 +74,7 @@ class OZWNodeDashboard extends LitElement {
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
- .tabs=${ozwNetworkTabs(this.ozwInstance)}
+ .tabs=${ozwNodeTabs(this.ozwInstance, this.nodeId)}
>
@@ -87,19 +87,29 @@ class OZWNodeDashboard extends LitElement {
${this._node
? html`
-
- ${this._node.node_manufacturer_name}
- ${this._node.node_product_name}
- Node ID: ${this._node.node_id}
- Query Stage: ${this._node.node_query_stage}
- ${this._metadata?.metadata.ProductManualURL
- ? html`
- Product Manual
- `
+
+
+
+ ${this._node.node_manufacturer_name}
+ ${this._node.node_product_name}
+
+
+ Node ID: ${this._node.node_id}
+ Query Stage: ${this._node.node_query_stage}
+ ${this._metadata?.metadata.ProductManualURL
+ ? html`
+ Product Manual
+ `
+ : ``}
+
+ ${this._metadata?.metadata.ProductPicBase64
+ ? html`
`
: ``}
@@ -199,6 +209,11 @@ class OZWNodeDashboard extends LitElement {
max-width: 600px;
}
+ .flex {
+ display: flex;
+ justify-content: space-between;
+ }
+
.card-actions.warning ha-call-service-button {
color: var(--error-color);
}
@@ -219,6 +234,15 @@ class OZWNodeDashboard extends LitElement {
[hidden] {
display: none;
}
+
+ .product-image {
+ padding: 12px;
+ max-height: 140px;
+ max-width: 140px;
+ }
+ .card-actions {
+ clear: right;
+ }
`,
];
}
diff --git a/src/panels/config/integrations/integration-panels/ozw/ozw-node-router.ts b/src/panels/config/integrations/integration-panels/ozw/ozw-node-router.ts
index baf68ddc96..ea30bf78ff 100644
--- a/src/panels/config/integrations/integration-panels/ozw/ozw-node-router.ts
+++ b/src/panels/config/integrations/integration-panels/ozw/ozw-node-router.ts
@@ -1,11 +1,31 @@
+import { mdiNetwork, mdiWrench } from "@mdi/js";
import { customElement, property } from "lit-element";
import { navigate } from "../../../../../common/navigate";
import {
HassRouterPage,
RouterOptions,
} from "../../../../../layouts/hass-router-page";
+import { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
import { HomeAssistant } from "../../../../../types";
+export const ozwNodeTabs = (
+ instance: number,
+ node: number
+): PageNavigation[] => {
+ return [
+ {
+ translationKey: "ui.panel.config.ozw.navigation.node.dashboard",
+ path: `/config/ozw/network/${instance}/node/${node}/dashboard`,
+ iconPath: mdiNetwork,
+ },
+ {
+ translationKey: "ui.panel.config.ozw.navigation.node.config",
+ path: `/config/ozw/network/${instance}/node/${node}/config`,
+ iconPath: mdiWrench,
+ },
+ ];
+};
+
@customElement("ozw-node-router")
class OZWNodeRouter extends HassRouterPage {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -33,6 +53,11 @@ class OZWNodeRouter extends HassRouterPage {
/* webpackChunkName: "ozw-node-dashboard" */ "./ozw-node-dashboard"
),
},
+ config: {
+ tag: "ozw-node-config",
+ load: () =>
+ import(/* webpackChunkName: "ozw-node-config" */ "./ozw-node-config"),
+ },
},
};
diff --git a/src/panels/config/integrations/integration-panels/zha/zha-add-group-page.ts b/src/panels/config/integrations/integration-panels/zha/zha-add-group-page.ts
index a7929f0bda..fdbe21ea19 100644
--- a/src/panels/config/integrations/integration-panels/zha/zha-add-group-page.ts
+++ b/src/panels/config/integrations/integration-panels/zha/zha-add-group-page.ts
@@ -42,7 +42,7 @@ export class ZHAAddGroupPage extends LitElement {
@internalProperty() private _groupName = "";
- @query("zha-device-endpoint-data-table")
+ @query("zha-device-endpoint-data-table", true)
private _zhaDevicesDataTable!: ZHADeviceEndpointDataTable;
private _firstUpdatedCalled = false;
@@ -105,12 +105,15 @@ export class ZHAAddGroupPage extends LitElement {
@click="${this._createGroup}"
class="button"
>
-
+ ${this._processingAdd
+ ? html` `
+ : ""}
${this.hass!.localize(
"ui.panel.config.zha.groups.create"
)} {
let outputClusters: ClusterRowData[] = clusters;
diff --git a/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard.ts b/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard.ts
index a0d8ff6be4..32db5e38d7 100644
--- a/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard.ts
+++ b/src/panels/config/integrations/integration-panels/zha/zha-config-dashboard.ts
@@ -87,7 +87,7 @@ class ZHAConfigDashboard extends LitElement {
title=${this.hass.localize("ui.panel.config.zha.add_device")}
?rtl=${computeRTL(this.hass)}
>
-
+
diff --git a/src/panels/config/integrations/integration-panels/zha/zha-device-endpoint-data-table.ts b/src/panels/config/integrations/integration-panels/zha/zha-device-endpoint-data-table.ts
index 0a12acabe6..9537ed3572 100644
--- a/src/panels/config/integrations/integration-panels/zha/zha-device-endpoint-data-table.ts
+++ b/src/panels/config/integrations/integration-panels/zha/zha-device-endpoint-data-table.ts
@@ -42,7 +42,7 @@ export class ZHADeviceEndpointDataTable extends LitElement {
@property({ type: Array }) public deviceEndpoints: ZHADeviceEndpoint[] = [];
- @query("ha-data-table") private _dataTable!: HaDataTable;
+ @query("ha-data-table", true) private _dataTable!: HaDataTable;
private _deviceEndpoints = memoizeOne(
(deviceEndpoints: ZHADeviceEndpoint[]) => {
diff --git a/src/panels/config/integrations/integration-panels/zha/zha-group-binding.ts b/src/panels/config/integrations/integration-panels/zha/zha-group-binding.ts
index bbf2306703..56b710b014 100644
--- a/src/panels/config/integrations/integration-panels/zha/zha-group-binding.ts
+++ b/src/panels/config/integrations/integration-panels/zha/zha-group-binding.ts
@@ -59,7 +59,7 @@ export class ZHAGroupBindingControl extends LitElement {
private _clustersToBind?: Cluster[];
- @query("zha-clusters-data-table")
+ @query("zha-clusters-data-table", true)
private _zhaClustersDataTable!: ZHAClustersDataTable;
protected updated(changedProperties: PropertyValues): void {
diff --git a/src/panels/config/integrations/integration-panels/zha/zha-group-page.ts b/src/panels/config/integrations/integration-panels/zha/zha-group-page.ts
index c4a928b669..2c52b29933 100644
--- a/src/panels/config/integrations/integration-panels/zha/zha-group-page.ts
+++ b/src/panels/config/integrations/integration-panels/zha/zha-group-page.ts
@@ -57,7 +57,7 @@ export class ZHAGroupPage extends LitElement {
@internalProperty() private _selectedDevicesToRemove: string[] = [];
- @query("#addMembers")
+ @query("#addMembers", true)
private _zhaAddMembersDataTable!: ZHADeviceEndpointDataTable;
@query("#removeMembers")
@@ -203,12 +203,13 @@ export class ZHAGroupPage extends LitElement {
@click="${this._addMembersToGroup}"
class="button"
>
-
+ ${this._processingAdd
+ ? html` `
+ : ""}
${this.hass!.localize(
"ui.panel.config.zha.groups.add_members"
)}
-
+
diff --git a/src/panels/config/logs/dialog-system-log-detail.ts b/src/panels/config/logs/dialog-system-log-detail.ts
index 24d73c7a94..34d0cfcd1a 100644
--- a/src/panels/config/logs/dialog-system-log-detail.ts
+++ b/src/panels/config/logs/dialog-system-log-detail.ts
@@ -36,7 +36,7 @@ class DialogSystemLogDetail extends LitElement {
@internalProperty() private _manifest?: IntegrationManifest;
- @query("paper-tooltip") private _toolTip?: PaperTooltipElement;
+ @query("paper-tooltip", true) private _toolTip?: PaperTooltipElement;
public async showDialog(params: SystemLogDetailDialogParams): Promise {
this._params = params;
@@ -206,7 +206,6 @@ class DialogSystemLogDetail extends LitElement {
font-family: var(--code-font-family, monospace);
}
.heading {
- display: flex;
display: flex;
align-items: center;
justify-content: space-between;
diff --git a/src/panels/config/logs/error-log-card.ts b/src/panels/config/logs/error-log-card.ts
index 88dac9b2de..89229e41ef 100644
--- a/src/panels/config/logs/error-log-card.ts
+++ b/src/panels/config/logs/error-log-card.ts
@@ -15,25 +15,29 @@ import { HomeAssistant } from "../../../types";
class ErrorLogCard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
- @internalProperty() private _errorLog?: string;
+ @internalProperty() private _errorHTML!: TemplateResult[] | string;
protected render(): TemplateResult {
return html`
-
- ${this._errorLog
+
+ ${this._errorHTML
? html`
-
+
+
+
+ ${this._errorHTML}
+
+
`
: html`
${this.hass.localize("ui.panel.config.logs.load_full_log")}
`}
-
- ${this._errorLog}
+
`;
}
@@ -59,17 +63,42 @@ class ErrorLogCard extends LitElement {
.error-log {
@apply --paper-font-code)
clear: both;
- white-space: pre-wrap;
- margin: 16px;
+ text-align: left;
+ padding-top: 12px;
+ }
+
+ .error {
+ color: var(--error-color);
+ }
+
+ .warning {
+ color: var(--warning-color);
}
`;
}
private async _refreshErrorLog(): Promise {
- this._errorLog = this.hass.localize("ui.panel.config.logs.loading_log");
+ this._errorHTML = this.hass.localize("ui.panel.config.logs.loading_log");
const log = await fetchErrorLog(this.hass!);
- this._errorLog =
- log || this.hass.localize("ui.panel.config.logs.no_errors");
+
+ this._errorHTML = log
+ ? log.split("\n").map((entry) => {
+ if (entry.includes("INFO"))
+ return html`${entry}`;
+
+ if (entry.includes("WARNING"))
+ return html`${entry}`;
+
+ if (
+ entry.includes("ERROR") ||
+ entry.includes("FATAL") ||
+ entry.includes("CRITICAL")
+ )
+ return html`${entry}`;
+
+ return html`${entry}`;
+ })
+ : this.hass.localize("ui.panel.config.logs.no_errors");
}
}
diff --git a/src/panels/config/logs/ha-config-logs.ts b/src/panels/config/logs/ha-config-logs.ts
index 84247d748c..178fa70863 100644
--- a/src/panels/config/logs/ha-config-logs.ts
+++ b/src/panels/config/logs/ha-config-logs.ts
@@ -28,7 +28,7 @@ export class HaConfigLogs extends LitElement {
@property() public route!: Route;
- @query("system-log-card") private systemLog?: SystemLogCard;
+ @query("system-log-card", true) private systemLog?: SystemLogCard;
public connectedCallback() {
super.connectedCallback();
diff --git a/src/panels/config/logs/system-log-card.ts b/src/panels/config/logs/system-log-card.ts
index bd80bbe5bf..dade77f9bb 100644
--- a/src/panels/config/logs/system-log-card.ts
+++ b/src/panels/config/logs/system-log-card.ts
@@ -77,7 +77,9 @@ export class SystemLogCard extends LitElement {
integrations[idx]!
)
: item.source[0]}
- (${item.level})
+ ${html`(${item.level})`}
${item.count > 1
? html`
-
@@ -164,6 +166,14 @@ export class SystemLogCard extends LitElement {
align-items: center;
justify-content: center;
}
+
+ .error {
+ color: var(--error-color);
+ }
+
+ .warning {
+ color: var(--warning-color);
+ }
`;
}
}
diff --git a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts
index 90205aa8a7..2255222781 100644
--- a/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts
+++ b/src/panels/config/lovelace/dashboards/ha-config-lovelace-dashboards.ts
@@ -221,6 +221,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
@row-click=${this._editDashboard}
id="url_path"
hasFab
+ clickable
>
-
+
`;
diff --git a/src/panels/config/lovelace/resources/dialog-lovelace-resource-detail.ts b/src/panels/config/lovelace/resources/dialog-lovelace-resource-detail.ts
index 0603562940..317f05f261 100644
--- a/src/panels/config/lovelace/resources/dialog-lovelace-resource-detail.ts
+++ b/src/panels/config/lovelace/resources/dialog-lovelace-resource-detail.ts
@@ -23,6 +23,20 @@ import { haStyleDialog } from "../../../../resources/styles";
import { HomeAssistant } from "../../../../types";
import { LovelaceResourceDetailsDialogParams } from "./show-dialog-lovelace-resource-detail";
+const detectResourceType = (url: string) => {
+ const ext = url.split(".").pop() || "";
+
+ if (ext === "css") {
+ return "css";
+ }
+
+ if (ext === "js") {
+ return "module";
+ }
+
+ return undefined;
+};
+
@customElement("dialog-lovelace-resource-detail")
export class DialogLovelaceResourceDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -31,7 +45,7 @@ export class DialogLovelaceResourceDetail extends LitElement {
@internalProperty() private _url!: LovelaceResource["url"];
- @internalProperty() private _type!: LovelaceResource["type"];
+ @internalProperty() private _type?: LovelaceResource["type"];
@internalProperty() private _error?: string;
@@ -44,10 +58,10 @@ export class DialogLovelaceResourceDetail extends LitElement {
this._error = undefined;
if (this._params.resource) {
this._url = this._params.resource.url || "";
- this._type = this._params.resource.type || "module";
+ this._type = this._params.resource.type || undefined;
} else {
this._url = "";
- this._type = "module";
+ this._type = undefined;
}
await this.updateComplete;
}
@@ -106,6 +120,7 @@ export class DialogLovelaceResourceDetail extends LitElement {
.selected=${this._type}
@iron-select=${this._typeChanged}
attr-for-selected="type"
+ .invalid=${!this._type}
>
${this.hass!.localize(
@@ -156,7 +171,7 @@ export class DialogLovelaceResourceDetail extends LitElement {
${this._params.resource
? this.hass!.localize(
@@ -173,6 +188,9 @@ export class DialogLovelaceResourceDetail extends LitElement {
private _urlChanged(ev: PolymerChangedEvent) {
this._error = undefined;
this._url = ev.detail.value;
+ if (!this._type) {
+ this._type = detectResourceType(this._url);
+ }
}
private _typeChanged(ev: CustomEvent) {
@@ -180,6 +198,10 @@ export class DialogLovelaceResourceDetail extends LitElement {
}
private async _updateResource() {
+ if (!this._type) {
+ return;
+ }
+
this._submitting = true;
try {
const values: LovelaceResourcesMutableParams = {
diff --git a/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts b/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts
index 5d35134956..5ae5003d5a 100644
--- a/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts
+++ b/src/panels/config/lovelace/resources/ha-config-lovelace-resources.ts
@@ -101,6 +101,7 @@ export class HaConfigLovelaceRescources extends LitElement {
)}
@row-click=${this._editResource}
hasFab
+ clickable
>
-
+
`;
diff --git a/src/panels/config/person/dialog-person-detail.ts b/src/panels/config/person/dialog-person-detail.ts
index 5844a0374f..67802ea928 100644
--- a/src/panels/config/person/dialog-person-detail.ts
+++ b/src/panels/config/person/dialog-person-detail.ts
@@ -14,7 +14,6 @@ import "../../../components/entity/ha-entities-picker";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-picture-upload";
import type { HaPictureUpload } from "../../../components/ha-picture-upload";
-import "../../../components/user/ha-user-picker";
import { PersonMutableParams } from "../../../data/person";
import { CropOptions } from "../../../dialogs/image-cropper-dialog/show-image-cropper-dialog";
import { PolymerChangedEvent } from "../../../polymer-types";
@@ -440,9 +439,6 @@ class DialogPersonDetail extends LitElement {
display: block;
padding: 16px 0;
}
- ha-user-picker {
- margin-top: 16px;
- }
a {
color: var(--primary-color);
}
diff --git a/src/panels/config/person/ha-config-person.ts b/src/panels/config/person/ha-config-person.ts
index 829b12278a..d7403b341a 100644
--- a/src/panels/config/person/ha-config-person.ts
+++ b/src/panels/config/person/ha-config-person.ts
@@ -1,7 +1,7 @@
+import "@material/mwc-fab";
import { mdiPlus } from "@mdi/js";
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
-import "@material/mwc-fab";
import {
css,
CSSResult,
@@ -23,10 +23,14 @@ import {
updatePerson,
} from "../../../data/person";
import { fetchUsers, User } from "../../../data/user";
-import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
+import {
+ showAlertDialog,
+ showConfirmationDialog,
+} from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-tabs-subpage";
import { HomeAssistant, Route } from "../../../types";
+import { documentationUrl } from "../../../util/documentation-url";
import "../ha-config-section";
import { configSections } from "../ha-panel-config";
import {
@@ -71,7 +75,9 @@ class HaConfigPerson extends LitElement {
>${hass.localize("ui.panel.config.person.caption")}
- ${hass.localize("ui.panel.config.person.introduction")}
+
+ ${hass.localize("ui.panel.config.person.introduction")}
+
${this._configItems.length > 0
? html`
@@ -81,7 +87,16 @@ class HaConfigPerson extends LitElement {
`
: ""}
+
+
+ ${this.hass.localize("ui.panel.config.person.learn_more")}
+
+
${this._storageItems.map((entry) => {
return html`
@@ -136,7 +151,7 @@ class HaConfigPerson extends LitElement {
title="${hass.localize("ui.panel.config.person.add_person")}"
@click=${this._createPerson}
>
-
+
`;
@@ -158,6 +173,31 @@ class HaConfigPerson extends LitElement {
this._configItems = personData.config.sort((ent1, ent2) =>
compare(ent1.name, ent2.name)
);
+ this._openDialogIfPersonSpecifiedInRoute();
+ }
+
+ private _openDialogIfPersonSpecifiedInRoute() {
+ if (!this.route.path.includes("/edit/")) {
+ return;
+ }
+
+ const routeSegments = this.route.path.split("/edit/");
+ const personId = routeSegments.length > 1 ? routeSegments[1] : null;
+ if (!personId) {
+ return;
+ }
+
+ const personToEdit = this._storageItems!.find((p) => p.id === personId);
+ if (personToEdit) {
+ this._openDialog(personToEdit);
+ } else {
+ showAlertDialog(this, {
+ title: this.hass?.localize(
+ "ui.panel.config.person.person_not_found_title"
+ ),
+ text: this.hass?.localize("ui.panel.config.person.person_not_found"),
+ });
+ }
}
private _createPerson() {
diff --git a/src/panels/config/scene/ha-scene-dashboard.ts b/src/panels/config/scene/ha-scene-dashboard.ts
index fcea68d01a..18666ee10c 100644
--- a/src/panels/config/scene/ha-scene-dashboard.ts
+++ b/src/panels/config/scene/ha-scene-dashboard.ts
@@ -1,5 +1,6 @@
import "@material/mwc-fab";
-import { mdiPlus } from "@mdi/js";
+import "@material/mwc-icon-button";
+import { mdiPlus, mdiHelpCircle } from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip";
import {
css,
@@ -147,18 +148,16 @@ class HaSceneDashboard extends LitElement {
)}
hasFab
>
-
+
+
+
-
+
diff --git a/src/panels/config/scene/ha-scene-editor.ts b/src/panels/config/scene/ha-scene-editor.ts
index 73f0e29ce3..22bbba718d 100644
--- a/src/panels/config/scene/ha-scene-editor.ts
+++ b/src/panels/config/scene/ha-scene-editor.ts
@@ -57,7 +57,9 @@ import { HomeAssistant, Route } from "../../../types";
import "../ha-config-section";
import { configSections } from "../ha-panel-config";
import "../../../components/ha-svg-icon";
+import { showToast } from "../../../util/toast";
import { mdiContentSave } from "@mdi/js";
+import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
interface DeviceEntities {
id: string;
@@ -70,7 +72,9 @@ interface DeviceEntitiesLookup {
}
@customElement("ha-scene-editor")
-export class HaSceneEditor extends SubscribeMixin(LitElement) {
+export class HaSceneEditor extends SubscribeMixin(
+ KeyboardShortcutMixin(LitElement)
+) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public narrow!: boolean;
@@ -265,7 +269,7 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) {
(device) =>
html`
-
+
${device.name}
-
+
${device.entities.map((entityId) => {
const entityStateObj = this.hass.states[entityId];
if (!entityStateObj) {
@@ -405,7 +409,7 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) {
@click=${this._saveScene}
class=${classMap({ dirty: this._dirty })}
>
-
+
`;
@@ -712,10 +716,17 @@ export class HaSceneEditor extends SubscribeMixin(LitElement) {
}
} catch (err) {
this._errors = err.body.message || err.message;
+ showToast(this, {
+ message: err.body.message || err.message,
+ });
throw err;
}
}
+ protected handleKeyboardSave() {
+ this._saveScene();
+ }
+
static get styles(): CSSResult[] {
return [
haStyle,
diff --git a/src/panels/config/script/ha-script-editor.ts b/src/panels/config/script/ha-script-editor.ts
index 6d248bac05..63dc5bfcb3 100644
--- a/src/panels/config/script/ha-script-editor.ts
+++ b/src/panels/config/script/ha-script-editor.ts
@@ -1,8 +1,10 @@
import "@material/mwc-fab";
-import { mdiContentSave } from "@mdi/js";
+import { mdiCheck, mdiContentSave, mdiDelete, mdiDotsVertical } from "@mdi/js";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
+import "@material/mwc-list/mwc-list-item";
+import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import { PaperListboxElement } from "@polymer/paper-listbox";
import {
css,
@@ -13,16 +15,20 @@ import {
property,
PropertyValues,
TemplateResult,
+ query,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import { computeObjectId } from "../../../common/entity/compute_object_id";
import { navigate } from "../../../common/navigate";
import { slugify } from "../../../common/string/slugify";
import { computeRTL } from "../../../common/util/compute_rtl";
+import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-input";
import "../../../components/ha-svg-icon";
+import "../../../components/ha-yaml-editor";
+import type { HaYamlEditor } from "../../../components/ha-yaml-editor";
import {
Action,
deleteScript,
@@ -34,6 +40,7 @@ import {
} from "../../../data/script";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/ha-app-layout";
+import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
@@ -43,7 +50,7 @@ import { HaDeviceAction } from "../automation/action/types/ha-automation-action-
import "../ha-config-section";
import { configSections } from "../ha-panel-config";
-export class HaScriptEditor extends LitElement {
+export class HaScriptEditor extends KeyboardShortcutMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public scriptEntityId!: string;
@@ -64,6 +71,10 @@ export class HaScriptEditor extends LitElement {
@internalProperty() private _errors?: string;
+ @internalProperty() private _mode: "gui" | "yaml" = "gui";
+
+ @query("ha-yaml-editor", true) private _editor?: HaYamlEditor;
+
protected render(): TemplateResult {
return html`
this._backTapped()}
.tabs=${configSections.automation}
>
- ${!this.scriptEntityId
- ? ""
- : html`
-
- `}
+
+
+
+
+
+ ${this.hass.localize("ui.panel.config.automation.editor.edit_ui")}
+ ${this._mode === "gui"
+ ? html` `
+ : ``}
+
+
+ ${this.hass.localize("ui.panel.config.automation.editor.edit_yaml")}
+ ${this._mode === "yaml"
+ ? html` `
+ : ``}
+
+
+
+
+
+ ${this.hass.localize("ui.panel.config.script.editor.delete_script")}
+
+
+
+
${this.narrow
? html` ${this._config?.alias} `
: ""}
@@ -93,174 +156,226 @@ export class HaScriptEditor extends LitElement {
${this._errors
? html` ${this._errors} `
: ""}
-
- ${this._config
- ? html`
-
- ${!this.narrow
- ? html` ${this._config.alias} `
- : ""}
-
- ${this.hass.localize(
- "ui.panel.config.script.editor.introduction"
- )}
-
-
-
-
-
-
-
- ${!this.scriptEntityId
- ? html`
+ ${this._config
+ ? html`
+
+ ${!this.narrow
+ ? html`
+ ${this._config.alias}
+ `
+ : ""}
+
+ ${this.hass.localize(
+ "ui.panel.config.script.editor.introduction"
+ )}
+
+
+
+
+
+
+
+ ${!this.scriptEntityId
+ ? html`
+ `
+ : ""}
+
+ ${this.hass.localize(
+ "ui.panel.config.script.editor.modes.description",
+ "documentation_link",
+ html`${this.hass.localize(
+ "ui.panel.config.script.editor.modes.documentation"
+ )}`
+ )}
+
+
+
+ ${MODES.map(
+ (mode) => html`
+
+ ${this.hass.localize(
+ `ui.panel.config.script.editor.modes.${mode}`
+ ) || mode}
+
+ `
+ )}
+
+
+ ${this._config.mode &&
+ MODES_MAX.includes(this._config.mode)
+ ? html`
+ `
+ : html``}
+
+ ${this.scriptEntityId
+ ? html`
+
+
+
+ ${this.hass.localize(
+ "ui.card.script.execute"
+ )}
+
+
+ `
+ : ``}
+
+
+
+
+
+ ${this.hass.localize(
+ "ui.panel.config.script.editor.sequence"
+ )}
+
+
+
+ ${this.hass.localize(
+ "ui.panel.config.script.editor.sequence_sentence"
)}
- .errorMessage=${this.hass.localize(
- "ui.panel.config.script.editor.id_already_exists"
- )}
- .invalid=${this._idError}
- .value=${this._entityId}
- @value-changed=${this._idChanged}
- >
-
`
- : ""}
-
- ${this.hass.localize(
- "ui.panel.config.script.editor.modes.description",
- "documentation_link",
- html`
+ ${this.hass.localize(
- "ui.panel.config.script.editor.modes.documentation"
- )}`
- )}
-
-
-
- ${MODES.map(
- (mode) => html`
-
- ${this.hass.localize(
- `ui.panel.config.script.editor.modes.${mode}`
- ) || mode}
-
- `
- )}
-
-
- ${this._config.mode &&
- MODES_MAX.includes(this._config.mode)
- ? html`
+ ${this.hass.localize(
+ "ui.panel.config.script.editor.link_available_actions"
)}
- type="number"
- name="max"
- .value=${this._config.max || "10"}
- @value-changed=${this._valueChanged}
- >
- `
- : html``}
-
- ${this.scriptEntityId
- ? html`
-
-
-
- ${this.hass.localize("ui.card.script.execute")}
-
-
- `
- : ``}
-
-
-
-
-
- ${this.hass.localize(
- "ui.panel.config.script.editor.sequence"
- )}
-
-
-
+
+
+
+
+ `
+ : ""}
+
+ `
+ : this._mode === "yaml"
+ ? html`
+
+ ${!this.narrow
+ ? html`${this._config?.alias}`
+ : ``}
+
+
+
+
${this.hass.localize(
- "ui.panel.config.script.editor.sequence_sentence"
+ "ui.panel.config.automation.editor.copy_to_clipboard"
)}
-
-
- ${this.hass.localize(
- "ui.panel.config.script.editor.link_available_actions"
- )}
-
-
-
-
- `
- : ""}
-
+
+
+ ${this.scriptEntityId
+ ? html`
+
+
+
+ ${this.hass.localize("ui.card.script.execute")}
+
+
+ `
+ : ``}
+
+
+ `
+ : ``}
-
+
`;
@@ -400,6 +515,26 @@ export class HaScriptEditor extends LitElement {
this._dirty = true;
}
+ private _preprocessYaml() {
+ return this._config;
+ }
+
+ private async _copyYaml() {
+ if (this._editor?.yaml) {
+ navigator.clipboard.writeText(this._editor.yaml);
+ }
+ }
+
+ private _yamlChanged(ev: CustomEvent) {
+ ev.stopPropagation();
+ if (!ev.detail.isValid) {
+ return;
+ }
+ this._config = ev.detail.value;
+ this._errors = undefined;
+ this._dirty = true;
+ }
+
private _backTapped(): void {
if (this._dirty) {
showConfirmationDialog(this, {
@@ -429,11 +564,33 @@ export class HaScriptEditor extends LitElement {
history.back();
}
+ private async _handleMenuAction(ev: CustomEvent) {
+ switch (ev.detail.index) {
+ case 0:
+ this._mode = "gui";
+ break;
+ case 1:
+ this._mode = "yaml";
+ break;
+ case 2:
+ this._deleteConfirm();
+ break;
+ }
+ }
+
private _saveScript(): void {
if (this._idError) {
- this._errors = this.hass.localize(
- "ui.panel.config.script.editor.id_already_exists_save_error"
- );
+ showToast(this, {
+ message: this.hass.localize(
+ "ui.panel.config.script.editor.id_already_exists_save_error"
+ ),
+ dismissable: false,
+ duration: 0,
+ action: {
+ action: () => {},
+ text: this.hass.localize("ui.dialogs.generic.ok"),
+ },
+ });
return;
}
const id = this.scriptEntityId
@@ -449,11 +606,18 @@ export class HaScriptEditor extends LitElement {
},
(errors) => {
this._errors = errors.body.message;
+ showToast(this, {
+ message: errors.body.message,
+ });
throw errors;
}
);
}
+ protected handleKeyboardSave() {
+ this._saveScript();
+ }
+
static get styles(): CSSResult[] {
return [
haStyle,
@@ -483,6 +647,12 @@ export class HaScriptEditor extends LitElement {
mwc-fab.dirty {
bottom: 0;
}
+ .selected_menu_item {
+ color: var(--primary-color);
+ }
+ li[role="separator"] {
+ border-bottom-color: var(--divider-color);
+ }
`,
];
}
diff --git a/src/panels/config/script/ha-script-picker.ts b/src/panels/config/script/ha-script-picker.ts
index d7b713596c..3dc94b280c 100644
--- a/src/panels/config/script/ha-script-picker.ts
+++ b/src/panels/config/script/ha-script-picker.ts
@@ -1,3 +1,4 @@
+import "@material/mwc-icon-button";
import "../../../components/ha-icon-button";
import { HassEntity } from "home-assistant-js-websocket";
import {
@@ -23,7 +24,7 @@ import { HomeAssistant, Route } from "../../../types";
import { showToast } from "../../../util/toast";
import { configSections } from "../ha-panel-config";
import "../../../components/ha-svg-icon";
-import { mdiPlus } from "@mdi/js";
+import { mdiPlus, mdiHelpCircle } from "@mdi/js";
import { stateIcon } from "../../../common/entity/state_icon";
import { documentationUrl } from "../../../util/documentation-url";
@@ -61,7 +62,7 @@ class HaScriptPicker extends LitElement {
.script=${script}
icon="hass:play"
title="${this.hass.localize(
- "ui.panel.config.script.picker.activate_script"
+ "ui.panel.config.script.picker.run_script"
)}"
@click=${(ev: Event) => this._runScript(ev)}
>
@@ -141,21 +142,19 @@ class HaScriptPicker extends LitElement {
)}
hasFab
>
-
+
+
+
-
+
diff --git a/src/panels/config/server_control/ha-config-server-control.ts b/src/panels/config/server_control/ha-config-server-control.ts
index 62622baeab..f63cfab076 100644
--- a/src/panels/config/server_control/ha-config-server-control.ts
+++ b/src/panels/config/server_control/ha-config-server-control.ts
@@ -270,7 +270,7 @@ export class HaConfigServerControl extends LitElement {
}
.validate-log {
- white-space: pre-wrap;
+ white-space: pre-line;
direction: ltr;
}
diff --git a/src/panels/config/tags/dialog-tag-detail.ts b/src/panels/config/tags/dialog-tag-detail.ts
index d34ed4e206..6e7cd00218 100644
--- a/src/panels/config/tags/dialog-tag-detail.ts
+++ b/src/panels/config/tags/dialog-tag-detail.ts
@@ -10,6 +10,7 @@ import {
property,
TemplateResult,
} from "lit-element";
+
import { fireEvent } from "../../../common/dom/fire_event";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-formfield";
@@ -21,8 +22,11 @@ import { haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
import { TagDetailDialogParams } from "./show-dialog-tag-detail";
+const QR_LOGO_URL = "/static/icons/favicon-192x192.png";
+
@customElement("dialog-tag-detail")
-class DialogTagDetail extends LitElement implements HassDialog {
+class DialogTagDetail extends LitElement
+ implements HassDialog {
@property({ attribute: false }) public hass!: HomeAssistant;
@internalProperty() private _id?: string;
@@ -35,6 +39,8 @@ class DialogTagDetail extends LitElement implements HassDialog {
@internalProperty() private _submitting = false;
+ @internalProperty() private _qrCode?: TemplateResult;
+
public showDialog(params: TagDetailDialogParams): void {
this._params = params;
this._error = undefined;
@@ -48,6 +54,7 @@ class DialogTagDetail extends LitElement implements HassDialog {
public closeDialog(): void {
this._params = undefined;
+ this._qrCode = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -106,6 +113,36 @@ class DialogTagDetail extends LitElement implements HassDialog {
>`
: ""}
+ ${this._params.entry
+ ? html`
+
+
+ ${this.hass!.localize(
+ "ui.panel.config.tags.detail.usage",
+ "companion_link",
+ html`${this.hass!.localize(
+ "ui.panel.config.tags.detail.companion_apps"
+ )}`
+ )}
+
+
+
+
+ ${this._qrCode
+ ? this._qrCode
+ : html`
+ Generate QR code
+
+ `}
+
+ `
+ : ``}
${this._params.entry
? html`
@@ -191,6 +228,33 @@ class DialogTagDetail extends LitElement implements HassDialog {
}
}
+ private async _generateQR() {
+ const qrcode = await import("qrcode");
+ const canvas = await qrcode.toCanvas(
+ `https://home-assistant.io/tag/${this._params?.entry?.id}`,
+ {
+ width: 180,
+ errorCorrectionLevel: "Q",
+ }
+ );
+ const context = canvas.getContext("2d");
+
+ const imageObj = new Image();
+ imageObj.src = QR_LOGO_URL;
+ await new Promise((resolve) => {
+ imageObj.onload = resolve;
+ });
+ context.drawImage(
+ imageObj,
+ canvas.width / 3,
+ canvas.height / 3,
+ canvas.width / 3,
+ canvas.height / 3
+ );
+
+ this._qrCode = html`
`;
+ }
+
static get styles(): CSSResult[] {
return [
haStyleDialog,
@@ -198,6 +262,9 @@ class DialogTagDetail extends LitElement implements HassDialog {
a {
color: var(--primary-color);
}
+ #qr {
+ text-align: center;
+ }
`,
];
}
diff --git a/src/panels/config/tags/ha-config-tags.ts b/src/panels/config/tags/ha-config-tags.ts
index a0dd7e32b8..2fbe093f43 100644
--- a/src/panels/config/tags/ha-config-tags.ts
+++ b/src/panels/config/tags/ha-config-tags.ts
@@ -1,5 +1,12 @@
import "@material/mwc-fab";
-import { mdiCog, mdiContentDuplicate, mdiPlus, mdiRobot } from "@mdi/js";
+import "@material/mwc-icon-button";
+import {
+ mdiCog,
+ mdiContentDuplicate,
+ mdiHelpCircle,
+ mdiPlus,
+ mdiRobot,
+} from "@mdi/js";
import {
customElement,
html,
@@ -23,11 +30,15 @@ import {
updateTag,
UpdateTagParams,
} from "../../../data/tag";
-import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
+import {
+ showAlertDialog,
+ showConfirmationDialog,
+} from "../../../dialogs/generic/show-dialog-box";
import { getExternalConfig } from "../../../external_app/external_config";
import "../../../layouts/hass-tabs-subpage-data-table";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { HomeAssistant, Route } from "../../../types";
+import { documentationUrl } from "../../../util/documentation-url";
import { configSections } from "../ha-panel-config";
import { showTagDetailDialog } from "./show-dialog-tag-detail";
import "./tag-image";
@@ -193,17 +204,51 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
.noDataText=${this.hass.localize("ui.panel.config.tags.no_tags")}
hasFab
>
+
+
+
-
+
`;
}
+ private _showHelp() {
+ showAlertDialog(this, {
+ title: this.hass.localize("ui.panel.config.tags.caption"),
+ text: html`
+
+ ${this.hass.localize(
+ "ui.panel.config.tags.detail.usage",
+ "companion_link",
+ html`${this.hass!.localize(
+ "ui.panel.config.tags.detail.companion_apps"
+ )}`
+ )}
+
+
+
+ ${this.hass.localize("ui.panel.config.tags.learn_more")}
+
+
+ `,
+ });
+ }
+
private async _fetchTags() {
this._tags = await fetchTags(this.hass);
}
diff --git a/src/panels/config/users/ha-config-users.ts b/src/panels/config/users/ha-config-users.ts
index 30ed2e8093..09ca727a22 100644
--- a/src/panels/config/users/ha-config-users.ts
+++ b/src/panels/config/users/ha-config-users.ts
@@ -96,13 +96,14 @@ export class HaConfigUsers extends LitElement {
.data=${this._users}
@row-click=${this._editUser}
hasFab
+ clickable
>
-
+
`;
diff --git a/src/panels/config/zone/dialog-zone-detail.ts b/src/panels/config/zone/dialog-zone-detail.ts
index 227c1b0ab8..f31f5e0565 100644
--- a/src/panels/config/zone/dialog-zone-detail.ts
+++ b/src/panels/config/zone/dialog-zone-detail.ts
@@ -303,9 +303,6 @@ class DialogZoneDetail extends LitElement {
ha-location-editor {
margin-top: 16px;
}
- ha-user-picker {
- margin-top: 16px;
- }
a {
color: var(--primary-color);
}
diff --git a/src/panels/config/zone/ha-config-zone.ts b/src/panels/config/zone/ha-config-zone.ts
index 83910d3c15..6f4f277ab6 100644
--- a/src/panels/config/zone/ha-config-zone.ts
+++ b/src/panels/config/zone/ha-config-zone.ts
@@ -260,7 +260,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
title=${hass.localize("ui.panel.config.zone.add_zone")}
@click=${this._createZone}
>
-
+
`;
diff --git a/src/panels/custom/ha-panel-custom.ts b/src/panels/custom/ha-panel-custom.ts
index 028cd9b227..e64476fef6 100644
--- a/src/panels/custom/ha-panel-custom.ts
+++ b/src/panels/custom/ha-panel-custom.ts
@@ -24,7 +24,7 @@ export class HaPanelCustom extends UpdatingElement {
@property() public panel!: CustomPanelInfo;
- private _setProperties?: (props: {}) => void | undefined;
+ private _setProperties?: (props: Record) => void | undefined;
// Since navigate fires events on `window`, we need to expose this as a function
// to allow custom panels to forward their location changes to the main window
diff --git a/src/panels/developer-tools/ha-panel-developer-tools.ts b/src/panels/developer-tools/ha-panel-developer-tools.ts
index 1a71602c8b..e8c8317e17 100644
--- a/src/panels/developer-tools/ha-panel-developer-tools.ts
+++ b/src/panels/developer-tools/ha-panel-developer-tools.ts
@@ -3,7 +3,7 @@ import "@polymer/app-layout/app-toolbar/app-toolbar";
import "../../layouts/ha-app-layout";
import "../../components/ha-icon-button";
import "@polymer/paper-tabs/paper-tab";
-import "@polymer/paper-tabs/paper-tabs";
+import "../../components/ha-tabs";
import {
css,
CSSResultArray,
@@ -44,7 +44,7 @@ class PanelDeveloperTools extends LitElement {
>
${this.hass.localize("panel.developer_tools")}
-
-
+
@@ -146,58 +145,59 @@ class HaPanelDevService extends LocalizeMixin(PolymerElement) {
-
-
- [[localize('ui.panel.developer-tools.tabs.services.select_service')]]
-
-
+
+
+
+ [[localize('ui.panel.developer-tools.tabs.services.select_service')]]
+
-
-
-
- [[localize('ui.panel.developer-tools.tabs.services.no_description')]]
-
-
-
-
- [[_description]]
-
-
-
-
-
- [[localize('ui.panel.developer-tools.tabs.services.column_parameter')]]
-
-
- [[localize('ui.panel.developer-tools.tabs.services.column_description')]]
-
-
- [[localize('ui.panel.developer-tools.tabs.services.column_example')]]
-
-
+
+
+ [[localize('ui.panel.developer-tools.tabs.services.no_description')]]
+
+
+ [[_description]]
+
+
+
+
+
-
-
- [[localize('ui.panel.developer-tools.tabs.services.no_parameters')]]
-
-
+ [[localize('ui.panel.developer-tools.tabs.services.no_parameters')]]
-
-
- [[attribute.key]]
- [[attribute.description]]
- [[attribute.example]]
-
-
-
-
-
- [[localize('ui.panel.developer-tools.tabs.services.fill_example_data')]]
-
+
+
+
+
+ [[localize('ui.panel.developer-tools.tabs.services.column_parameter')]]
+
+
+ [[localize('ui.panel.developer-tools.tabs.services.column_description')]]
+
+
+ [[localize('ui.panel.developer-tools.tabs.services.column_example')]]
+
+
+
+
+ [[attribute.key]]
+ [[attribute.description]]
+ [[attribute.example]]
+
+
+
+
+
+
+
+ [[localize('ui.panel.developer-tools.tabs.services.fill_example_data')]]
+
+
-
+
+
`;
}
@@ -247,6 +247,7 @@ class HaPanelDevService extends LocalizeMixin(PolymerElement) {
type: String,
computed: "_computeDescription(hass, _domain, _service)",
},
+
rtl: {
reflectToAttribute: true,
computed: "_computeRTL(hass)",
@@ -276,7 +277,7 @@ class HaPanelDevService extends LocalizeMixin(PolymerElement) {
return serviceDomains[domain][service].description;
}
- _computeServicedataKey(domainService) {
+ _computeServiceDataKey(domainService) {
return `panel-dev-service-state-servicedata.${domainService}`;
}
diff --git a/src/panels/developer-tools/state/developer-tools-state.js b/src/panels/developer-tools/state/developer-tools-state.js
index 19b55d1496..530ae3f736 100644
--- a/src/panels/developer-tools/state/developer-tools-state.js
+++ b/src/panels/developer-tools/state/developer-tools-state.js
@@ -237,6 +237,7 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
computed:
"computeEntities(hass, _entityFilter, _stateFilter, _attributeFilter)",
},
+
rtl: {
reflectToAttribute: true,
computed: "_computeRTL(hass)",
diff --git a/src/panels/developer-tools/template/developer-tools-template.ts b/src/panels/developer-tools/template/developer-tools-template.ts
index f7d3c39efa..a44755deb5 100644
--- a/src/panels/developer-tools/template/developer-tools-template.ts
+++ b/src/panels/developer-tools/template/developer-tools-template.ts
@@ -151,24 +151,33 @@ class HaPanelDevTemplate extends LitElement {
class="rendered ${classMap({ error: Boolean(this._error) })}"
>${this._error}${this._templateResult
?.result}
+ ${this._templateResult?.listeners.time
+ ? html`
+
+ ${this.hass.localize(
+ "ui.panel.developer-tools.tabs.templates.time"
+ )}
+
+ `
+ : ""}
${!this._templateResult?.listeners
? ""
: this._templateResult.listeners.all
? html`
-
+
${this.hass.localize(
"ui.panel.developer-tools.tabs.templates.all_listeners"
)}
-
+
`
: this._templateResult.listeners.domains.length ||
this._templateResult.listeners.entities.length
? html`
-
+
${this.hass.localize(
"ui.panel.developer-tools.tabs.templates.listeners"
)}
-
+
${this._templateResult.listeners.domains
.sort()
diff --git a/src/panels/history/ha-panel-history.ts b/src/panels/history/ha-panel-history.ts
index 8b59281ad6..566aa3c0da 100644
--- a/src/panels/history/ha-panel-history.ts
+++ b/src/panels/history/ha-panel-history.ts
@@ -16,6 +16,7 @@ import { haStyle } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import type { DateRangePickerRanges } from "../../components/ha-date-range-picker";
import "../../components/ha-date-range-picker";
+import "../../components/entity/ha-entity-picker";
import { fetchDate, computeHistory } from "../../data/history";
import "../../components/ha-circular-progress";
@@ -77,6 +78,16 @@ class HaPanelHistory extends LitElement {
.ranges=${this._ranges}
@change=${this._dateRangeChanged}
>
+
+
${this._isLoading
? html`
@@ -170,7 +181,8 @@ class HaPanelHistory extends LitElement {
const dateHistory = await fetchDate(
this.hass,
this._startDate,
- this._endDate
+ this._endDate,
+ this._entityId
);
this._stateHistory = computeHistory(
this.hass,
@@ -191,6 +203,10 @@ class HaPanelHistory extends LitElement {
this._endDate = endDate;
}
+ private _entityPicked(ev) {
+ this._entityId = ev.target.value;
+ }
+
static get styles() {
return [
haStyle,
@@ -211,12 +227,32 @@ class HaPanelHistory extends LitElement {
position: relative;
}
+ ha-date-range-picker {
+ margin-right: 16px;
+ max-width: 100%;
+ }
+
+ :host([narrow]) ha-date-range-picker {
+ margin-right: 0;
+ }
+
ha-circular-progress {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
+
+ ha-entity-picker {
+ display: inline-block;
+ flex-grow: 1;
+ max-width: 400px;
+ }
+
+ :host([narrow]) ha-entity-picker {
+ max-width: none;
+ width: 100%;
+ }
`,
];
}
diff --git a/src/panels/iframe/ha-panel-iframe.js b/src/panels/iframe/ha-panel-iframe.js
index b7ebf03359..28d220de25 100644
--- a/src/panels/iframe/ha-panel-iframe.js
+++ b/src/panels/iframe/ha-panel-iframe.js
@@ -13,7 +13,7 @@ class HaPanelIframe extends PolymerElement {
border: 0;
width: 100%;
position: absolute;
- height: calc(100% - 64px);
+ height: calc(100% - var(--header-height));
background-color: var(--primary-background-color);
}
diff --git a/src/panels/logbook/ha-panel-logbook.ts b/src/panels/logbook/ha-panel-logbook.ts
index e3542ba572..87e1b1f519 100644
--- a/src/panels/logbook/ha-panel-logbook.ts
+++ b/src/panels/logbook/ha-panel-logbook.ts
@@ -86,7 +86,7 @@ export class HaPanelLogbook extends LitElement {
@click=${this._refreshLogbook}
.disabled=${this._isLoading}
>
-
+
@@ -327,9 +327,6 @@ export class HaPanelLogbook extends LitElement {
display: inline-block;
flex-grow: 1;
max-width: 400px;
- --paper-input-suffix: {
- height: 24px;
- }
}
:host([narrow]) ha-entity-picker {
diff --git a/src/panels/lovelace/cards/hui-alarm-panel-card.ts b/src/panels/lovelace/cards/hui-alarm-panel-card.ts
index 639c5c705d..634941cfcf 100644
--- a/src/panels/lovelace/cards/hui-alarm-panel-card.ts
+++ b/src/panels/lovelace/cards/hui-alarm-panel-card.ts
@@ -78,14 +78,14 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
public async getCardSize(): Promise {
if (!this._config || !this.hass) {
- return 5;
+ return 9;
}
const stateObj = this.hass.states[this._config.entity];
return !stateObj || stateObj.attributes.code_format !== FORMAT_NUMBER
- ? 3
- : 8;
+ ? 4
+ : 9;
}
public setConfig(config: AlarmPanelCardConfig): void {
@@ -270,6 +270,8 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard {
ha-card {
padding-bottom: 16px;
position: relative;
+ height: 100%;
+ box-sizing: border-box;
--alarm-color-disarmed: var(--label-badge-green);
--alarm-color-pending: var(--label-badge-yellow);
--alarm-color-triggered: var(--label-badge-red);
diff --git a/src/panels/lovelace/cards/hui-button-card.ts b/src/panels/lovelace/cards/hui-button-card.ts
index 2d75c7e33b..c7fd8ba8ab 100644
--- a/src/panels/lovelace/cards/hui-button-card.ts
+++ b/src/panels/lovelace/cards/hui-button-card.ts
@@ -80,7 +80,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
public getCardSize(): number {
return (
- (this._config?.show_icon ? 3 : 0) + (this._config?.show_name ? 1 : 0)
+ (this._config?.show_icon ? 4 : 0) + (this._config?.show_name ? 1 : 0)
);
}
diff --git a/src/panels/lovelace/cards/hui-calendar-card.ts b/src/panels/lovelace/cards/hui-calendar-card.ts
index 625e915fac..9e373f2f19 100644
--- a/src/panels/lovelace/cards/hui-calendar-card.ts
+++ b/src/panels/lovelace/cards/hui-calendar-card.ts
@@ -101,7 +101,7 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard {
}
public getCardSize(): number {
- return 4;
+ return this._config?.header ? 1 : 0 + 11;
}
public connectedCallback(): void {
@@ -208,6 +208,8 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard {
ha-card {
position: relative;
padding: 0 8px 8px;
+ box-sizing: border-box;
+ height: 100%;
}
.header {
diff --git a/src/panels/lovelace/cards/hui-entities-card.ts b/src/panels/lovelace/cards/hui-entities-card.ts
index 81080552d6..d23d32cce8 100644
--- a/src/panels/lovelace/cards/hui-entities-card.ts
+++ b/src/panels/lovelace/cards/hui-entities-card.ts
@@ -96,7 +96,7 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
}
// +1 for the header
let size =
- (this._config.title || this._showHeaderToggle ? 1 : 0) +
+ (this._config.title || this._showHeaderToggle ? 2 : 0) +
(this._config.entities.length || 1);
if (this._headerElement) {
const headerSize = computeCardSize(this._headerElement);
@@ -186,7 +186,7 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
${!this._config.title && !this._showHeaderToggle && !this._config.icon
? ""
: html`
-
+
${this._config.icon
? html`
@@ -204,11 +204,11 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
!("type" in conf)
+ (conf) => "entity" in conf
) as EntityConfig[]).map((conf) => conf.entity)}
>
`}
-
+
`}
${this._configEntities!.map((entityConf) =>
diff --git a/src/panels/lovelace/cards/hui-gauge-card.ts b/src/panels/lovelace/cards/hui-gauge-card.ts
index 686f36a480..c6ce2aa379 100644
--- a/src/panels/lovelace/cards/hui-gauge-card.ts
+++ b/src/panels/lovelace/cards/hui-gauge-card.ts
@@ -4,26 +4,25 @@ import {
CSSResult,
customElement,
html,
+ internalProperty,
LitElement,
property,
- internalProperty,
PropertyValues,
TemplateResult,
} from "lit-element";
-
+import { styleMap } from "lit-html/directives/style-map";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { isValidEntityId } from "../../../common/entity/valid_entity_id";
import "../../../components/ha-card";
+import "../../../components/ha-gauge";
import type { HomeAssistant } from "../../../types";
import { findEntities } from "../common/find-entites";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import type { LovelaceCard, LovelaceCardEditor } from "../types";
import type { GaugeCardConfig } from "./types";
-import "../../../components/ha-gauge";
-import { styleMap } from "lit-html/directives/style-map";
export const severityMap = {
red: "var(--label-badge-red)",
@@ -69,7 +68,7 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
@internalProperty() private _config?: GaugeCardConfig;
public getCardSize(): number {
- return 2;
+ return 4;
}
public setConfig(config: GaugeCardConfig): void {
@@ -195,10 +194,6 @@ class HuiGaugeCard extends LitElement implements LovelaceCard {
static get styles(): CSSResult {
return css`
- :host {
- display: block;
- }
-
ha-card {
cursor: pointer;
height: 100%;
diff --git a/src/panels/lovelace/cards/hui-glance-card.ts b/src/panels/lovelace/cards/hui-glance-card.ts
index 0da08a86a1..f3e9fb7f55 100644
--- a/src/panels/lovelace/cards/hui-glance-card.ts
+++ b/src/panels/lovelace/cards/hui-glance-card.ts
@@ -73,13 +73,14 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
public getCardSize(): number {
const rowHeight =
(this._config!.show_icon ? 1 : 0) +
- (this._config!.show_name && this._config!.show_state ? 1 : 0) || 1;
+ (this._config!.show_name ? 1 : 0) +
+ (this._config!.show_state ? 1 : 0);
const numRows = Math.ceil(
this._configEntities!.length / (this._config!.columns || 5)
);
- return (this._config!.title ? 1 : 0) + rowHeight * numRows;
+ return (this._config!.title ? 2 : 0) + rowHeight * numRows;
}
public setConfig(config: GlanceCardConfig): void {
@@ -189,10 +190,16 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
static get styles(): CSSResult {
return css`
+ ha-card {
+ height: 100%;
+ }
.entities {
display: flex;
padding: 0 16px 4px;
flex-wrap: wrap;
+ height: 100%;
+ box-sizing: border-box;
+ align-content: center;
}
.entities.no-header {
padding-top: 16px;
diff --git a/src/panels/lovelace/cards/hui-history-graph-card.ts b/src/panels/lovelace/cards/hui-history-graph-card.ts
index e49ebc1ed2..57b932c2db 100644
--- a/src/panels/lovelace/cards/hui-history-graph-card.ts
+++ b/src/panels/lovelace/cards/hui-history-graph-card.ts
@@ -3,24 +3,25 @@ import {
CSSResult,
customElement,
html,
+ internalProperty,
LitElement,
property,
- internalProperty,
PropertyValues,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
+import { throttle } from "../../../common/util/throttle";
import "../../../components/ha-card";
import "../../../components/state-history-charts";
import { CacheConfig, getRecentWithCache } from "../../../data/cached-history";
+import { HistoryResult } from "../../../data/history";
import { HomeAssistant } from "../../../types";
import { findEntities } from "../common/find-entites";
+import { hasConfigOrEntitiesChanged } from "../common/has-changed";
import { processConfigEntities } from "../common/process-config-entities";
import { EntityConfig } from "../entity-rows/types";
import { LovelaceCard } from "../types";
import { HistoryGraphCardConfig } from "./types";
-import { HistoryResult } from "../../../data/history";
-import { hasConfigOrEntitiesChanged } from "../common/has-changed";
@customElement("hui-history-graph-card")
export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
@@ -63,10 +64,12 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
private _fetching = false;
- private _date?: Date;
+ private _throttleGetStateHistory?: () => void;
public getCardSize(): number {
- return 4;
+ return this._config?.title
+ ? 2
+ : 0 + 2 * (this._configEntities?.length || 1);
}
public setConfig(config: HistoryGraphCardConfig): void {
@@ -92,10 +95,13 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
}
});
+ this._throttleGetStateHistory = throttle(() => {
+ this._getStateHistory();
+ }, config.refresh_interval || 10 * 1000);
+
this._cacheConfig = {
cacheKey: _entities.join(),
hoursToShow: config.hours_to_show || 24,
- refresh: config.refresh_interval || 0,
};
}
@@ -105,7 +111,12 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
- if (!this._config || !this.hass || !this._cacheConfig) {
+ if (
+ !this._config ||
+ !this.hass ||
+ !this._throttleGetStateHistory ||
+ !this._cacheConfig
+ ) {
return;
}
@@ -113,15 +124,19 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
return;
}
- const oldConfig = changedProps.get("_config") as HistoryGraphCardConfig;
+ const oldConfig = changedProps.get("_config") as
+ | HistoryGraphCardConfig
+ | undefined;
- if (changedProps.has("_config") && oldConfig !== this._config) {
- this._getStateHistory();
- } else if (
- this._cacheConfig.refresh &&
- Date.now() - this._date!.getTime() >= this._cacheConfig.refresh * 100
+ if (
+ changedProps.has("_config") &&
+ (oldConfig?.entities !== this._config.entities ||
+ oldConfig?.hours_to_show !== this._config.hours_to_show)
) {
- this._getStateHistory();
+ this._throttleGetStateHistory();
+ } else if (changedProps.has("hass")) {
+ // wait for commit of data (we only account for the default setting of 1 sec)
+ setTimeout(this._throttleGetStateHistory, 1000);
}
}
@@ -154,7 +169,6 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
if (this._fetching) {
return;
}
- this._date = new Date();
this._fetching = true;
try {
this._stateHistory = {
@@ -173,6 +187,10 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard {
static get styles(): CSSResult {
return css`
+ ha-card {
+ height: 100%;
+ overflow-y: auto;
+ }
.content {
padding: 16px;
}
diff --git a/src/panels/lovelace/cards/hui-horizontal-stack-card.ts b/src/panels/lovelace/cards/hui-horizontal-stack-card.ts
index 22b2cd4c49..2ae05adaf1 100644
--- a/src/panels/lovelace/cards/hui-horizontal-stack-card.ts
+++ b/src/panels/lovelace/cards/hui-horizontal-stack-card.ts
@@ -25,6 +25,7 @@ class HuiHorizontalStackCard extends HuiStackCard {
css`
#root {
display: flex;
+ height: 100%;
}
#root > * {
flex: 1 1 0;
diff --git a/src/panels/lovelace/cards/hui-humidifier-card.ts b/src/panels/lovelace/cards/hui-humidifier-card.ts
index 2f8ba20d7c..614e359c05 100644
--- a/src/panels/lovelace/cards/hui-humidifier-card.ts
+++ b/src/panels/lovelace/cards/hui-humidifier-card.ts
@@ -1,4 +1,3 @@
-import "../../../components/ha-icon-button";
import "@thomasloven/round-slider";
import { HassEntity } from "home-assistant-js-websocket";
import {
@@ -6,9 +5,9 @@ import {
CSSResult,
customElement,
html,
+ internalProperty,
LitElement,
property,
- internalProperty,
PropertyValues,
svg,
TemplateResult,
@@ -18,8 +17,9 @@ import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { computeRTLDirection } from "../../../common/util/compute_rtl";
import "../../../components/ha-card";
-import { HumidifierEntity } from "../../../data/humidifier";
+import "../../../components/ha-icon-button";
import { UNAVAILABLE_STATES } from "../../../data/entity";
+import { HumidifierEntity } from "../../../data/humidifier";
import { HomeAssistant } from "../../../types";
import { findEntities } from "../common/find-entites";
import { hasConfigOrEntityChanged } from "../common/has-changed";
@@ -61,7 +61,7 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
@internalProperty() private _setHum?: number;
public getCardSize(): number {
- return 5;
+ return 6;
}
public setConfig(config: HumidifierCardConfig): void {
diff --git a/src/panels/lovelace/cards/hui-iframe-card.ts b/src/panels/lovelace/cards/hui-iframe-card.ts
index 3e464d76d4..c2fd589e03 100644
--- a/src/panels/lovelace/cards/hui-iframe-card.ts
+++ b/src/panels/lovelace/cards/hui-iframe-card.ts
@@ -40,7 +40,7 @@ export class HuiIframeCard extends LitElement implements LovelaceCard {
public getCardSize(): number {
if (!this._config) {
- return 3;
+ return 5;
}
const aspectRatio = this._config.aspect_ratio
? Number(this._config.aspect_ratio.replace("%", ""))
diff --git a/src/panels/lovelace/cards/hui-light-card.ts b/src/panels/lovelace/cards/hui-light-card.ts
index 0a9bc908b2..f05168c0dc 100644
--- a/src/panels/lovelace/cards/hui-light-card.ts
+++ b/src/panels/lovelace/cards/hui-light-card.ts
@@ -68,7 +68,7 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
private _brightnessTimout?: number;
public getCardSize(): number {
- return 4;
+ return 5;
}
public setConfig(config: LightCardConfig): void {
@@ -99,7 +99,7 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
}
const brightness =
- Math.round((stateObj.attributes.brightness / 254) * 100) || 0;
+ Math.round((stateObj.attributes.brightness / 255) * 100) || 0;
return html`
@@ -116,7 +116,8 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
${this._elements}
@@ -149,6 +151,8 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
ha-card {
overflow: hidden;
+ height: 100%;
+ box-sizing: border-box;
}
`;
}
diff --git a/src/panels/lovelace/cards/hui-picture-entity-card.ts b/src/panels/lovelace/cards/hui-picture-entity-card.ts
index 60b1384670..d20181536f 100644
--- a/src/panels/lovelace/cards/hui-picture-entity-card.ts
+++ b/src/panels/lovelace/cards/hui-picture-entity-card.ts
@@ -3,9 +3,9 @@ import {
CSSResult,
customElement,
html,
+ internalProperty,
LitElement,
property,
- internalProperty,
PropertyValues,
TemplateResult,
} from "lit-element";
@@ -139,9 +139,9 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
`;
} else if (this._config.show_name) {
- footer = html` `;
+ footer = html` `;
} else if (this._config.show_state) {
- footer = html` `;
+ footer = html` `;
}
return html`
@@ -182,6 +182,8 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
min-height: 75px;
overflow: hidden;
position: relative;
+ height: 100%;
+ box-sizing: border-box;
}
hui-image.clickable {
diff --git a/src/panels/lovelace/cards/hui-picture-glance-card.ts b/src/panels/lovelace/cards/hui-picture-glance-card.ts
index 0c04eeccf3..b5a6ed9e1f 100644
--- a/src/panels/lovelace/cards/hui-picture-glance-card.ts
+++ b/src/panels/lovelace/cards/hui-picture-glance-card.ts
@@ -266,7 +266,7 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
`}
>
${this._config!.show_state !== true && entityConf.show_state !== true
- ? html` `
+ ? html``
: html`
${entityConf.attribute
@@ -297,6 +297,8 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
position: relative;
min-height: 48px;
overflow: hidden;
+ height: 100%;
+ box-sizing: border-box;
}
hui-image.clickable {
diff --git a/src/panels/lovelace/cards/hui-plant-status-card.ts b/src/panels/lovelace/cards/hui-plant-status-card.ts
index 18ecd2a0ed..d263c26b80 100644
--- a/src/panels/lovelace/cards/hui-plant-status-card.ts
+++ b/src/panels/lovelace/cards/hui-plant-status-card.ts
@@ -4,9 +4,9 @@ import {
CSSResult,
customElement,
html,
+ internalProperty,
LitElement,
property,
- internalProperty,
PropertyValues,
TemplateResult,
} from "lit-element";
@@ -19,9 +19,9 @@ import { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { findEntities } from "../common/find-entites";
import { hasConfigOrEntityChanged } from "../common/has-changed";
+import { createEntityNotFoundWarning } from "../components/hui-warning";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { PlantAttributeTarget, PlantStatusCardConfig } from "./types";
-import { createEntityNotFoundWarning } from "../components/hui-warning";
const SENSORS = {
moisture: "hass:water",
@@ -163,6 +163,10 @@ class HuiPlantStatusCard extends LitElement implements LovelaceCard {
static get styles(): CSSResult {
return css`
+ ha-card {
+ height: 100%;
+ box-sizing: border-box;
+ }
.banner {
display: flex;
align-items: flex-end;
diff --git a/src/panels/lovelace/cards/hui-shopping-list-card.ts b/src/panels/lovelace/cards/hui-shopping-list-card.ts
index fc1518c945..7fcc865d8b 100644
--- a/src/panels/lovelace/cards/hui-shopping-list-card.ts
+++ b/src/panels/lovelace/cards/hui-shopping-list-card.ts
@@ -1,13 +1,14 @@
import "@polymer/paper-checkbox/paper-checkbox";
import { PaperInputElement } from "@polymer/paper-input/paper-input";
+import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
CSSResult,
customElement,
html,
+ internalProperty,
LitElement,
property,
- internalProperty,
PropertyValues,
TemplateResult,
} from "lit-element";
@@ -23,11 +24,10 @@ import {
ShoppingListItem,
updateItem,
} from "../../../data/shopping-list";
+import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../../types";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { SensorCardConfig, ShoppingListCardConfig } from "./types";
-import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
-import { UnsubscribeFunc } from "home-assistant-js-websocket";
@customElement("hui-shopping-list-card")
class HuiShoppingListCard extends SubscribeMixin(LitElement)
@@ -52,7 +52,7 @@ class HuiShoppingListCard extends SubscribeMixin(LitElement)
@internalProperty() private _checkedItems?: ShoppingListItem[];
public getCardSize(): number {
- return (this._config ? (this._config.title ? 1 : 0) : 0) + 3;
+ return (this._config ? (this._config.title ? 2 : 0) : 0) + 3;
}
public setConfig(config: ShoppingListCardConfig): void {
@@ -254,6 +254,8 @@ class HuiShoppingListCard extends SubscribeMixin(LitElement)
return css`
ha-card {
padding: 16px;
+ height: 100%;
+ box-sizing: border-box;
}
.has-header {
diff --git a/src/panels/lovelace/cards/hui-stack-card.ts b/src/panels/lovelace/cards/hui-stack-card.ts
index 0f6d0ebb71..dfe7a0ec6f 100644
--- a/src/panels/lovelace/cards/hui-stack-card.ts
+++ b/src/panels/lovelace/cards/hui-stack-card.ts
@@ -22,7 +22,7 @@ export abstract class HuiStackCard extends LitElement implements LovelaceCard {
return document.createElement("hui-stack-card-editor");
}
- public static getStubConfig(): object {
+ public static getStubConfig(): Record {
return { cards: [] };
}
@@ -75,7 +75,7 @@ export abstract class HuiStackCard extends LitElement implements LovelaceCard {
return html`
${this._config.title
- ? html` ${this._config.title} `
+ ? html`${this._config.title}
`
: ""}
${this._cards}
`;
diff --git a/src/panels/lovelace/cards/hui-starting-card.ts b/src/panels/lovelace/cards/hui-starting-card.ts
index 668cbce764..883cca483c 100644
--- a/src/panels/lovelace/cards/hui-starting-card.ts
+++ b/src/panels/lovelace/cards/hui-starting-card.ts
@@ -57,7 +57,7 @@ export class HuiStartingCard extends LitElement implements LovelaceCard {
return css`
:host {
display: block;
- height: calc(100vh - 64px);
+ height: calc(100vh - var(--header-height));
}
ha-circular-progress {
padding-bottom: 20px;
diff --git a/src/panels/lovelace/cards/hui-thermostat-card.ts b/src/panels/lovelace/cards/hui-thermostat-card.ts
index 8470bd4c34..19e1ca9e95 100644
--- a/src/panels/lovelace/cards/hui-thermostat-card.ts
+++ b/src/panels/lovelace/cards/hui-thermostat-card.ts
@@ -82,7 +82,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
@query("ha-card") private _card?: HaCard;
public getCardSize(): number {
- return 5;
+ return 7;
}
public setConfig(config: ThermostatCardConfig): void {
diff --git a/src/panels/lovelace/cards/hui-vertical-stack-card.ts b/src/panels/lovelace/cards/hui-vertical-stack-card.ts
index fea2bc7bf0..81ac8dd930 100644
--- a/src/panels/lovelace/cards/hui-vertical-stack-card.ts
+++ b/src/panels/lovelace/cards/hui-vertical-stack-card.ts
@@ -26,6 +26,7 @@ class HuiVerticalStackCard extends HuiStackCard {
#root {
display: flex;
flex-direction: column;
+ height: 100%;
}
#root > * {
margin: 4px 0 4px 0;
diff --git a/src/panels/lovelace/cards/hui-weather-forecast-card.ts b/src/panels/lovelace/cards/hui-weather-forecast-card.ts
index 98726230bc..36c4bd8d1b 100644
--- a/src/panels/lovelace/cards/hui-weather-forecast-card.ts
+++ b/src/panels/lovelace/cards/hui-weather-forecast-card.ts
@@ -3,9 +3,9 @@ import {
CSSResult,
customElement,
html,
+ internalProperty,
LitElement,
property,
- internalProperty,
PropertyValues,
TemplateResult,
} from "lit-element";
@@ -21,18 +21,20 @@ import "../../../components/ha-icon";
import { UNAVAILABLE } from "../../../data/entity";
import {
getSecondaryWeatherAttribute,
- getWeatherUnit,
getWeatherStateIcon,
+ getWeatherUnit,
+ getWind,
+ weatherAttrIcons,
weatherSVGStyles,
} from "../../../data/weather";
import type { HomeAssistant, WeatherEntity } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { findEntities } from "../common/find-entites";
import { hasConfigOrEntityChanged } from "../common/has-changed";
+import { installResizeObserver } from "../common/install-resize-observer";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import type { LovelaceCard, LovelaceCardEditor } from "../types";
import type { WeatherForecastCardConfig } from "./types";
-import { installResizeObserver } from "../common/install-resize-observer";
const DAY_IN_MILLISECONDS = 86400000;
@@ -84,7 +86,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
}
public getCardSize(): number {
- return this._config?.show_forecast !== false ? 4 : 2;
+ return this._config?.show_forecast !== false ? 5 : 2;
}
public setConfig(config: WeatherForecastCardConfig): void {
@@ -221,16 +223,34 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
${this._config.secondary_info_attribute !== undefined
? html`
- ${this.hass!.localize(
- `ui.card.weather.attributes.${this._config.secondary_info_attribute}`
- )}
- ${stateObj.attributes[
- this._config.secondary_info_attribute
- ]}
- ${getWeatherUnit(
- this.hass,
- this._config.secondary_info_attribute
- )}
+ ${this._config.secondary_info_attribute in
+ weatherAttrIcons
+ ? html`
+
+ `
+ : this.hass!.localize(
+ `ui.card.weather.attributes.${this._config.secondary_info_attribute}`
+ )}
+ ${this._config.secondary_info_attribute === "wind_speed"
+ ? getWind(
+ this.hass,
+ stateObj.attributes.wind_speed,
+ stateObj.attributes.wind_bearing
+ )
+ : html`
+ ${stateObj.attributes[
+ this._config.secondary_info_attribute
+ ]}
+ ${getWeatherUnit(
+ this.hass,
+ this._config.secondary_info_attribute
+ )}
+ `}
`
: getSecondaryWeatherAttribute(this.hass, stateObj)}
@@ -334,31 +354,38 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
return;
}
- if (this.offsetWidth < 375) {
+ const card = this.shadowRoot!.querySelector("ha-card");
+ // If we show an error or warning there is no ha-card
+ if (!card) {
+ return;
+ }
+
+ if (card.offsetWidth < 375) {
this.setAttribute("narrow", "");
} else {
this.removeAttribute("narrow");
}
- if (this.offsetWidth < 300) {
+ if (card.offsetWidth < 300) {
this.setAttribute("verynarrow", "");
} else {
this.removeAttribute("verynarrow");
}
- this._veryVeryNarrow = this.offsetWidth < 245;
+ this._veryVeryNarrow = card.offsetWidth < 245;
}
static get styles(): CSSResult[] {
return [
weatherSVGStyles,
css`
- :host {
- display: block;
- }
-
ha-card {
cursor: pointer;
- padding: 16px;
outline: none;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ padding: 16px;
+ box-sizing: border-box;
}
.content {
@@ -470,6 +497,10 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
--mdc-icon-size: 40px;
}
+ .attr-icon {
+ --mdc-icon-size: 20px;
+ }
+
.attribute,
.templow,
.daynight,
diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts
index 78997f8b31..6ecb657ad5 100644
--- a/src/panels/lovelace/cards/types.ts
+++ b/src/panels/lovelace/cards/types.ts
@@ -50,7 +50,7 @@ export interface EntitiesCardEntityConfig extends EntityConfig {
| "brightness";
action_name?: string;
service?: string;
- service_data?: object;
+ service_data?: Record;
url?: string;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
@@ -212,12 +212,14 @@ export interface PictureElementsCardConfig extends LovelaceCardConfig {
image?: string;
camera_image?: string;
camera_view?: HuiImage["cameraView"];
- state_image?: {};
+ state_image?: Record;
state_filter?: string[];
aspect_ratio?: string;
entity?: string;
elements: LovelaceElementConfig[];
theme?: string;
+ dark_mode_image?: string;
+ dark_mode_filter?: string;
}
export interface PictureEntityCardConfig extends LovelaceCardConfig {
@@ -226,7 +228,7 @@ export interface PictureEntityCardConfig extends LovelaceCardConfig {
image?: string;
camera_image?: string;
camera_view?: HuiImage["cameraView"];
- state_image?: {};
+ state_image?: Record;
state_filter?: string[];
aspect_ratio?: string;
tap_action?: ActionConfig;
@@ -243,7 +245,7 @@ export interface PictureGlanceCardConfig extends LovelaceCardConfig {
image?: string;
camera_image?: string;
camera_view?: HuiImage["cameraView"];
- state_image?: {};
+ state_image?: Record;
state_filter?: string[];
aspect_ratio?: string;
entity?: string;
diff --git a/src/panels/lovelace/common/generate-lovelace-config.ts b/src/panels/lovelace/common/generate-lovelace-config.ts
index bf49d2116a..63c9830134 100644
--- a/src/panels/lovelace/common/generate-lovelace-config.ts
+++ b/src/panels/lovelace/common/generate-lovelace-config.ts
@@ -173,6 +173,7 @@ export const computeCards = (
const cardConfig = {
type: "weather-forecast",
entity: entityId,
+ show_forecast: false,
};
cards.push(cardConfig);
} else if (
diff --git a/src/panels/lovelace/components/hui-buttons-base.ts b/src/panels/lovelace/components/hui-buttons-base.ts
index 0a616df12c..84d9ff81c7 100644
--- a/src/panels/lovelace/components/hui-buttons-base.ts
+++ b/src/panels/lovelace/components/hui-buttons-base.ts
@@ -8,18 +8,20 @@ import {
queryAll,
TemplateResult,
} from "lit-element";
+
import { computeStateName } from "../../../common/entity/compute_state_name";
-import "../../../components/entity/state-badge";
-import type { StateBadge } from "../../../components/entity/state-badge";
-import "../../../components/ha-icon";
-import type { ActionHandlerEvent } from "../../../data/lovelace";
-import type { HomeAssistant } from "../../../types";
-import type { EntitiesCardEntityConfig } from "../cards/types";
import { computeTooltip } from "../common/compute-tooltip";
import { actionHandler } from "../common/directives/action-handler-directive";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
+import type { StateBadge } from "../../../components/entity/state-badge";
+import type { ActionHandlerEvent } from "../../../data/lovelace";
+import type { HomeAssistant } from "../../../types";
+import type { EntitiesCardEntityConfig } from "../cards/types";
+
+import "../../../components/entity/state-badge";
+
@customElement("hui-buttons-base")
export class HuiButtonsBase extends LitElement {
@property() public configEntities?: EntitiesCardEntityConfig[];
@@ -43,11 +45,6 @@ export class HuiButtonsBase extends LitElement {
return html`
${(this.configEntities || []).map((entityConf) => {
const stateObj = this._hass!.states[entityConf.entity];
- if (!stateObj) {
- return html`
-
- `;
- }
return html`
- ${entityConf.show_name ||
+ ${(entityConf.show_name && stateObj) ||
(entityConf.name && entityConf.show_name !== false)
? entityConf.name || computeStateName(stateObj)
: ""}
@@ -94,9 +91,6 @@ export class HuiButtonsBase extends LitElement {
display: flex;
justify-content: space-evenly;
}
- .missing {
- color: #fce588;
- }
div {
cursor: pointer;
align-items: center;
diff --git a/src/panels/lovelace/components/hui-card-options.ts b/src/panels/lovelace/components/hui-card-options.ts
index 4e6d5ddb3d..a74acde408 100644
--- a/src/panels/lovelace/components/hui-card-options.ts
+++ b/src/panels/lovelace/components/hui-card-options.ts
@@ -58,14 +58,14 @@ export class HuiCardOptions extends LitElement {
.length ===
this.path![1] + 1}
>
-
+
-
+
diff --git a/src/panels/lovelace/components/hui-image.ts b/src/panels/lovelace/components/hui-image.ts
index f9133cc612..7ad57d7f5b 100644
--- a/src/panels/lovelace/components/hui-image.ts
+++ b/src/panels/lovelace/components/hui-image.ts
@@ -46,6 +46,10 @@ export class HuiImage extends LitElement {
@property() public stateFilter?: StateSpecificConfig;
+ @property() public darkModeImage?: string;
+
+ @property() public darkModeFilter?: string;
+
@internalProperty() private _loadError?: boolean;
@internalProperty() private _cameraImageSrc?: string;
@@ -97,6 +101,8 @@ export class HuiImage extends LitElement {
imageSrc = this.image;
imageFallback = true;
}
+ } else if (this.darkModeImage && this.hass.themes.darkMode) {
+ imageSrc = this.darkModeImage;
} else {
imageSrc = this.image;
}
@@ -108,8 +114,12 @@ export class HuiImage extends LitElement {
// Figure out filter to use
let filter = this.filter || "";
+ if (this.hass.themes.darkMode && this.darkModeFilter) {
+ filter += this.darkModeFilter;
+ }
+
if (this.stateFilter && this.stateFilter[state]) {
- filter = this.stateFilter[state];
+ filter += this.stateFilter[state];
}
if (!filter && this.entity) {
diff --git a/src/panels/lovelace/components/hui-warning.ts b/src/panels/lovelace/components/hui-warning.ts
index 05732f51b1..2d58d9cbba 100644
--- a/src/panels/lovelace/components/hui-warning.ts
+++ b/src/panels/lovelace/components/hui-warning.ts
@@ -35,7 +35,7 @@ export class HuiWarning extends LitElement {
color: black;
background-color: #fce588;
padding: 8px;
- word-break: break-word;
+ word-break: break-word;
}
`;
}
diff --git a/src/panels/lovelace/editor/card-editor/hui-dialog-create-card.ts b/src/panels/lovelace/editor/card-editor/hui-dialog-create-card.ts
index 9b3963485c..3eb419a3cd 100644
--- a/src/panels/lovelace/editor/card-editor/hui-dialog-create-card.ts
+++ b/src/panels/lovelace/editor/card-editor/hui-dialog-create-card.ts
@@ -40,7 +40,8 @@ interface SelectedChangedEvent {
}
@customElement("hui-dialog-create-card")
-export class HuiCreateDialogCard extends LitElement implements HassDialog {
+export class HuiCreateDialogCard extends LitElement
+ implements HassDialog {
@property({ attribute: false }) protected hass!: HomeAssistant;
@internalProperty() private _params?: CreateCardDialogParams;
@@ -119,14 +120,13 @@ export class HuiCreateDialogCard extends LitElement implements HassDialog {
>
`
: html`
-
-
-
+
`
)}
@@ -203,12 +203,14 @@ export class HuiCreateDialogCard extends LitElement implements HassDialog {
var(--mdc-dialog-scroll-divider-color, rgba(0, 0, 0, 0.12));
}
- .entity-picker-container {
- display: flex;
- flex-direction: column;
- height: 100%;
- min-height: calc(100vh - 112px);
- margin-top: -20px;
+ hui-entity-picker-table {
+ display: block;
+ height: calc(100vh - 198px);
+ }
+ @media all and (max-width: 450px), all and (max-height: 500px) {
+ hui-entity-picker-table {
+ height: calc(100vh - 158px);
+ }
}
`,
];
diff --git a/src/panels/lovelace/editor/card-editor/hui-dialog-delete-card.ts b/src/panels/lovelace/editor/card-editor/hui-dialog-delete-card.ts
index 08f3678efc..b70afdd32e 100644
--- a/src/panels/lovelace/editor/card-editor/hui-dialog-delete-card.ts
+++ b/src/panels/lovelace/editor/card-editor/hui-dialog-delete-card.ts
@@ -28,7 +28,7 @@ export class HuiDialogDeleteCard extends LitElement {
@internalProperty() private _cardConfig?: LovelaceCardConfig;
- @query("ha-paper-dialog") private _dialog!: HaPaperDialog;
+ @query("ha-paper-dialog", true) private _dialog!: HaPaperDialog;
public async showDialog(params: DeleteCardDialogParams): Promise {
this._params = params;
diff --git a/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts b/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts
index aa45460df6..a779dd2080 100755
--- a/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts
+++ b/src/panels/lovelace/editor/card-editor/hui-dialog-edit-card.ts
@@ -50,7 +50,8 @@ declare global {
}
@customElement("hui-dialog-edit-card")
-export class HuiDialogEditCard extends LitElement implements HassDialog {
+export class HuiDialogEditCard extends LitElement
+ implements HassDialog {
@property({ attribute: false }) public hass!: HomeAssistant;
@internalProperty() private _params?: EditCardDialogParams;
@@ -174,7 +175,7 @@ export class HuiDialogEditCard extends LitElement implements HassDialog {
dir=${computeRTLDirection(this.hass)}
>
-
+
`
@@ -238,7 +239,7 @@ export class HuiDialogEditCard extends LitElement implements HassDialog {
? html`
`
@@ -364,7 +365,6 @@ export class HuiDialogEditCard extends LitElement implements HassDialog {
@media all and (min-width: 850px) {
ha-dialog {
--mdc-dialog-min-width: 845px;
- --dialog-surface-top: 40px;
--mdc-dialog-max-height: calc(100% - 72px);
}
}
@@ -424,10 +424,6 @@ export class HuiDialogEditCard extends LitElement implements HassDialog {
max-width: 500px;
}
}
-
- mwc-button ha-circular-progress {
- margin-right: 20px;
- }
.hidden {
display: none;
}
diff --git a/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts b/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts
index 0a23b164e6..a8e2d4bfc4 100755
--- a/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts
+++ b/src/panels/lovelace/editor/card-editor/hui-dialog-suggest-card.ts
@@ -108,7 +108,8 @@ export class HuiDialogSuggestCard extends LitElement {
? html`
`
: this.hass!.localize(
@@ -142,11 +143,6 @@ export class HuiDialogSuggestCard extends LitElement {
max-width: 845px;
--dialog-z-index: 5;
}
- mwc-button ha-circular-progress {
- width: 14px;
- height: 14px;
- margin-right: 20px;
- }
.hidden {
display: none;
}
diff --git a/src/panels/lovelace/editor/card-editor/hui-entity-picker-table.ts b/src/panels/lovelace/editor/card-editor/hui-entity-picker-table.ts
index 3a724fc6e2..c9e2bb6f03 100644
--- a/src/panels/lovelace/editor/card-editor/hui-entity-picker-table.ts
+++ b/src/panels/lovelace/editor/card-editor/hui-entity-picker-table.ts
@@ -27,12 +27,14 @@ export class HuiEntityPickerTable extends LitElement {
@property({ type: Boolean }) public narrow?: boolean;
+ @property({ type: Boolean, attribute: "no-label-float" })
+ public noLabelFloat? = false;
+
@property({ type: Array }) public entities!: DataTableRowData[];
protected render(): TemplateResult {
return html`
{
return this._config!.state_image || {};
}
diff --git a/src/panels/lovelace/editor/config-elements/hui-stack-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-stack-card-editor.ts
index 84ca356c9c..d2640e7cae 100644
--- a/src/panels/lovelace/editor/config-elements/hui-stack-card-editor.ts
+++ b/src/panels/lovelace/editor/config-elements/hui-stack-card-editor.ts
@@ -1,3 +1,4 @@
+import { mdiArrowLeft, mdiArrowRight, mdiDelete, mdiPlus } from "@mdi/js";
import "@polymer/paper-tabs";
import "@polymer/paper-tabs/paper-tab";
import {
@@ -13,7 +14,6 @@ import {
} from "lit-element";
import { any, array, assert, object, optional, string } from "superstruct";
import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event";
-import "../../../../components/ha-icon-button";
import { LovelaceCardConfig, LovelaceConfig } from "../../../../data/lovelace";
import { HomeAssistant } from "../../../../types";
import { StackCardConfig } from "../../cards/types";
@@ -87,7 +87,7 @@ export class HuiStackCardEditor extends LitElement
@iron-activate=${this._handleSelectedCard}
>
-
+
@@ -107,26 +107,37 @@ export class HuiStackCardEditor extends LitElement
: "ui.panel.lovelace.editor.edit_card.show_visual_editor"
)}
-
-
+
+
+
+
+ .move=${1}
+ >
+
+
-
+ >
+
+
-
+ >
-
+
`}
>
@@ -135,10 +135,13 @@ export class HuiSaveConfig extends LitElement implements HassDialog {
?disabled=${this._saving}
@click=${this._saveConfig}
>
-
+ ${this._saving
+ ? html` `
+ : ""}
${this.hass!.localize(
"ui.panel.lovelace.editor.save_config.save"
)}
@@ -204,17 +207,6 @@ export class HuiSaveConfig extends LitElement implements HassDialog {
ha-paper-dialog {
max-width: 650px;
}
- ha-circular-progress {
- display: none;
- }
- ha-circular-progress[active] {
- display: block;
- }
- mwc-button ha-circular-progress {
- width: 14px;
- height: 14px;
- margin-right: 20px;
- }
ha-switch {
padding-bottom: 16px;
}
diff --git a/src/panels/lovelace/editor/lovelace-editor/hui-dialog-edit-lovelace.ts b/src/panels/lovelace/editor/lovelace-editor/hui-dialog-edit-lovelace.ts
index 5ff5cf67e2..c41c4ba593 100644
--- a/src/panels/lovelace/editor/lovelace-editor/hui-dialog-edit-lovelace.ts
+++ b/src/panels/lovelace/editor/lovelace-editor/hui-dialog-edit-lovelace.ts
@@ -76,10 +76,13 @@ export class HuiDialogEditLovelace extends LitElement {
?disabled="${!this._config || this._saving}"
@click="${this._save}"
>
-
+ ${this._saving
+ ? html` `
+ : ""}
${this.hass!.localize("ui.common.save")}
@@ -149,17 +152,6 @@ export class HuiDialogEditLovelace extends LitElement {
ha-paper-dialog {
max-width: 650px;
}
- mwc-button ha-circular-progress {
- width: 14px;
- height: 14px;
- margin-right: 20px;
- }
- ha-circular-progress {
- display: none;
- }
- ha-circular-progress[active] {
- display: block;
- }
`,
];
}
diff --git a/src/panels/lovelace/editor/unused-entities/hui-unused-entities.ts b/src/panels/lovelace/editor/unused-entities/hui-unused-entities.ts
index 1e81f187a5..8a500d2499 100644
--- a/src/panels/lovelace/editor/unused-entities/hui-unused-entities.ts
+++ b/src/panels/lovelace/editor/unused-entities/hui-unused-entities.ts
@@ -112,7 +112,7 @@ export class HuiUnusedEntities extends LitElement {
.label=${this.hass.localize("ui.panel.lovelace.editor.edit_card.add")}
@click=${this._addToLovelaceView}
>
-
+
`;
@@ -159,11 +159,11 @@ export class HuiUnusedEntities extends LitElement {
return css`
:host {
background: var(--lovelace-background);
+ overflow: hidden;
}
.container {
display: flex;
flex-direction: column;
- /* min-height: calc(100vh - 112px); */
height: 100%;
}
ha-card {
diff --git a/src/panels/lovelace/editor/view-editor/hui-dialog-edit-view.ts b/src/panels/lovelace/editor/view-editor/hui-dialog-edit-view.ts
index 99a4022efc..d4b1eafced 100644
--- a/src/panels/lovelace/editor/view-editor/hui-dialog-edit-view.ts
+++ b/src/panels/lovelace/editor/view-editor/hui-dialog-edit-view.ts
@@ -117,6 +117,15 @@ export class HuiDialogEditView extends LitElement {
content = html`
${this._badges?.length
? html`
+ ${this._config?.panel
+ ? html`
+
+ ${this.hass!.localize(
+ "ui.panel.lovelace.editor.edit_badges.panel_mode"
+ )}
+
+ `
+ : ""}
${this._badges.map((badgeConfig) => {
return html`
@@ -206,10 +215,13 @@ export class HuiDialogEditView extends LitElement {
?disabled="${!this._config || this._saving}"
@click="${this._save}"
>
-
+ ${this._saving
+ ? html` `
+ : ""}
${this.hass!.localize("ui.common.save")}
@@ -386,11 +398,6 @@ export class HuiDialogEditView extends LitElement {
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
padding: 0 20px;
}
- mwc-button ha-circular-progress {
- width: 14px;
- height: 14px;
- margin-right: 20px;
- }
mwc-button.warning {
margin-right: auto;
}
@@ -413,6 +420,10 @@ export class HuiDialogEditView extends LitElement {
margin: 12px 16px;
flex-wrap: wrap;
}
+ .warning {
+ color: var(--warning-color);
+ text-align: center;
+ }
@media all and (min-width: 600px) {
ha-dialog {
diff --git a/src/panels/lovelace/elements/hui-service-button-element.ts b/src/panels/lovelace/elements/hui-service-button-element.ts
index 08f0988818..a9af59cf0e 100644
--- a/src/panels/lovelace/elements/hui-service-button-element.ts
+++ b/src/panels/lovelace/elements/hui-service-button-element.ts
@@ -22,10 +22,6 @@ export class HuiServiceButtonElement extends LitElement
private _service?: string;
- static get properties() {
- return { _config: {} };
- }
-
public setConfig(config: ServiceButtonElementConfig): void {
if (!config || !config.service) {
throw Error("Invalid Configuration: 'service' required");
diff --git a/src/panels/lovelace/elements/types.ts b/src/panels/lovelace/elements/types.ts
index 46374b34c2..1894946a13 100644
--- a/src/panels/lovelace/elements/types.ts
+++ b/src/panels/lovelace/elements/types.ts
@@ -4,7 +4,7 @@ import { Condition } from "../common/validate-condition";
interface LovelaceElementConfigBase {
type: string;
- style: object;
+ style: Record;
}
export type LovelaceElementConfig =
@@ -51,7 +51,7 @@ export interface ImageElementConfig extends LovelaceElementConfigBase {
export interface ServiceButtonElementConfig extends LovelaceElementConfigBase {
title?: string;
service?: string;
- service_data?: object;
+ service_data?: Record;
}
export interface StateBadgeElementConfig extends LovelaceElementConfigBase {
diff --git a/src/panels/lovelace/entity-rows/hui-group-entity-row.ts b/src/panels/lovelace/entity-rows/hui-group-entity-row.ts
index 8982640a61..fb414e312f 100644
--- a/src/panels/lovelace/entity-rows/hui-group-entity-row.ts
+++ b/src/panels/lovelace/entity-rows/hui-group-entity-row.ts
@@ -29,7 +29,7 @@ class HuiGroupEntityRow extends LitElement implements LovelaceRow {
if (domain === "group") {
return this._computeCanToggle(
hass,
- this.hass?.states[entityId].attributes["entity_id"]
+ this.hass?.states[entityId].attributes.entity_id
);
}
return DOMAINS_TOGGLE.has(domain);
diff --git a/src/panels/lovelace/entity-rows/types.ts b/src/panels/lovelace/entity-rows/types.ts
index 03c162fb2f..e1d328c330 100644
--- a/src/panels/lovelace/entity-rows/types.ts
+++ b/src/panels/lovelace/entity-rows/types.ts
@@ -50,12 +50,12 @@ export interface ButtonRowConfig extends EntityConfig {
}
export interface CastConfig {
type: "cast";
- icon: string;
- name: string;
+ icon?: string;
+ name?: string;
view: string | number;
dashboard?: string;
// Hide the row if either unsupported browser or no API available.
- hide_if_unavailable: boolean;
+ hide_if_unavailable?: boolean;
}
export interface ButtonsRowConfig {
type: "buttons";
diff --git a/src/panels/lovelace/header-footer/hui-buttons-header-footer.ts b/src/panels/lovelace/header-footer/hui-buttons-header-footer.ts
index 19f8be82be..4b734da662 100644
--- a/src/panels/lovelace/header-footer/hui-buttons-header-footer.ts
+++ b/src/panels/lovelace/header-footer/hui-buttons-header-footer.ts
@@ -1,10 +1,10 @@
import {
customElement,
html,
+ internalProperty,
LitElement,
property,
TemplateResult,
- internalProperty,
} from "lit-element";
import { HomeAssistant } from "../../../types";
import { processConfigEntities } from "../common/process-config-entities";
@@ -16,7 +16,7 @@ import { ButtonsHeaderFooterConfig } from "./types";
@customElement("hui-buttons-header-footer")
export class HuiButtonsHeaderFooter extends LitElement
implements LovelaceHeaderFooter {
- public static getStubConfig(): object {
+ public static getStubConfig(): Record {
return { entities: [] };
}
@@ -25,7 +25,7 @@ export class HuiButtonsHeaderFooter extends LitElement
@internalProperty() private _configEntities?: EntityConfig[];
public getCardSize(): number {
- return 1;
+ return 3;
}
public setConfig(config: ButtonsHeaderFooterConfig): void {
diff --git a/src/panels/lovelace/header-footer/hui-graph-header-footer.ts b/src/panels/lovelace/header-footer/hui-graph-header-footer.ts
index 8cbda2ed3e..30fda681c4 100644
--- a/src/panels/lovelace/header-footer/hui-graph-header-footer.ts
+++ b/src/panels/lovelace/header-footer/hui-graph-header-footer.ts
@@ -1,16 +1,16 @@
-import "../../../components/ha-circular-progress";
import { HassEntity } from "home-assistant-js-websocket";
import {
css,
CSSResult,
customElement,
html,
+ internalProperty,
LitElement,
property,
- internalProperty,
PropertyValues,
TemplateResult,
} from "lit-element";
+import "../../../components/ha-circular-progress";
import { fetchRecent } from "../../../data/history";
import { HomeAssistant } from "../../../types";
import { coordinates } from "../common/graph/coordinates";
@@ -25,7 +25,7 @@ const DAY = 86400000;
@customElement("hui-graph-header-footer")
export class HuiGraphHeaderFooter extends LitElement
implements LovelaceHeaderFooter {
- public static getStubConfig(): object {
+ public static getStubConfig(): Record {
return {};
}
@@ -42,7 +42,7 @@ export class HuiGraphHeaderFooter extends LitElement
private _fetching = false;
public getCardSize(): number {
- return 2;
+ return 3;
}
public setConfig(config: GraphHeaderFooterConfig): void {
diff --git a/src/panels/lovelace/header-footer/hui-picture-header-footer.ts b/src/panels/lovelace/header-footer/hui-picture-header-footer.ts
index 7b00580748..dd762486ef 100644
--- a/src/panels/lovelace/header-footer/hui-picture-header-footer.ts
+++ b/src/panels/lovelace/header-footer/hui-picture-header-footer.ts
@@ -22,7 +22,7 @@ import { PictureHeaderFooterConfig } from "./types";
@customElement("hui-picture-header-footer")
export class HuiPictureHeaderFooter extends LitElement
implements LovelaceHeaderFooter {
- public static getStubConfig(): object {
+ public static getStubConfig(): Record {
return {
image:
"https://www.home-assistant.io/images/merchandise/shirt-frontpage.png",
diff --git a/src/panels/lovelace/hui-root.ts b/src/panels/lovelace/hui-root.ts
index a0f0a6dfa3..740c5a7afb 100644
--- a/src/panels/lovelace/hui-root.ts
+++ b/src/panels/lovelace/hui-root.ts
@@ -3,17 +3,20 @@ import "@material/mwc-list/mwc-list-item";
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
import {
mdiClose,
+ mdiCog,
mdiDotsVertical,
+ mdiHelp,
mdiHelpCircle,
mdiMicrophone,
mdiPencil,
mdiPlus,
+ mdiRefresh,
+ mdiShape,
} from "@mdi/js";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-scroll-effects/effects/waterfall";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import "@polymer/paper-tabs/paper-tab";
-import "@polymer/paper-tabs/paper-tabs";
import {
css,
CSSResult,
@@ -22,6 +25,7 @@ import {
LitElement,
property,
PropertyValues,
+ query,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
@@ -40,6 +44,7 @@ import "../../components/ha-icon-button-arrow-next";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-menu-button";
import "../../components/ha-svg-icon";
+import "../../components/ha-tabs";
import type {
LovelaceConfig,
LovelacePanelConfig,
@@ -51,6 +56,7 @@ import {
} from "../../dialogs/generic/show-dialog-box";
import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
import "../../layouts/ha-app-layout";
+import type { haAppLayout } from "../../layouts/ha-app-layout";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
@@ -72,6 +78,8 @@ class HUIRoot extends LitElement {
@internalProperty() private _curView?: number | "hass-unused-entities";
+ @query("ha-app-layout", true) private _appLayout!: haAppLayout;
+
private _viewCache?: { [viewId: string]: HUIView };
private _debouncedConfigChanged: () => void;
@@ -115,7 +123,7 @@ class HUIRoot extends LitElement {
)}"
@click="${this._editModeDisable}"
>
-
+
${this.config.title ||
@@ -130,7 +138,7 @@ class HUIRoot extends LitElement {
class="edit-icon"
@click="${this._editLovelace}"
>
-
+
-
+
@@ -157,7 +165,7 @@ class HUIRoot extends LitElement {
"ui.panel.lovelace.editor.menu.open"
)}
>
-
+
${__DEMO__ /* No unused entities available in the demo */
? ""
@@ -187,14 +195,53 @@ class HUIRoot extends LitElement {
.hass=${this.hass}
.narrow=${this.narrow}
>
- ${this.config.title || "Home Assistant"}
- ${this._conversation(this.hass.config.components)
+ ${this.lovelace!.config.views.length > 1
+ ? html`
+
+ ${this.lovelace!.config.views.map(
+ (view) => html`
+ e.user === this.hass!.user!.id
+ )) ||
+ view.visible === false)
+ ),
+ })}"
+ >
+ ${view.icon
+ ? html`
+
+ `
+ : view.title || "Unnamed view"}
+
+ `
+ )}
+
+ `
+ : html`${this.config.title}`}
+ ${!this.narrow &&
+ this._conversation(this.hass.config.components)
? html`
-
+
`
: ""}
@@ -208,29 +255,65 @@ class HUIRoot extends LitElement {
"ui.panel.lovelace.editor.menu.open"
)}"
>
-
+
+ ${this.narrow &&
+ this._conversation(this.hass.config.components)
+ ? html`
+
+ ${this.hass!.localize(
+ "ui.panel.lovelace.menu.start_conversation"
+ )}
+
+
+ `
+ : ""}
${this._yamlMode
? html`
- ${this.hass!.localize(
- "ui.panel.lovelace.menu.refresh"
- )}
+ ${this.hass!.localize(
+ "ui.panel.lovelace.menu.refresh"
+ )}
+
- ${this.hass!.localize(
- "ui.panel.lovelace.unused_entities.title"
- )}
+ ${this.hass!.localize(
+ "ui.panel.lovelace.unused_entities.title"
+ )}
+
`
: ""}
@@ -238,6 +321,7 @@ class HUIRoot extends LitElement {
?.mode === "yaml"
? html`
`
: ""}
${this.hass!.user?.is_admin && !this.hass!.config.safe_mode
? html`
`
: ""}
@@ -270,20 +363,25 @@ class HUIRoot extends LitElement {
target="_blank"
>
${this.hass!.localize("ui.panel.lovelace.menu.help")}
+
`}
- ${this.lovelace!.config.views.length > 1 || this._editMode
+ ${this._editMode
? html`
-
-
+
`
: ""}
-
+
`
: ""}
-
+
`;
}
@@ -443,10 +534,7 @@ class HUIRoot extends LitElement {
if (!oldLovelace || oldLovelace.editMode !== this.lovelace!.editMode) {
const views = this.config && this.config.views;
- // Adjust for higher header
- if (!views || views.length < 2) {
- fireEvent(this, "iron-resize");
- }
+ fireEvent(this, "iron-resize");
// Leave unused entities when leaving edit mode
if (
@@ -643,12 +731,6 @@ class HUIRoot extends LitElement {
unusedEntities.lovelace = this.lovelace!;
unusedEntities.narrow = this.narrow;
});
- if (this.config.background) {
- unusedEntities.style.setProperty(
- "--lovelace-background",
- this.config.background
- );
- }
root.appendChild(unusedEntities);
return;
}
@@ -675,7 +757,12 @@ class HUIRoot extends LitElement {
const configBackground = viewConfig.background || this.config.background;
if (configBackground) {
- view.style.setProperty("--lovelace-background", configBackground);
+ this._appLayout.style.setProperty(
+ "--lovelace-background",
+ configBackground
+ );
+ } else {
+ this._appLayout.style.removeProperty("--lovelace-background");
}
root.appendChild(view);
@@ -697,13 +784,19 @@ class HUIRoot extends LitElement {
ha-app-layout {
min-height: 100%;
+ background: var(--lovelace-background);
}
- paper-tabs {
- margin-left: max(env(safe-area-inset-left), 12px);
- margin-right: env(safe-area-inset-right);
+ ha-tabs {
+ width: 100%;
+ height: 100%;
+ margin-left: 4px;
--paper-tabs-selection-bar-color: var(--text-primary-color, #fff);
text-transform: uppercase;
}
+ .edit-mode ha-tabs {
+ margin-left: max(env(safe-area-inset-left), 24px);
+ margin-right: max(env(safe-area-inset-right), 24px);
+ }
.edit-mode {
background-color: var(--dark-color, #455a64);
color: var(--text-dark-color);
@@ -738,7 +831,7 @@ class HUIRoot extends LitElement {
color: var(--error-color);
}
#view {
- min-height: calc(100vh - 112px);
+ min-height: calc(100vh - var(--header-height));
/**
* Since we only set min-height, if child nodes need percentage
* heights they must use absolute positioning so we need relative
@@ -761,9 +854,6 @@ class HUIRoot extends LitElement {
flex: 1 1 100%;
max-width: 100%;
}
- #view.tabs-hidden {
- min-height: calc(100vh - 64px);
- }
.hide-tab {
display: none;
}
diff --git a/src/panels/lovelace/special-rows/hui-buttons-row.ts b/src/panels/lovelace/special-rows/hui-buttons-row.ts
index 574ffd0bf4..f910df90b0 100644
--- a/src/panels/lovelace/special-rows/hui-buttons-row.ts
+++ b/src/panels/lovelace/special-rows/hui-buttons-row.ts
@@ -17,7 +17,7 @@ import {
@customElement("hui-buttons-row")
export class HuiButtonsRow extends LitElement implements LovelaceRow {
- public static getStubConfig(): object {
+ public static getStubConfig(): Record {
return { entities: [] };
}
diff --git a/src/panels/lovelace/types.ts b/src/panels/lovelace/types.ts
index 8609f47b78..b6e52a3d1c 100644
--- a/src/panels/lovelace/types.ts
+++ b/src/panels/lovelace/types.ts
@@ -10,8 +10,8 @@ import { LovelaceHeaderFooterConfig } from "./header-footer/types";
declare global {
// eslint-disable-next-line
interface HASSDomEvents {
- "ll-rebuild": {};
- "ll-badge-rebuild": {};
+ "ll-rebuild": Record;
+ "ll-badge-rebuild": Record;
}
}
diff --git a/src/panels/lovelace/views/hui-masonry-view.ts b/src/panels/lovelace/views/hui-masonry-view.ts
index fb3290fff7..00555cd6b1 100644
--- a/src/panels/lovelace/views/hui-masonry-view.ts
+++ b/src/panels/lovelace/views/hui-masonry-view.ts
@@ -90,7 +90,7 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
rtl: computeRTL(this.hass!),
})}
>
-
+
`
: ""}
@@ -260,7 +260,6 @@ export class MasonryView extends LitElement implements LovelaceViewElement {
return css`
:host {
display: block;
- background: var(--lovelace-background);
padding-top: 4px;
height: 100%;
box-sizing: border-box;
diff --git a/src/panels/lovelace/views/hui-panel-view.ts b/src/panels/lovelace/views/hui-panel-view.ts
index e1488f0996..2b9c5ae1e0 100644
--- a/src/panels/lovelace/views/hui-panel-view.ts
+++ b/src/panels/lovelace/views/hui-panel-view.ts
@@ -82,7 +82,7 @@ export class PanelView extends LitElement implements LovelaceViewElement {
rtl: computeRTL(this.hass!),
})}
>
-
+
`
: ""}
@@ -131,7 +131,6 @@ export class PanelView extends LitElement implements LovelaceViewElement {
return css`
:host {
display: block;
- background: var(--lovelace-background);
height: 100%;
}
diff --git a/src/panels/lovelace/views/hui-view.ts b/src/panels/lovelace/views/hui-view.ts
index 3a8dcbcd04..60ee6c4760 100644
--- a/src/panels/lovelace/views/hui-view.ts
+++ b/src/panels/lovelace/views/hui-view.ts
@@ -131,7 +131,13 @@ export class HUIView extends UpdatingElement {
this._layoutElement!.lovelace = lovelace;
}
- if (configChanged || hassChanged || editModeChanged) {
+ if (
+ configChanged ||
+ hassChanged ||
+ editModeChanged ||
+ changedProperties.has("_cards") ||
+ changedProperties.has("_badges")
+ ) {
this._layoutElement!.cards = this._cards;
this._layoutElement!.badges = this._badges;
}
diff --git a/src/panels/mailbox/ha-dialog-show-audio-message.js b/src/panels/mailbox/ha-dialog-show-audio-message.js
index d288138881..790d3b4ab6 100644
--- a/src/panels/mailbox/ha-dialog-show-audio-message.js
+++ b/src/panels/mailbox/ha-dialog-show-audio-message.js
@@ -21,7 +21,7 @@ class HaDialogShowAudioMessage extends LocalizeMixin(PolymerElement) {
ha-paper-dialog {
margin: 0;
width: 100%;
- max-height: calc(100% - 64px);
+ max-height: calc(100% - var(--header-height));
position: fixed !important;
bottom: 0px;
diff --git a/src/panels/mailbox/ha-panel-mailbox.js b/src/panels/mailbox/ha-panel-mailbox.js
index fb9e17acb0..08c5e1fd51 100644
--- a/src/panels/mailbox/ha-panel-mailbox.js
+++ b/src/panels/mailbox/ha-panel-mailbox.js
@@ -6,7 +6,7 @@ import "@polymer/paper-input/paper-textarea";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-tabs/paper-tab";
-import "@polymer/paper-tabs/paper-tabs";
+import "../../components/ha-tabs";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
@@ -46,6 +46,13 @@ class HaPanelMailbox extends EventsMixin(LocalizeMixin(PolymerElement)) {
cursor: pointer;
}
+ ha-tabs {
+ margin-left: max(env(safe-area-inset-left), 24px);
+ margin-right: max(env(safe-area-inset-right), 24px);
+ --paper-tabs-selection-bar-color: #fff;
+ text-transform: uppercase;
+ }
+
.empty {
text-align: center;
color: var(--secondary-text-color);
@@ -86,7 +93,7 @@ class HaPanelMailbox extends EventsMixin(LocalizeMixin(PolymerElement)) {
[[localize('panel.mailbox')]]
-
-
+
diff --git a/src/panels/map/ha-panel-map.js b/src/panels/map/ha-panel-map.js
index a3c2d5103b..f6b299526a 100644
--- a/src/panels/map/ha-panel-map.js
+++ b/src/panels/map/ha-panel-map.js
@@ -25,7 +25,7 @@ class HaPanelMap extends LocalizeMixin(PolymerElement) {
return html`
- Create snapshot + Create Snapshot
Snapshots allow you to easily backup and restore all data of your @@ -219,7 +219,7 @@ class HassioSnapshots extends LitElement {
Available snapshots
+Available Snapshots
+
-
+ ${resolution.unsupported.map(
+ (issue) => html`
+
- + ${ISSUES[issue] + ? html` + ${ISSUES[issue].title} + ` + : issue} + + ` + )} +
-
@@ -264,6 +277,10 @@ class HaChartBase extends mixinBehaviors(
const title = tooltip.title ? tooltip.title[0] || "" : "";
this.set(["tooltip", "title"], title);
+ if (tooltip.beforeBody) {
+ this.set(["tooltip", "beforeBody"], tooltip.beforeBody.join("\n"));
+ }
+
const bodyLines = tooltip.body.map((n) => n.lines);
// Set Text
diff --git a/src/components/entity/ha-entity-attribute-picker.ts b/src/components/entity/ha-entity-attribute-picker.ts
index ee2958323b..e9c3b52e0f 100644
--- a/src/components/entity/ha-entity-attribute-picker.ts
+++ b/src/components/entity/ha-entity-attribute-picker.ts
@@ -1,3 +1,4 @@
+import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
@@ -16,8 +17,9 @@ import {
import { fireEvent } from "../../common/dom/fire_event";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
-import "../ha-icon-button";
+import "../ha-svg-icon";
import "./state-badge";
+import "@material/mwc-icon-button/mwc-icon-button";
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
@@ -55,7 +57,7 @@ class HaEntityAttributePicker extends LitElement {
@property({ type: Boolean }) private _opened = false;
- @query("vaadin-combo-box-light") private _comboBox!: HTMLElement;
+ @query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
protected shouldUpdate(changedProps: PropertyValues) {
return !(!changedProps.has("_opened") && this._opened);
@@ -80,6 +82,7 @@ class HaEntityAttributePicker extends LitElement {
.value=${this._value}
.allowCustomValue=${this.allowCustomValue}
.renderer=${rowRenderer}
+ attr-for-value="bind-value"
@opened-changed=${this._openedChanged}
@value-changed=${this._valueChanged}
>
@@ -97,33 +100,35 @@ class HaEntityAttributePicker extends LitElement {
autocorrect="off"
spellcheck="false"
>
- ${this.value
- ? html`
-
${this.header}
` : html``}${currentItem.media_content_id === "media-source://media_source/local/." ? html`
${this.hass.localize( @@ -398,7 +399,7 @@ export class HaMediaPlayerBrowse extends LitElement {
${this.hass.localize( "ui.components.media-browser.local_media_files" - )}.` + )}` : ""}
${this._params.text} @@ -180,6 +181,9 @@ class DialogBox extends LitElement { /* Place above other dialogs */ --dialog-z-index: 104; } + .warning { + color: var(--warning-color); + } `, ]; } diff --git a/src/dialogs/generic/show-dialog-box.ts b/src/dialogs/generic/show-dialog-box.ts index 86c5cfee8b..5e965bab2f 100644 --- a/src/dialogs/generic/show-dialog-box.ts +++ b/src/dialogs/generic/show-dialog-box.ts @@ -5,6 +5,7 @@ interface BaseDialogParams { confirmText?: string; text?: string | TemplateResult; title?: string; + warning?: boolean; } export interface AlertDialogParams extends BaseDialogParams { diff --git a/src/dialogs/image-cropper-dialog/image-cropper-dialog.ts b/src/dialogs/image-cropper-dialog/image-cropper-dialog.ts index ed9e3a05aa..c2ab0ca148 100644 --- a/src/dialogs/image-cropper-dialog/image-cropper-dialog.ts +++ b/src/dialogs/image-cropper-dialog/image-cropper-dialog.ts @@ -29,7 +29,7 @@ export class HaImagecropperDialog extends LitElement { @internalProperty() private _open = false; - @query("img") private _image!: HTMLImageElement; + @query("img", true) private _image!: HTMLImageElement; private _cropper?: Cropper; diff --git a/src/dialogs/more-info/controls/more-info-group.js b/src/dialogs/more-info/controls/more-info-group.js deleted file mode 100644 index fabab470cc..0000000000 --- a/src/dialogs/more-info/controls/more-info-group.js +++ /dev/null @@ -1,111 +0,0 @@ -import { dom } from "@polymer/polymer/lib/legacy/polymer.dom"; -import { html } from "@polymer/polymer/lib/utils/html-tag"; -/* eslint-plugin-disable lit */ -import { PolymerElement } from "@polymer/polymer/polymer-element"; -import dynamicContentUpdater from "../../../common/dom/dynamic_content_updater"; -import { computeStateDomain } from "../../../common/entity/compute_state_domain"; -import "../../../state-summary/state-card-content"; - -class MoreInfoGroup extends PolymerElement { - static get template() { - return html` - - -
- -+ + [[localize("ui.panel.config.customize.picker.documentation")]] +
+
[[localize('ui.panel.config.customize.attributes_customize')]]
-
+
+
[[localize('ui.panel.config.customize.attributes_outside')]]
[[localize('ui.panel.config.customize.different_include')]]
-
+
+
[[localize('ui.panel.config.customize.attributes_set')]]
[[localize('ui.panel.config.customize.attributes_override')]]
-
+
+
[[localize('ui.panel.config.customize.attributes_not_set')]] - +
${this.hass.localize( "ui.panel.config.devices.automation.automations" )} @@ -270,7 +270,7 @@ export class HaConfigDevicePage extends LitElement { )} icon="hass:plus-circle" > -
${this.hass.localize( "ui.panel.config.devices.scene.scenes" )} @@ -340,7 +340,7 @@ export class HaConfigDevicePage extends LitElement { )} icon="hass:plus-circle" > -
${this.hass.localize( "ui.panel.config.devices.script.scripts" )} @@ -413,18 +413,14 @@ export class HaConfigDevicePage extends LitElement { )} icon="hass:plus-circle" > -
@@ -441,11 +469,32 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { "ui.panel.config.filtering.filtering_by" )} ${activeFilters.join(", ")} + ${this._numHiddenEntities + ? "(" + + this.hass.localize( + "ui.panel.config.entities.picker.filter.hidden_entities", + "number", + this._numHiddenEntities + ) + + ")" + : ""}
+
+ ${domainToName(this.hass.localize, "system_health")}
+
+
+
+
+
+ ${this.hass.localize("ui.common.copied")}
+
+
+ + ${this.hass.localize( + "ui.panel.config.ozw.node_config.help_source" + )} + +
++ Note: This panel is currently read-only. The ability to change + values will come in a later update. +
++ ${this.hass.localize("ui.panel.config.ozw.common.node_id")}: + ${this._node.node_id}
+ ${this.hass.localize( + "ui.panel.config.ozw.common.query_stage" + )}: + ${this._node.node_query_stage} + ${this._metadata?.metadata.ProductManualURL + ? html` +
+ ${this.hass.localize( + "ui.panel.config.ozw.node_metadata.product_manual" + )} +
+ ` + : ``} ++ ${this._metadata.metadata.WakeupHelp} +
++ ${item.help} +
${item.value}
+- Node ID: ${this._node.node_id}
- Query Stage: ${this._node.node_query_stage} - ${this._metadata?.metadata.ProductManualURL - ? html` -
Product Manual
- ` ++ Node ID: ${this._node.node_id}
+ Query Stage: ${this._node.node_query_stage} + ${this._metadata?.metadata.ProductManualURL + ? html` +
Product Manual
+ ` + : ``} +- ${this._errorLog +
+ ${hass.localize("ui.panel.config.person.introduction")} +
${this._configItems.length > 0 ? html`@@ -81,7 +87,16 @@ class HaConfigPerson extends LitElement {
` : ""} + + + ${this.hass.localize("ui.panel.config.person.learn_more")} + +
${device.name}
-
+ ${this.hass.localize( + "ui.panel.config.script.editor.modes.description", + "documentation_link", + html`${this.hass.localize( + "ui.panel.config.script.editor.modes.documentation" + )}` + )} +
++ ${this.hass.localize( + "ui.panel.config.script.editor.sequence_sentence" )} - .errorMessage=${this.hass.localize( - "ui.panel.config.script.editor.id_already_exists" - )} - .invalid=${this._idError} - .value=${this._entityId} - @value-changed=${this._idChanged} - > -
- ${this.hass.localize( - "ui.panel.config.script.editor.modes.description", - "documentation_link", - html` + ${this.hass.localize( - "ui.panel.config.script.editor.modes.documentation" - )}` - )} -
-+ +
++ ${this.hass!.localize( + "ui.panel.config.tags.detail.usage", + "companion_link", + html`${this.hass!.localize( + "ui.panel.config.tags.detail.companion_apps" + )}` + )} +
++ ${this.hass.localize( + "ui.panel.config.tags.detail.usage", + "companion_link", + html`${this.hass!.localize( + "ui.panel.config.tags.detail.companion_apps" + )}` + )} +
++ + ${this.hass.localize("ui.panel.config.tags.learn_more")} + +
+ `, + }); + } + private async _fetchTags() { this._tags = await fetchTags(this.hass); } diff --git a/src/panels/config/users/ha-config-users.ts b/src/panels/config/users/ha-config-users.ts index 30ed2e8093..09ca727a22 100644 --- a/src/panels/config/users/ha-config-users.ts +++ b/src/panels/config/users/ha-config-users.ts @@ -96,13 +96,14 @@ export class HaConfigUsers extends LitElement { .data=${this._users} @row-click=${this._editUser} hasFab + clickable >- [[localize('ui.panel.developer-tools.tabs.services.select_service')]] -
- +- [[localize('ui.panel.developer-tools.tabs.services.no_description')]] -
- - -[[_description]]
-- [[localize('ui.panel.developer-tools.tabs.services.column_parameter')]] - | -- [[localize('ui.panel.developer-tools.tabs.services.column_description')]] - | -- [[localize('ui.panel.developer-tools.tabs.services.column_example')]] - | -
---|---|---|
- [[localize('ui.panel.developer-tools.tabs.services.no_parameters')]] - | -||
[[attribute.key]] |
- [[attribute.description]] | -[[attribute.example]] | -
+ [[localize('ui.panel.developer-tools.tabs.services.column_parameter')]] + | ++ [[localize('ui.panel.developer-tools.tabs.services.column_description')]] + | ++ [[localize('ui.panel.developer-tools.tabs.services.column_example')]] + | +
---|---|---|
[[attribute.key]] |
+ [[attribute.description]] | +[[attribute.example]] | +
+ ${this.hass.localize( + "ui.panel.developer-tools.tabs.templates.time" + )} +
+ ` + : ""} ${!this._templateResult?.listeners ? "" : this._templateResult.listeners.all ? html` -+
${this.hass.localize( "ui.panel.developer-tools.tabs.templates.all_listeners" )} - +
` : this._templateResult.listeners.domains.length || this._templateResult.listeners.entities.length ? html` -+
${this.hass.localize( "ui.panel.developer-tools.tabs.templates.listeners" )} - +
-
${this._templateResult.listeners.domains
.sort()
diff --git a/src/panels/history/ha-panel-history.ts b/src/panels/history/ha-panel-history.ts
index 8b59281ad6..566aa3c0da 100644
--- a/src/panels/history/ha-panel-history.ts
+++ b/src/panels/history/ha-panel-history.ts
@@ -16,6 +16,7 @@ import { haStyle } from "../../resources/styles";
import { HomeAssistant } from "../../types";
import type { DateRangePickerRanges } from "../../components/ha-date-range-picker";
import "../../components/ha-date-range-picker";
+import "../../components/entity/ha-entity-picker";
import { fetchDate, computeHistory } from "../../data/history";
import "../../components/ha-circular-progress";
@@ -77,6 +78,16 @@ class HaPanelHistory extends LitElement {
.ranges=${this._ranges}
@change=${this._dateRangeChanged}
>
+
+
${this._config.icon
? html`
@@ -204,11 +204,11 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
!("type" in conf)
+ (conf) => "entity" in conf
) as EntityConfig[]).map((conf) => conf.entity)}
>
`}
-
+
`}
${this._config.title}
` : ""}+ ${this.hass!.localize( + "ui.panel.lovelace.editor.edit_badges.panel_mode" + )} +
+ ` + : ""}