Compare commits

...

92 Commits

Author SHA1 Message Date
Ludeeus
a0b11eb357 Move partial backup logic to backend 2021-12-16 12:36:02 +00:00
J. Nick Koston
6f9b2ee569 Add hardware version to the device info card (#10914) 2021-12-16 05:16:23 -06:00
Bram Kragten
4ebdca2a46 Bumped version to 20211215.0 2021-12-15 13:36:34 +01:00
Philip Allgaier
fc700fdaf0 Outline new collapsable area in state dev tools + auto-expand (#10917) 2021-12-15 13:15:50 +01:00
Philip Allgaier
d8e12f4280 Add tooltips and aria-labels to media player buttons (#10881) 2021-12-13 16:33:34 -08:00
krazos
86114758c3 Add group to input row domains to fix mobile focus issue (#10897) 2021-12-13 16:30:21 -08:00
Joakim Sørensen
792278cf17 Hide stop for hassio (#10905)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-12-13 16:29:01 -08:00
Joakim Sørensen
b8832f2121 Change entrypoint for Settings (#10904) 2021-12-13 16:08:19 -08:00
Joakim Sørensen
76339c90f7 Show app configuration in sidebar for non-admin users (#10890) 2021-12-13 16:06:46 -08:00
Bram Kragten
b3d4451035 Not valid config, but we support it in the editor (#10893) 2021-12-13 11:01:41 -08:00
Joakim Sørensen
dc58481918 Fix overriding username suggestion (#10899) 2021-12-13 18:56:44 +01:00
Philip Allgaier
14af735507 Fix tooltip and aria-label for password input field (#10898)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-12-13 16:32:45 +00:00
Joakim Sørensen
a7b558b64a Add no update available message (#10891) 2021-12-13 17:20:38 +01:00
Joakim Sørensen
b7665bef6f Don't backup core for supervisor/os updates (#10886) 2021-12-13 10:53:02 +01:00
Christopher Toth
5ec37a35f1 Fix all instances where HTML ARIA-ROLE should actually just be role (#10888) 2021-12-13 08:35:46 +00:00
Philip Allgaier
91bb2ddcc4 Make energy graph colors brighter in dark mode (#10789) 2021-12-12 14:10:30 +01:00
Bram Kragten
85168b3a35 Bumped version to 20211212.0 2021-12-12 13:37:28 +01:00
Bram Kragten
942150cda2 Remove milliseconds from state trigger when 0 (#10879) 2021-12-12 12:27:14 +00:00
Philip Allgaier
2606d55895 Add tooltips and aria-labels to climate modes (#10875) 2021-12-12 13:25:05 +01:00
Philip Allgaier
1f671198aa Fix tooltip and aria-label for ZWave JS log download (#10876) 2021-12-12 13:24:24 +01:00
Bram Kragten
deb65e7108 Fix button with images (#10872) 2021-12-12 13:19:32 +01:00
Philip Allgaier
cd00f7f874 Fix typo in cover close tilt translation key (#10871) 2021-12-11 20:59:42 +01:00
Bram Kragten
2b0359edba Bumped version to 20211211.0 2021-12-11 17:15:38 +01:00
Philip Allgaier
35e9687170 Replace mwc-icon-button with ha-icon-button in automation picker (#10858) 2021-12-11 17:15:16 +01:00
Bram Kragten
b730676914 Fix translations cover controls (#10868) 2021-12-11 17:13:43 +01:00
Bram Kragten
2890192c05 Fix formfield label touch (#10867) 2021-12-11 17:13:24 +01:00
Bram Kragten
bfb84a834f Still have manual input if camera is not supported (#10849)
* Still have manual input if camera is not supported

* Adjust & fix
2021-12-11 17:12:41 +01:00
Philip Allgaier
ca6fd6c770 Prevent quickbar command entry duplicates (#10861) 2021-12-11 17:01:24 +01:00
Joakim Sørensen
585648ac4c Revert "handle ha-radio and ha-checkbox in ha-formfield" (#10863) 2021-12-10 23:30:35 -08:00
Matthias de Baat
bec5c564b6 Update blueprint description (#10854) 2021-12-10 11:18:05 -08:00
Erik Montnemery
48c66e6349 Tweak some energy related translation strings (#10852) 2021-12-10 09:49:53 -08:00
Bram Kragten
cea40610c0 Add base trigger to struct (#10851) 2021-12-10 14:44:40 +01:00
Bram Kragten
0c3fd8f3ad typo login -> log in (#10850) 2021-12-10 14:41:09 +01:00
Paulus Schoutsen
02bdeebc82 Bumped version to 20211209.0 2021-12-09 13:39:03 -08:00
Bram Kragten
60c7669d8f Put set state in expansion panel (#10845) 2021-12-09 13:38:27 -08:00
Bram Kragten
919bf94a03 Only add milliseconds when enabled or if it has a value (#10842) 2021-12-09 13:38:03 -08:00
Bram Kragten
ead5e288eb Use normal card color in narrow config screen too (#10843) 2021-12-09 13:37:45 -08:00
Bram Kragten
add8a702cc Change select camera UI, remove manual QR input (#10844) 2021-12-09 13:37:30 -08:00
Paulus Schoutsen
39774c0e02 Allow trigger reconnect from external bus (#10819) 2021-12-09 13:30:20 -08:00
Joakim Sørensen
149f381bc3 Make dashboard entries translatable (#10831) 2021-12-09 09:59:27 -08:00
Bram Kragten
faccb12430 Fix keep me logged in (#10835) 2021-12-09 09:57:11 -08:00
Paulus Schoutsen
7039bae9be Disable local only option for system generated users (#10827) 2021-12-09 11:22:32 +01:00
Bram Kragten
0a7b703d57 Clear warnings when yaml changes (#10820) 2021-12-08 09:12:09 +01:00
Joakim Sørensen
24e8028e8f Use _version_latest for change log URL (#10821) 2021-12-07 23:37:06 +01:00
Paulus Schoutsen
8412cd71cb Bumped version to 20211206.0 2021-12-06 15:11:11 -08:00
Joakim Sørensen
5c78b74005 Reorder configuration (#10817) 2021-12-06 15:10:50 -08:00
Bram Kragten
2459477ec4 Add struct for state trigger and condition (#10811)
* Add struct for state trigger and condition

* remove `milliseconds` from struct
2021-12-06 20:13:32 +01:00
Raman Gupta
a065740c91 zwave_js config param should only be a toggle if there are 2 states (#10812) 2021-12-06 10:49:23 -08:00
Bram Kragten
f3104d3c93 Fix zwavejs provisioned view (#10809) 2021-12-06 10:47:35 -08:00
Paulus Schoutsen
1916c179b4 Merge pull request #10816 from spacegaier/issue-10751 2021-12-06 10:43:33 -08:00
Philip Allgaier
e8b9766eb6 Fix camera stream rendering in area card without picture (#10815) 2021-12-06 10:39:09 -08:00
Philip Allgaier
ff7a2c8cb7 Fix clearing of picture (e.g. area and person config) 2021-12-06 19:29:51 +01:00
Bram Kragten
7ccde2cb41 Fix disabled date input (#10813) 2021-12-06 17:25:23 +00:00
Bram Kragten
d6b9b16f02 Add toggle for camera view in area card (#10810)
Co-authored-by: Zack Barett <arnett.zackary@gmail.com>
2021-12-06 18:02:32 +01:00
Joakim Sørensen
66df15007a Fetch cloud and updates on reconnect (#10808) 2021-12-06 12:54:16 +01:00
Philip Allgaier
f164d21c44 Filter out invalid text input for input_text (#10797) 2021-12-06 12:53:45 +01:00
Philip Allgaier
911d322aac Mark more trigger fields as optional (#10798) 2021-12-06 12:52:38 +01:00
Joakim Sørensen
419879ee7a Fix core changelog URL (#10804) 2021-12-06 12:51:56 +01:00
Joakim Sørensen
c3e1a2edf0 Use ha-logo-svg on info page (#10807) 2021-12-06 12:51:21 +01:00
Philip Allgaier
8f5751d5bb Use correct label in area card editor (#10799) 2021-12-05 12:09:06 -06:00
Philip Allgaier
4095450476 Add switch to input row domains to fix mobile focus issue (#10792) 2021-12-04 11:43:14 +01:00
Bram Kragten
e61f587c51 Bumped version to 20211203.0 2021-12-03 18:07:07 +01:00
Bram Kragten
d43d19190e Fix entity marker (#10787) 2021-12-03 17:04:50 +00:00
Bram Kragten
a283acaabf safari doesnt support overflow-wrap: anywhere 2021-12-03 18:04:03 +01:00
Philip Allgaier
ea18fc0078 Ensure we always have an active theme name (fixes dark theme issues) (#10780)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-12-03 17:02:54 +00:00
Bram Kragten
1df11e9bf1 Use groupBy (#10786) 2021-12-03 08:42:23 -08:00
Bram Kragten
c71b2e6b9d Add provisioned device overview to zwave js (#10785) 2021-12-03 08:34:26 -08:00
Bram Kragten
db4aa05bf4 Differentiate between assigned and targeting scene/automations/script (#10781) 2021-12-03 08:21:26 -08:00
Bram Kragten
a54a2a54f8 Add support for local only users (#10784)
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
2021-12-03 16:34:34 +01:00
Philip Allgaier
0bcb4d0e09 Restore flex alignment for select and input-select rows (#10783) 2021-12-03 16:19:00 +01:00
Bram Kragten
95dbc811d3 Allow overriding device class (#10777) 2021-12-03 16:07:49 +01:00
Philip Allgaier
e28a11964e Use correct styling for cloud certificate dialog (#10782) 2021-12-03 15:08:49 +01:00
Bram Kragten
46a9e36516 Guard for non numeric states (#10775)
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
2021-12-03 12:53:50 +01:00
Paulus Schoutsen
e99f20c4f3 Tweak ZJS dashboard (#10772) 2021-12-03 10:51:50 +01:00
Joakim Sørensen
2100603cdc Remove handling of the supervisor panel from the sidebar (#10773) 2021-12-03 10:49:42 +01:00
Joakim Sørensen
da4942aca3 Add default icons for button entities (#10774) 2021-12-03 09:11:29 +01:00
Paulus Schoutsen
7c78fb314e Show add devices fab on devices page for ZJS (#10771) 2021-12-03 08:42:39 +01:00
Paulus Schoutsen
5bc2468cbc Bumped version to 20211202.0 2021-12-02 14:31:09 -08:00
Bram Kragten
a580904c52 Use chips for button rows (#10770) 2021-12-02 23:29:52 +01:00
Bram Kragten
48d12ceafe Group entities in area card by domain (#10767)
* Group entities in area card by domain

* Update hui-area-card.ts

* Update

* Add background color when no image

* Add camera support

* exclude unavailable states

* Update hui-area-card.ts
2021-12-02 23:15:18 +01:00
Carlos Garcia Saura
60ce805b3b Update hui-graph-header-footer.ts (#10476) 2021-12-02 13:32:38 -08:00
Paulus Schoutsen
251416b51d Add missing translation (#10769) 2021-12-02 13:01:19 -08:00
Bram Kragten
c41c6eedd8 Remove thingtalk cleanup create new automation dialog (#10748)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-12-02 11:26:41 -08:00
Joakim Sørensen
6877fd9e00 Hide updates for dev as well (#10761) 2021-12-02 17:32:18 +01:00
Joakim Sørensen
4cc104a99f Use add-ons for mobile header (#10760) 2021-12-02 17:31:41 +01:00
Joakim Sørensen
6494177821 Fix SU sidebar issues (#10757) 2021-12-02 17:31:09 +01:00
Joakim Sørensen
cea1a62867 handle ha-radio and ha-checkbox in ha-formfield (#10759) 2021-12-02 17:30:10 +01:00
rianadon
a6b5262d02 Use unit system definitions for weather units (#10657) 2021-12-02 17:27:23 +01:00
Joakim Sørensen
2a5fc5181e Fix create backup checkbox (#10756) 2021-12-02 11:54:05 +01:00
Joakim Sørensen
2fe8f5ff27 Use puzzle for addons and blur entries on click (#10755) 2021-12-02 11:05:14 +01:00
Philip Allgaier
0c75d5afc9 Make graph colors themable (#10698) 2021-12-02 10:49:46 +01:00
Philip Allgaier
cf062bf0f4 Fix pointer/more-info inconsistencies for entity rows (#10025)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-12-02 10:48:30 +01:00
125 changed files with 2519 additions and 1381 deletions

View File

@@ -52,17 +52,13 @@ class DemoBlackWhiteRow extends LitElement {
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: false,
},
"default",
{ dark: true }
);
applyThemesOnElement(this.shadowRoot!.querySelector(".dark"), {
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: true,
theme: "default",
});
}
handleSubmit(ev) {

View File

@@ -159,17 +159,13 @@ export class DemoHaAlert extends LitElement {
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: false,
},
"default",
{ dark: true }
);
applyThemesOnElement(this.shadowRoot!.querySelector(".dark"), {
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: true,
theme: "default",
});
}
static get styles() {

View File

@@ -206,6 +206,7 @@ const createDeviceRegistryEntries = (
model: "Mock Device",
name: "Tag Reader",
sw_version: null,
hw_version: "1.0.0",
id: "mock-device-id",
identifiers: [],
via_device_id: null,

View File

@@ -35,11 +35,14 @@ class HassioDashboard extends LitElement {
hasFab
>
<span slot="header">
${this.supervisor.localize("panel.dashboard")}
${this.supervisor.localize(
atLeastVersion(this.hass.config.version, 2021, 12)
? "panel.addons"
: "panel.dashboard"
)}
</span>
<div class="content">
${this.hass.config.version.includes("dev") ||
!atLeastVersion(this.hass.config.version, 2021, 12)
${!atLeastVersion(this.hass.config.version, 2021, 12)
? html`
<hassio-update
.hass=${this.hass}

View File

@@ -29,10 +29,6 @@ import {
HassioAddonDetails,
updateHassioAddon,
} from "../../../src/data/hassio/addon";
import {
createHassioPartialBackup,
HassioPartialBackupCreateParams,
} from "../../../src/data/hassio/backup";
import {
extractApiErrorMessage,
ignoreSupervisorError,
@@ -48,7 +44,6 @@ import "../../../src/layouts/hass-subpage";
import "../../../src/layouts/hass-tabs-subpage";
import { SUPERVISOR_UPDATE_NAMES } from "../../../src/panels/config/dashboard/ha-config-updates";
import { HomeAssistant, Route } from "../../../src/types";
import { documentationUrl } from "../../../src/util/documentation-url";
import { addonArchIsSupported, extractChangelog } from "../util/addon";
declare global {
@@ -60,7 +55,6 @@ declare global {
type updateType = "os" | "supervisor" | "core" | "addon";
const changelogUrl = (
hass: HomeAssistant,
entry: updateType,
version: string
): string | undefined => {
@@ -68,17 +62,19 @@ const changelogUrl = (
return undefined;
}
if (entry === "core") {
return version?.includes("dev")
return version.includes("dev")
? "https://github.com/home-assistant/core/commits/dev"
: documentationUrl(hass, "/latest-release-notes/");
: version.includes("b")
? "https://next.home-assistant.io/latest-release-notes/"
: "https://www.home-assistant.io/latest-release-notes/";
}
if (entry === "os") {
return version?.includes("dev")
return version.includes("dev")
? "https://github.com/home-assistant/operating-system/commits/dev"
: `https://github.com/home-assistant/operating-system/releases/tag/${version}`;
}
if (entry === "supervisor") {
return version?.includes("dev")
return version.includes("dev")
? "https://github.com/home-assistant/supervisor/commits/main"
: `https://github.com/home-assistant/supervisor/releases/tag/${version}`;
}
@@ -103,7 +99,7 @@ class UpdateAvailableCard extends LitElement {
@state() private _addonInfo?: HassioAddonDetails;
@state() private _action: "backup" | "update" | null = null;
@state() private _updating = false;
@state() private _error?: string;
@@ -120,7 +116,7 @@ class UpdateAvailableCard extends LitElement {
return html``;
}
const changelog = changelogUrl(this.hass, this._updateType, this._version);
const changelog = changelogUrl(this._updateType, this._version_latest);
return html`
<ha-card
@@ -132,7 +128,13 @@ class UpdateAvailableCard extends LitElement {
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
${this._action === null
${this._version === this._version_latest
? html`<p>
${this.supervisor.localize("update_available.no_update", {
name: this._name,
})}
</p>`
: !this._updating
? html`
${this._changelogContent
? html`
@@ -166,18 +168,13 @@ class UpdateAvailableCard extends LitElement {
: html`<ha-circular-progress alt="Updating" size="large" active>
</ha-circular-progress>
<p class="progress-text">
${this._action === "update"
? this.supervisor.localize("update_available.updating", {
name: this._name,
version: this._version_latest,
})
: this.supervisor.localize(
"update_available.creating_backup",
{ name: this._name }
)}
${this.supervisor.localize("update_available.updating", {
name: this._name,
version: this._version_latest,
})}
</p>`}
</div>
${this._action === null
${this._version !== this._version_latest && !this._updating
? html`
<div class="card-actions">
${changelog
@@ -194,7 +191,7 @@ class UpdateAvailableCard extends LitElement {
<ha-progress-button
.disabled=${!this._version ||
(this._shouldCreateBackup &&
this.supervisor.info.state !== "running")}
this.supervisor.info?.state !== "running")}
@click=${this._update}
raised
>
@@ -224,7 +221,14 @@ class UpdateAvailableCard extends LitElement {
}
get _shouldCreateBackup(): boolean {
return this.shadowRoot?.querySelector("ha-checkbox")?.checked || true;
if (this._updateType && !["core", "addon"].includes(this._updateType)) {
return false;
}
const checkbox = this.shadowRoot?.querySelector("ha-checkbox");
if (checkbox) {
return checkbox.checked;
}
return true;
}
get _version(): string {
@@ -306,37 +310,16 @@ class UpdateAvailableCard extends LitElement {
private async _update() {
this._error = undefined;
if (this._shouldCreateBackup) {
let backupArgs: HassioPartialBackupCreateParams;
if (this._updateType === "addon") {
backupArgs = {
name: `addon_${this.addonSlug}_${this._version}`,
addons: [this.addonSlug!],
homeassistant: false,
};
} else {
backupArgs = {
name: `${this._updateType}_${this._version}`,
folders: ["homeassistant"],
homeassistant: true,
};
}
this._action = "backup";
try {
await createHassioPartialBackup(this.hass, backupArgs);
} catch (err: any) {
this._error = extractApiErrorMessage(err);
this._action = null;
return;
}
}
this._action = "update";
this._updating = true;
try {
if (this._updateType === "addon") {
await updateHassioAddon(this.hass, this.addonSlug!);
await updateHassioAddon(
this.hass,
this.addonSlug!,
this._shouldCreateBackup
);
} else if (this._updateType === "core") {
await updateCore(this.hass);
await updateCore(this.hass, this._shouldCreateBackup);
} else if (this._updateType === "os") {
await updateOS(this.hass);
} else if (this._updateType === "supervisor") {
@@ -345,7 +328,7 @@ class UpdateAvailableCard extends LitElement {
} catch (err: any) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
this._error = extractApiErrorMessage(err);
this._action = null;
this._updating = false;
return;
}
}

View File

@@ -102,7 +102,7 @@
"fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2",
"hls.js": "^1.0.11",
"home-assistant-js-websocket": "^5.11.1",
"home-assistant-js-websocket": "^5.11.3",
"idb-keyval": "^5.1.3",
"intl-messageformat": "^9.9.1",
"js-yaml": "^4.1.0",

View File

@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20211201.0",
version="20211215.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/frontend",
author="The Home Assistant Authors",

View File

@@ -101,17 +101,13 @@ class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
this._fetchAuthProviders();
if (matchMedia("(prefers-color-scheme: dark)").matches) {
applyThemesOnElement(
document.documentElement,
{
default_theme: "default",
default_dark_theme: null,
themes: {},
darkMode: false,
},
"default",
{ dark: true }
);
applyThemesOnElement(document.documentElement, {
default_theme: "default",
default_dark_theme: null,
themes: {},
darkMode: true,
theme: "default",
});
}
if (!this.redirectUri) {

View File

@@ -61,3 +61,14 @@ export const COLORS = [
export function getColorByIndex(index: number) {
return COLORS[index % COLORS.length];
}
export function getGraphColorByIndex(
index: number,
style: CSSStyleDeclaration
) {
// The CSS vars for the colors use range 1..n, so we need to adjust the index from the internal 0..n color index range.
return (
style.getPropertyValue(`--graph-color-${index + 1}`) ||
getColorByIndex(index)
);
}

View File

@@ -188,8 +188,9 @@ export const DOMAINS_WITH_MORE_INFO = [
"weather",
];
/** Domains that show no more info dialog. */
export const DOMAINS_HIDE_MORE_INFO = [
/** Domains that do not show the default more info dialog content (e.g. the attribute section)
* and do not have a separate more info (so not in DOMAINS_WITH_MORE_INFO). */
export const DOMAINS_HIDE_DEFAULT_MORE_INFO = [
"input_number",
"input_select",
"input_text",
@@ -198,6 +199,32 @@ export const DOMAINS_HIDE_MORE_INFO = [
"select",
];
/** Domains that render an input element instead of a text value when rendered in a row.
* Those rows should then not show a cursor pointer when hovered (which would normally
* be the default) unless the element itself enforces it (e.g. a button). Also those elements
* should not act as a click target to open the more info dialog (the row name and state icon
* still do of course) as the click might instead e.g. activate the input field that this row shows.
*/
export const DOMAINS_INPUT_ROW = [
"cover",
"fan",
"group",
"humidifier",
"input_boolean",
"input_datetime",
"input_number",
"input_select",
"input_text",
"light",
"lock",
"media_player",
"number",
"scene",
"script",
"select",
"switch",
];
/** Domains that should have the history hidden in the more info dialog. */
export const DOMAINS_MORE_INFO_NO_HISTORY = ["camera", "configurator", "scene"];

View File

@@ -23,9 +23,9 @@ let PROCESSED_THEMES: Record<string, ProcessedTheme> = {};
* Apply a theme to an element by setting the CSS variables on it.
*
* element: Element to apply theme on.
* themes: HASS theme information.
* selectedTheme: Selected theme.
* themeSettings: Settings such as selected dark mode and colors.
* themes: HASS theme information (e.g. active dark mode and globally active theme name).
* selectedTheme: Selected theme (used to override the globally active theme for this element).
* themeSettings: Additional settings such as selected colors.
*/
export const applyThemesOnElement = (
element,
@@ -33,31 +33,33 @@ export const applyThemesOnElement = (
selectedTheme?: string,
themeSettings?: Partial<HomeAssistant["selectedTheme"]>
) => {
let cacheKey = selectedTheme;
let themeRules: Partial<ThemeVars> = {};
// If there is no explicitly desired theme provided, we automatically
// use the active one from `themes`.
const themeToApply = selectedTheme || themes.theme;
// If there is no explicitly desired dark mode provided, we automatically
// use the active one from hass.themes.
if (!themeSettings || themeSettings?.dark === undefined) {
themeSettings = {
...themeSettings,
dark: themes.darkMode,
};
}
// use the active one from `themes`.
const darkMode =
themeSettings && themeSettings?.dark !== undefined
? themeSettings?.dark
: themes.darkMode;
if (themeSettings.dark) {
let cacheKey = themeToApply;
let themeRules: Partial<ThemeVars> = {};
if (darkMode) {
cacheKey = `${cacheKey}__dark`;
themeRules = { ...darkStyles };
}
if (selectedTheme === "default") {
if (themeToApply === "default") {
// Determine the primary and accent colors from the current settings.
// Fallbacks are implicitly the HA default blue and orange or the
// derived "darkStyles" values, depending on the light vs dark mode.
const primaryColor = themeSettings.primaryColor;
const accentColor = themeSettings.accentColor;
const primaryColor = themeSettings?.primaryColor;
const accentColor = themeSettings?.accentColor;
if (themeSettings.dark && primaryColor) {
if (darkMode && primaryColor) {
themeRules["app-header-background-color"] = hexBlend(
primaryColor,
"#121212",
@@ -98,17 +100,17 @@ export const applyThemesOnElement = (
// Custom theme logic (not relevant for default theme, since it would override
// the derived calculations from above)
if (
selectedTheme &&
selectedTheme !== "default" &&
themes.themes[selectedTheme]
themeToApply &&
themeToApply !== "default" &&
themes.themes[themeToApply]
) {
// Apply theme vars that are relevant for all modes (but extract the "modes" section first)
const { modes, ...baseThemeRules } = themes.themes[selectedTheme];
const { modes, ...baseThemeRules } = themes.themes[themeToApply];
themeRules = { ...themeRules, ...baseThemeRules };
// Apply theme vars for the specific mode if available
if (modes) {
if (themeSettings?.dark) {
if (darkMode) {
themeRules = { ...themeRules, ...modes.dark };
} else {
themeRules = { ...themeRules, ...modes.light };

View File

@@ -1,30 +1,33 @@
import {
mdiAccount,
mdiAccountArrowRight,
mdiAirHumidifierOff,
mdiAirHumidifier,
mdiFlash,
mdiAirHumidifierOff,
mdiBluetooth,
mdiBluetoothConnect,
mdiCalendar,
mdiCast,
mdiCastConnected,
mdiClock,
mdiEmoticonDead,
mdiFlash,
mdiGestureTapButton,
mdiLanConnect,
mdiLanDisconnect,
mdiLockOpen,
mdiLock,
mdiLockAlert,
mdiLockClock,
mdiLock,
mdiCastConnected,
mdiCast,
mdiEmoticonDead,
mdiLockOpen,
mdiPackageUp,
mdiPowerPlug,
mdiPowerPlugOff,
mdiRestart,
mdiSleep,
mdiTimerSand,
mdiToggleSwitch,
mdiToggleSwitchOff,
mdiZWave,
mdiClock,
mdiCalendar,
mdiWeatherNight,
mdiZWave,
} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
/**
@@ -52,6 +55,16 @@ export const domainIcon = (
case "binary_sensor":
return binarySensorIcon(compareState, stateObj);
case "button":
switch (stateObj?.attributes.device_class) {
case "restart":
return mdiRestart;
case "update":
return mdiPackageUp;
default:
return mdiGestureTapButton;
}
case "cover":
return coverIcon(compareState, stateObj);

View File

@@ -1,7 +1,7 @@
import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
import { html, LitElement, PropertyValues } from "lit";
import { property, state } from "lit/decorators";
import { getColorByIndex } from "../../common/color/colors";
import { getGraphColorByIndex } from "../../common/color/colors";
import {
formatNumber,
numberFormatToLocale,
@@ -164,7 +164,7 @@ class StateHistoryChartLine extends LitElement {
const pushData = (timestamp: Date, datavalues: any[] | null) => {
if (!datavalues) return;
if (timestamp > endTime) {
// Drop datapoints that are after the requested endTime. This could happen if
// Drop data points that are after the requested endTime. This could happen if
// endTime is "now" and client time is not in sync with server time.
return;
}
@@ -190,7 +190,7 @@ class StateHistoryChartLine extends LitElement {
color?: string
) => {
if (!color) {
color = getColorByIndex(colorIndex);
color = getGraphColorByIndex(colorIndex, computedStyles);
colorIndex++;
}
data.push({

View File

@@ -2,7 +2,7 @@ import type { ChartData, ChartDataset, ChartOptions } from "chart.js";
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { getColorByIndex } from "../../common/color/colors";
import { getGraphColorByIndex } from "../../common/color/colors";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import { computeDomain } from "../../common/entity/compute_domain";
import { numberFormatToLocale } from "../../common/number/format_number";
@@ -71,7 +71,7 @@ const getColor = (
stateColorMap.set(stateString, color);
return color;
}
const color = getColorByIndex(colorIndex);
const color = getGraphColorByIndex(colorIndex, computedStyles);
colorIndex++;
stateColorMap.set(stateString, color);
return color;

View File

@@ -13,7 +13,7 @@ import {
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { getColorByIndex } from "../../common/color/colors";
import { getGraphColorByIndex } from "../../common/color/colors";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { computeStateName } from "../../common/entity/compute_state_name";
import {
@@ -59,6 +59,8 @@ class StatisticsChart extends LitElement {
@state() private _chartOptions?: ChartOptions;
private _computedStyle?: CSSStyleDeclaration;
protected shouldUpdate(changedProps: PropertyValues): boolean {
return changedProps.size > 1 || !changedProps.has("hass");
}
@@ -72,6 +74,10 @@ class StatisticsChart extends LitElement {
}
}
public firstUpdated() {
this._computedStyle = getComputedStyle(this);
}
protected render(): TemplateResult {
if (!isComponentLoaded(this.hass, "history")) {
return html`<div class="info">
@@ -261,7 +267,7 @@ class StatisticsChart extends LitElement {
) => {
if (!dataValues) return;
if (timestamp > endTime) {
// Drop datapoints that are after the requested endTime. This could happen if
// Drop data points that are after the requested endTime. This could happen if
// endTime is "now" and client time is not in sync with server time.
return;
}
@@ -280,7 +286,7 @@ class StatisticsChart extends LitElement {
prevValues = dataValues;
};
const color = getColorByIndex(colorIndex);
const color = getGraphColorByIndex(colorIndex, this._computedStyle!);
colorIndex++;
const statTypes: this["statTypes"] = [];

View File

@@ -46,6 +46,7 @@ class HaAlert extends LitElement {
rtl: this.rtl,
[this.alertType]: true,
})}"
role="alert"
>
<div class="icon ${this.title ? "" : "no-title"}">
<slot name="icon">
@@ -121,6 +122,7 @@ class HaAlert extends LitElement {
}
.main-content {
overflow-wrap: anywhere;
word-break: break-word;
margin-left: 8px;
margin-right: 0;
}

View File

@@ -23,6 +23,10 @@ class HaBluePrintPicker extends LitElement {
@property({ type: Boolean }) public disabled = false;
public open() {
this.shadowRoot!.querySelector("paper-dropdown-menu-light")!.open();
}
private _processedBlueprints = memoizeOne((blueprints?: Blueprints) => {
if (!blueprints) {
return [];

View File

@@ -56,6 +56,7 @@ export class HaRelatedFilterButtonMenu extends LitElement {
return html`
<ha-icon-button
@click=${this._handleClick}
.label=${this.hass.localize("ui.components.related-filter-menu.filter")}
.path=${mdiFilterVariant}
></ha-icon-button>
<mwc-menu-surface

View File

@@ -14,9 +14,11 @@ import { customElement, property } from "lit/decorators";
export class HaChip extends LitElement {
@property({ type: Boolean }) public hasIcon = false;
@property({ type: Boolean }) public noText = false;
protected render(): TemplateResult {
return html`
<div class="mdc-chip">
<div class="mdc-chip ${this.noText ? "no-text" : ""}">
${this.hasIcon
? html`<div class="mdc-chip__icon mdc-chip__icon--leading">
<slot name="icon"></slot>
@@ -43,6 +45,10 @@ export class HaChip extends LitElement {
color: var(--ha-chip-text-color, var(--primary-text-color));
}
.mdc-chip.no-text {
padding: 0 10px;
}
.mdc-chip:hover {
color: var(--ha-chip-text-color, var(--primary-text-color));
}
@@ -51,6 +57,10 @@ export class HaChip extends LitElement {
--mdc-icon-size: 20px;
color: var(--ha-chip-icon-color, var(--ha-chip-text-color));
}
.mdc-chip.no-text
.mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden) {
margin-right: -4px;
}
`;
}
}

View File

@@ -35,7 +35,7 @@ class HaCoverControls extends LitElement {
hidden: !supportsOpen(this.stateObj),
})}
.label=${this.hass.localize(
"ui.dialogs.more_info_control.open_cover"
"ui.dialogs.more_info_control.cover.open_cover"
)}
@click=${this._onOpenTap}
.disabled=${this._computeOpenDisabled()}
@@ -47,7 +47,7 @@ class HaCoverControls extends LitElement {
hidden: !supportsStop(this.stateObj),
})}
.label=${this.hass.localize(
"ui.dialogs.more_info_control.stop_cover"
"ui.dialogs.more_info_control.cover.stop_cover"
)}
.path=${mdiStop}
@click=${this._onStopTap}
@@ -58,7 +58,7 @@ class HaCoverControls extends LitElement {
hidden: !supportsClose(this.stateObj),
})}
.label=${this.hass.localize(
"ui.dialogs.more_info_control.close_cover"
"ui.dialogs.more_info_control.cover.close_cover"
)}
@click=${this._onCloseTap}
.disabled=${this._computeClosedDisabled()}

View File

@@ -30,7 +30,7 @@ class HaCoverTiltControls extends LitElement {
invisible: !supportsOpenTilt(this.stateObj),
})}
.label=${this.hass.localize(
"ui.dialogs.more_info_control.open_tilt_cover"
"ui.dialogs.more_info_control.cover.open_tilt_cover"
)}
.path=${mdiArrowTopRight}
@click=${this._onOpenTiltTap}
@@ -40,7 +40,9 @@ class HaCoverTiltControls extends LitElement {
class=${classMap({
invisible: !supportsStopTilt(this.stateObj),
})}
.label=${this.hass.localize("ui.dialogs.more_info_control.stop_cover")}
.label=${this.hass.localize(
"ui.dialogs.more_info_control.cover.stop_cover"
)}
.path=${mdiStop}
@click=${this._onStopTiltTap}
.disabled=${this.stateObj.state === UNAVAILABLE}
@@ -50,7 +52,7 @@ class HaCoverTiltControls extends LitElement {
invisible: !supportsCloseTilt(this.stateObj),
})}
.label=${this.hass.localize(
"ui.dialogs.more_info_control.close_tilt_cover"
"ui.dialogs.more_info_control.cover.close_tilt_cover"
)}
.path=${mdiArrowBottomLeft}
@click=${this._onCloseTiltTap}

View File

@@ -96,7 +96,11 @@ export class HaDateInput extends LitElement {
attr-for-value="value"
.i18n=${i18n}
>
<paper-input .label=${this.label} no-label-float>
<paper-input
.label=${this.label}
.disabled=${this.disabled}
no-label-float
>
<ha-svg-icon slot="suffix" .path=${mdiCalendar}></ha-svg-icon>
</paper-input>
</vaadin-date-picker-light>`;

View File

@@ -122,14 +122,20 @@ class HaDurationInput extends LitElement {
value %= 60;
}
const newValue: HaDurationData = {
hours,
minutes,
seconds: this._seconds,
};
if (this.enableMillisecond || this._milliseconds) {
newValue.milliseconds = this._milliseconds;
}
newValue[unit] = value;
fireEvent(this, "value-changed", {
value: {
hours,
minutes,
seconds: this._seconds,
milliseconds: this._milliseconds,
...{ [unit]: value },
},
value: newValue,
});
}
}

View File

@@ -66,7 +66,7 @@ export class HaFormString extends LitElement implements HaFormElement {
${isPassword
? html`<ha-icon-button
toggles
.label="Click to toggle between masked and clear password"
.label=${`${this._unmaskedPassword ? "Hide" : "Show"} password`}
@click=${this._toggleUnmaskedPassword}
tabindex="-1"
.path=${this._unmaskedPassword ? mdiEyeOff : mdiEye}

View File

@@ -1,10 +1,28 @@
import { Formfield } from "@material/mwc-formfield";
import { css, CSSResultGroup } from "lit";
import { customElement } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
@customElement("ha-formfield")
// @ts-expect-error
export class HaFormfield extends Formfield {
protected _labelClick() {
const input = this.input;
if (input) {
input.focus();
switch (input.tagName) {
case "HA-CHECKBOX":
case "HA-RADIO":
(input as any).checked = !(input as any).checked;
fireEvent(input, "change");
break;
default:
input.click();
break;
}
}
}
protected static get styles(): CSSResultGroup {
return [
Formfield.styles,

View File

@@ -29,7 +29,7 @@ export class HaIconOverflowMenu extends LitElement {
protected render(): TemplateResult {
return html`
${this.narrow
? html` <!-- Collapsed Representation for Small Screens -->
? html` <!-- Collapsed representation for small screens -->
<ha-button-menu
@click=${this._handleIconOverflowMenuOpened}
@closed=${this._handleIconOverflowMenuClosed}
@@ -59,8 +59,7 @@ export class HaIconOverflowMenu extends LitElement {
)}
</ha-button-menu>`
: html`
<!-- Icon Representation for Big Screens -->
<!-- Icon representation for big screens -->
${this.items.map((item) =>
item.narrowOnly
? ""
@@ -70,13 +69,12 @@ export class HaIconOverflowMenu extends LitElement {
${item.tooltip}
</paper-tooltip>`
: ""}
<mwc-icon-button
<ha-icon-button
@click=${item.action}
.label=${item.label}
.path=${item.path}
.disabled=${item.disabled}
>
<ha-svg-icon .path=${item.path}></ha-svg-icon>
</mwc-icon-button>
></ha-icon-button>
</div> `
)}
`}

View File

@@ -39,6 +39,7 @@ export class HaPictureUpload extends LitElement {
.uploading=${this._uploading}
.value=${this.value ? html`<img .src=${this.value} />` : ""}
@file-picked=${this._handleFilePicked}
@change=${this._handleFileCleared}
accept="image/png, image/jpeg, image/gif"
></ha-file-upload>
`;
@@ -53,6 +54,10 @@ export class HaPictureUpload extends LitElement {
}
}
private async _handleFileCleared() {
this.value = null;
}
private async _cropFile(file: File) {
if (!["image/png", "image/jpeg", "image/gif"].includes(file.type)) {
showAlertDialog(this, {

View File

@@ -1,6 +1,8 @@
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-select/mwc-select";
import type { Select } from "@material/mwc-select/mwc-select";
import "@material/mwc-textfield/mwc-textfield";
import type { TextField } from "@material/mwc-textfield/mwc-textfield";
import { mdiCamera } from "@mdi/js";
import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import type QrScanner from "qr-scanner";
@@ -8,6 +10,8 @@ import { fireEvent } from "../common/dom/fire_event";
import { stopPropagation } from "../common/dom/stop_propagation";
import { LocalizeFunc } from "../common/translations/localize";
import "./ha-alert";
import "./ha-button-menu";
import "@material/mwc-button/mwc-button";
@customElement("ha-qr-scanner")
class HaQrScanner extends LitElement {
@@ -25,6 +29,8 @@ class HaQrScanner extends LitElement {
@query("#canvas-container", true) private _canvasContainer!: HTMLDivElement;
@query("mwc-textfield") private _manualInput?: TextField;
public disconnectedCallback(): void {
super.disconnectedCallback();
this._qrNotFoundCount = 0;
@@ -58,34 +64,53 @@ class HaQrScanner extends LitElement {
}
protected render(): TemplateResult {
return html`${this._cameras && this._cameras.length > 1
? html`<mwc-select
.label=${this.localize(
"ui.panel.config.zwave_js.add_node.select_camera"
)}
fixedMenuPosition
naturalMenuWidth
@closed=${stopPropagation}
@selected=${this._cameraChanged}
>
${this._cameras!.map(
(camera) => html`
<mwc-list-item .value=${camera.id}>${camera.label}</mwc-list-item>
`
)}
</mwc-select>`
: ""}
${this._error
return html`${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
${navigator.mediaDevices
? html`<video></video>
<div id="canvas-container"></div>`
: html`<ha-alert alert-type="warning"
>${!window.isSecureContext
? "You can only use your camera to scan a QR core when using HTTPS."
: "Your browser doesn't support QR scanning."}</ha-alert
>`}`;
<div id="canvas-container">
${this._cameras && this._cameras.length > 1
? html`<ha-button-menu
corner="BOTTOM_START"
fixed
@closed=${stopPropagation}
>
<ha-icon-button
slot="trigger"
.label=${this.localize(
"ui.components.qr-scanner.select_camera"
)}
.path=${mdiCamera}
></ha-icon-button>
${this._cameras!.map(
(camera) => html`
<mwc-list-item
.value=${camera.id}
@click=${this._cameraChanged}
>${camera.label}</mwc-list-item
>
`
)}
</ha-button-menu>`
: ""}
</div>`
: html`<ha-alert alert-type="warning">
${!window.isSecureContext
? this.localize("ui.components.qr-scanner.only_https_supported")
: this.localize("ui.components.qr-scanner.not_supported")}
</ha-alert>
<p>${this.localize("ui.components.qr-scanner.manual_input")}</p>
<div class="row">
<mwc-textfield
.label=${this.localize("ui.components.qr-scanner.enter_qr_code")}
@keyup=${this._manualKeyup}
@paste=${this._manualPaste}
></mwc-textfield>
<mwc-button @click=${this._manualSubmit}
>${this.localize("ui.common.submit")}</mwc-button
>
</div>`}`;
}
private async _loadQrScanner() {
@@ -134,17 +159,49 @@ class HaQrScanner extends LitElement {
fireEvent(this, "qr-code-scanned", { value: qrCodeString });
};
private _manualKeyup(ev: KeyboardEvent) {
if (ev.key === "Enter") {
this._qrCodeScanned((ev.target as TextField).value);
}
}
private _manualPaste(ev: ClipboardEvent) {
this._qrCodeScanned(
// @ts-ignore
(ev.clipboardData || window.clipboardData).getData("text")
);
}
private _manualSubmit() {
this._qrCodeScanned(this._manualInput!.value);
}
private _cameraChanged(ev: CustomEvent): void {
this._qrScanner?.setCamera((ev.target as Select).value);
this._qrScanner?.setCamera((ev.target as any).value);
}
static styles = css`
canvas {
width: 100%;
}
mwc-select {
width: 100%;
margin-bottom: 16px;
#canvas-container {
position: relative;
}
ha-button-menu {
position: absolute;
bottom: 8px;
right: 8px;
background: #727272b2;
color: white;
border-radius: 50%;
}
.row {
display: flex;
align-items: center;
}
mwc-textfield {
flex: 1;
margin-right: 8px;
}
`;
}

View File

@@ -3,12 +3,12 @@ import {
mdiBell,
mdiCalendar,
mdiCart,
mdiCellphoneCog,
mdiChartBox,
mdiClose,
mdiCog,
mdiFormatListBulletedType,
mdiHammer,
mdiHomeAssistant,
mdiLightningBolt,
mdiMenu,
mdiMenuOpen,
@@ -44,6 +44,10 @@ import {
PersistentNotification,
subscribeNotifications,
} from "../data/persistent_notification";
import {
ExternalConfig,
getExternalConfig,
} from "../external_app/external_config";
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant, PanelInfo, Route } from "../types";
@@ -53,7 +57,7 @@ import "./ha-menu-button";
import "./ha-svg-icon";
import "./user/ha-user-badge";
const SHOW_AFTER_SPACER = ["config", "developer-tools", "hassio"];
const SHOW_AFTER_SPACER = ["config", "developer-tools"];
const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body;
@@ -63,7 +67,6 @@ const SORT_VALUE_URL_PATHS = {
logbook: 3,
history: 4,
"developer-tools": 9,
hassio: 10,
config: 11,
};
@@ -72,7 +75,6 @@ const PANEL_ICONS = {
config: mdiCog,
"developer-tools": mdiHammer,
energy: mdiLightningBolt,
hassio: mdiHomeAssistant,
history: mdiChartBox,
logbook: mdiFormatListBulletedType,
lovelace: mdiViewDashboard,
@@ -190,6 +192,8 @@ class HaSidebar extends LitElement {
@property({ type: Boolean }) public editMode = false;
@state() private _externalConfig?: ExternalConfig;
@state() private _notifications?: PersistentNotification[];
@state() private _renderEmptySortable = false;
@@ -236,6 +240,7 @@ class HaSidebar extends LitElement {
changedProps.has("expanded") ||
changedProps.has("narrow") ||
changedProps.has("alwaysExpand") ||
changedProps.has("_externalConfig") ||
changedProps.has("_notifications") ||
changedProps.has("editMode") ||
changedProps.has("_renderEmptySortable") ||
@@ -266,6 +271,12 @@ class HaSidebar extends LitElement {
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
if (this.hass && this.hass.auth.external) {
getExternalConfig(this.hass.auth.external).then((conf) => {
this._externalConfig = conf;
});
}
subscribeNotifications(this.hass.connection, (notifications) => {
this._notifications = notifications;
});
@@ -340,10 +351,8 @@ class HaSidebar extends LitElement {
this._hiddenPanels
);
// Show the update-available as beeing part of configuration
const selectedPanel = this.route.path?.startsWith(
"/hassio/update-available"
)
// Show the supervisor as beeing part of configuration
const selectedPanel = this.route.path?.startsWith("/hassio/")
? "config"
: this.hass.panelUrl;
@@ -363,6 +372,7 @@ class HaSidebar extends LitElement {
: this._renderPanels(beforeSpacer)}
${this._renderSpacer()}
${this._renderPanels(afterSpacer)}
${this._renderExternalConfiguration()}
</paper-listbox>
`;
}
@@ -370,9 +380,7 @@ class HaSidebar extends LitElement {
private _renderPanels(panels: PanelInfo[]) {
return panels.map((panel) =>
this._renderPanel(
panel.url_path === "hassio"
? "config/dashboard?focusedPath=hassio"
: panel.url_path,
panel.url_path,
panel.url_path === this.hass.defaultPanel
? panel.title || this.hass.localize("panel.states")
: this.hass.localize(`panel.${panel.title}`) || panel.title,
@@ -394,7 +402,7 @@ class HaSidebar extends LitElement {
) {
return html`
<a
aria-role="option"
role="option"
href=${`/${urlPath}`}
data-panel=${urlPath}
tabindex="-1"
@@ -499,7 +507,7 @@ class HaSidebar extends LitElement {
>
<paper-icon-item
class="notifications"
aria-role="option"
role="option"
@click=${this._handleShowNotificationDrawer}
>
<ha-svg-icon slot="item-icon" .path=${mdiBell}></ha-svg-icon>
@@ -530,7 +538,7 @@ class HaSidebar extends LitElement {
href="/profile"
data-panel="panel"
tabindex="-1"
aria-role="option"
role="option"
aria-label=${this.hass.localize("panel.profile")}
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
@@ -549,6 +557,43 @@ class HaSidebar extends LitElement {
</a>`;
}
private _renderExternalConfiguration() {
return html`${!this.hass.user?.is_admin &&
this._externalConfig &&
this._externalConfig.hasSettingsScreen
? html`
<a
role="option"
aria-label=${this.hass.localize(
"ui.sidebar.external_app_configuration"
)}
href="#external-app-configuration"
tabindex="-1"
@click=${this._handleExternalAppConfiguration}
@mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave}
>
<paper-icon-item>
<ha-svg-icon
slot="item-icon"
.path=${mdiCellphoneCog}
></ha-svg-icon>
<span class="item-text">
${this.hass.localize("ui.sidebar.external_app_configuration")}
</span>
</paper-icon-item>
</a>
`
: ""}`;
}
private _handleExternalAppConfiguration(ev: Event) {
ev.preventDefault();
this.hass.auth.external!.fireMessage({
type: "config_screen/show",
});
}
private get _tooltip() {
return this.shadowRoot!.querySelector(".tooltip")! as HTMLDivElement;
}

View File

@@ -2,11 +2,8 @@ import { LitElement, html, css } from "lit";
import { property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types";
class HaEntityMarker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: "entity-id" }) public entityId?: string;
@property({ attribute: "entity-name" }) public entityName?: string;
@@ -26,9 +23,7 @@ class HaEntityMarker extends LitElement {
? html`<div
class="entity-picture"
style=${styleMap({
"background-image": `url(${this.hass.hassUrl(
this.entityPicture
)})`,
"background-image": `url(${this.entityPicture})`,
})}
></div>`
: this.entityName}
@@ -69,3 +64,9 @@ class HaEntityMarker extends LitElement {
}
customElements.define("ha-entity-marker", HaEntityMarker);
declare global {
interface HTMLElementTagNameMap {
"ha-entity-marker": HaEntityMarker;
}
}

View File

@@ -412,7 +412,9 @@ export class HaMap extends ReactiveElement {
<ha-entity-marker
entity-id="${getEntityId(entity)}"
entity-name="${entityName}"
entity-picture="${entityPicture || ""}"
entity-picture="${
entityPicture ? this.hass.hassUrl(entityPicture) : ""
}"
${
typeof entity !== "string"
? `entity-color="${entity.color}"`

View File

@@ -13,6 +13,7 @@ export interface DeviceRegistryEntry {
model: string | null;
name: string | null;
sw_version: string | null;
hw_version: string | null;
via_device_id: string | null;
area_id: string | null;
name_by_user: string | null;

View File

@@ -21,6 +21,8 @@ export interface ExtEntityRegistryEntry extends EntityRegistryEntry {
capabilities: Record<string, unknown>;
original_name?: string;
original_icon?: string;
device_class?: string;
original_device_class?: string;
}
export interface UpdateEntityRegistryEntryResult {
@@ -32,6 +34,7 @@ export interface UpdateEntityRegistryEntryResult {
export interface EntityRegistryEntryUpdateParams {
name?: string | null;
icon?: string | null;
device_class?: string | null;
area_id?: string | null;
disabled_by?: string | null;
new_entity_id?: string;

View File

@@ -302,7 +302,8 @@ export const installHassioAddon = async (
export const updateHassioAddon = async (
hass: HomeAssistant,
slug: string
slug: string,
backup: boolean
): Promise<void> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
@@ -310,11 +311,13 @@ export const updateHassioAddon = async (
endpoint: `/store/addons/${slug}/update`,
method: "post",
timeout: null,
data: { backup: backup },
});
} else {
await hass.callApi<HassioResponse<void>>(
"POST",
`hassio/addons/${slug}/update`
`hassio/addons/${slug}/update`,
{ backup: backup }
);
}
};

View File

@@ -156,6 +156,7 @@ export interface MediaPlayerThumbnail {
export interface ControlButton {
icon: string;
// Used as key for action as well as tooltip and aria-label translation key
action: string;
}

View File

@@ -6,15 +6,18 @@ export const restartCore = async (hass: HomeAssistant) => {
await hass.callService("homeassistant", "restart");
};
export const updateCore = async (hass: HomeAssistant) => {
export const updateCore = async (hass: HomeAssistant, backup: boolean) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: "/core/update",
method: "post",
timeout: null,
data: { backup: backup },
});
} else {
await hass.callApi<HassioResponse<void>>("POST", `hassio/core/update`);
await hass.callApi<HassioResponse<void>>("POST", `hassio/core/update`, {
backup: backup,
});
}
};

View File

@@ -13,6 +13,7 @@ export interface User {
name: string;
is_owner: boolean;
is_active: boolean;
local_only: boolean;
system_generated: boolean;
group_ids: string[];
credentials: Credential[];
@@ -22,6 +23,7 @@ export interface UpdateUserParams {
name?: User["name"];
is_active?: User["is_active"];
group_ids?: User["group_ids"];
local_only?: boolean;
}
export const fetchUsers = async (hass: HomeAssistant) =>
@@ -33,12 +35,14 @@ export const createUser = async (
hass: HomeAssistant,
name: string,
// eslint-disable-next-line: variable-name
group_ids?: User["group_ids"]
group_ids?: User["group_ids"],
local_only?: boolean
) =>
hass.callWS<{ user: User }>({
type: "config/auth/create",
name,
group_ids,
local_only,
});
export const updateUser = async (

View File

@@ -152,17 +152,11 @@ export const getWeatherUnit = (
hass: HomeAssistant,
measure: string
): string => {
const lengthUnit = hass.config.unit_system.length || "";
switch (measure) {
case "pressure":
return lengthUnit === "km" ? "hPa" : "inHg";
case "wind_speed":
return `${lengthUnit}/h`;
case "visibility":
case "length":
return lengthUnit;
return hass.config.unit_system.length || "";
case "precipitation":
return lengthUnit === "km" ? "mm" : "in";
return hass.config.unit_system.accumulated_precipitation || "";
case "humidity":
case "precipitation_probability":
return "%";

View File

@@ -23,6 +23,8 @@ export interface Themes {
// in theme picker, this property will still contain either true or false based on
// what has been determined via system preferences and support from the selected theme.
darkMode: boolean;
// Currently globally active theme name
theme: string;
}
const fetchThemes = (conn) =>

View File

@@ -205,6 +205,16 @@ export const enum NodeStatus {
Alive,
}
export interface ZwaveJSProvisioningEntry {
/** The device specific key (DSK) in the form aaaaa-bbbbb-ccccc-ddddd-eeeee-fffff-11111-22222 */
dsk: string;
security_classes: SecurityClass[];
additional_properties: {
nodeId?: number;
[prop: string]: any;
};
}
export interface RequestedGrant {
/**
* An array of security classes that are requested or to be granted.
@@ -265,6 +275,15 @@ export const setZwaveDataCollectionPreference = (
opted_in,
});
export const fetchZwaveProvisioningEntries = (
hass: HomeAssistant,
entry_id: string
): Promise<ZwaveJSProvisioningEntry[]> =>
hass.callWS({
type: "zwave_js/get_provisioning_entries",
entry_id,
});
export const subscribeAddZwaveNode = (
hass: HomeAssistant,
entry_id: string,
@@ -350,6 +369,19 @@ export const provisionZwaveSmartStartNode = (
planned_provisioning_entry,
});
export const unprovisionZwaveSmartStartNode = (
hass: HomeAssistant,
entry_id: string,
dsk?: string,
node_id?: number
): Promise<QRProvisioningInformation> =>
hass.callWS({
type: "zwave_js/unprovision_smart_start_node",
entry_id,
dsk,
node_id,
});
export const fetchZwaveNodeStatus = (
hass: HomeAssistant,
entry_id: string,

View File

@@ -65,6 +65,9 @@ class MoreInfoMediaPlayer extends LitElement {
action=${control.action}
@click=${this._handleClick}
.path=${control.icon}
.label=${this.hass.localize(
`ui.card.media_player.${control.action}`
)}
>
</ha-icon-button>
`

View File

@@ -1,6 +1,6 @@
import type { HassEntity } from "home-assistant-js-websocket";
import {
DOMAINS_HIDE_MORE_INFO,
DOMAINS_HIDE_DEFAULT_MORE_INFO,
DOMAINS_WITH_MORE_INFO,
} from "../../common/const";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
@@ -40,7 +40,7 @@ export const domainMoreInfoType = (domain: string): string => {
if (DOMAINS_WITH_MORE_INFO.includes(domain)) {
return domain;
}
if (DOMAINS_HIDE_MORE_INFO.includes(domain)) {
if (DOMAINS_HIDE_DEFAULT_MORE_INFO.includes(domain)) {
return "hidden";
}
return "default";

View File

@@ -99,6 +99,8 @@ export class QuickBar extends LitElement {
private _focusSet = false;
private _focusListElement?: ListItem | null;
public async showDialog(params: QuickBarParams) {
this._commandMode = params.commandMode || this._toggleIfAlreadyOpened();
this._initializeItemsIfNeeded();
@@ -317,7 +319,8 @@ export class QuickBar extends LitElement {
} else if (ev.code === "ArrowDown") {
ev.preventDefault();
this._getItemAtIndex(0)?.focus();
this._getItemAtIndex(1)?.focus();
this._focusSet = true;
this._focusListElement = this._getItemAtIndex(0);
}
}
@@ -350,6 +353,11 @@ export class QuickBar extends LitElement {
this._initializeItemsIfNeeded();
this._filter = this._search;
} else {
if (this._focusSet && this._focusListElement) {
this._focusSet = false;
// @ts-ignore
this._focusListElement.rippleHandlers.endFocus();
}
this._debouncedSetFilter(this._search);
}
}
@@ -366,12 +374,14 @@ export class QuickBar extends LitElement {
private _setFocusFirstListItem() {
// @ts-ignore
this._getItemAtIndex(0)?.rippleHandlers.startFocus();
this._focusListElement = this._getItemAtIndex(0);
}
private _handleListItemKeyDown(ev: KeyboardEvent) {
const isSingleCharacter = ev.key.length === 1;
const isFirstListItem =
(ev.target as HTMLElement).getAttribute("index") === "0";
this._focusListElement = ev.target as ListItem;
if (ev.key === "ArrowUp") {
if (isFirstListItem) {
this._filterInputField?.focus();
@@ -511,7 +521,13 @@ export class QuickBar extends LitElement {
if (page.component) {
const info = this._getNavigationInfoFromConfig(page);
if (info) {
// Add to list, but only if we do not already have an entry for the same path and component
if (
info &&
!items.some(
(e) => e.path === info.path && e.component === info.component
)
) {
items.push(info);
}
}

View File

@@ -37,6 +37,25 @@ declare global {
}
}
const clearUrlParams = () => {
// Clear auth data from url if we have been able to establish a connection
if (location.search.includes("auth_callback=1")) {
const searchParams = new URLSearchParams(location.search);
// https://github.com/home-assistant/home-assistant-js-websocket/blob/master/lib/auth.ts
// Remove all data from QueryCallbackData type
searchParams.delete("auth_callback");
searchParams.delete("code");
searchParams.delete("state");
searchParams.delete("storeToken");
const search = searchParams.toString();
history.replaceState(
null,
"",
`${location.pathname}${search ? `?${search}` : ""}`
);
}
};
const authProm = isExternal
? () =>
import("../external_app/external_auth").then(({ createExternalAuth }) =>
@@ -52,23 +71,7 @@ const authProm = isExternal
const connProm = async (auth) => {
try {
const conn = await createConnection({ auth });
// Clear auth data from url if we have been able to establish a connection
if (location.search.includes("auth_callback=1")) {
const searchParams = new URLSearchParams(location.search);
// https://github.com/home-assistant/home-assistant-js-websocket/blob/master/lib/auth.ts
// Remove all data from QueryCallbackData type
searchParams.delete("auth_callback");
searchParams.delete("code");
searchParams.delete("state");
searchParams.delete("storeToken");
const search = searchParams.toString();
history.replaceState(
null,
"",
`${location.pathname}${search ? `?${search}` : ""}`
);
}
clearUrlParams();
return { auth, conn };
} catch (err: any) {
if (err !== ERR_INVALID_AUTH) {
@@ -85,6 +88,7 @@ const connProm = async (auth) => {
}
auth = await authProm();
const conn = await createConnection({ auth });
clearUrlParams();
return { auth, conn };
}
};

View File

@@ -2,7 +2,7 @@
* Auth class that connects to a native app for authentication.
*/
import { Auth } from "home-assistant-js-websocket";
import { ExternalMessaging, InternalMessage } from "./external_messaging";
import { ExternalMessaging, EMMessage } from "./external_messaging";
const CALLBACK_SET_TOKEN = "externalAuthSetToken";
const CALLBACK_REVOKE_TOKEN = "externalAuthRevokeToken";
@@ -36,7 +36,7 @@ declare global {
postMessage(payload: BasePayload);
};
externalBus: {
postMessage(payload: InternalMessage);
postMessage(payload: EMMessage);
};
};
};

View File

@@ -1,3 +1,4 @@
import { Connection } from "home-assistant-js-websocket";
import {
externalForwardConnectionEvents,
externalForwardHaptics,
@@ -7,39 +8,50 @@ const CALLBACK_EXTERNAL_BUS = "externalBus";
interface CommandInFlight {
resolve: (data: any) => void;
reject: (err: ExternalError) => void;
reject: (err: EMError) => void;
}
export interface InternalMessage {
export interface EMMessage {
id?: number;
type: string;
payload?: unknown;
}
interface ExternalError {
interface EMError {
code: string;
message: string;
}
interface ExternalMessageResult {
interface EMMessageResultSuccess {
id: number;
type: "result";
success: true;
result: unknown;
}
interface ExternalMessageResultError {
interface EMMessageResultError {
id: number;
type: "result";
success: false;
error: ExternalError;
error: EMError;
}
type ExternalMessage = ExternalMessageResult | ExternalMessageResultError;
interface EMExternalMessageRestart {
id: number;
type: "command";
command: "restart";
}
type ExternalMessage =
| EMMessageResultSuccess
| EMMessageResultError
| EMExternalMessageRestart;
export class ExternalMessaging {
public commands: { [msgId: number]: CommandInFlight } = {};
public connection?: Connection;
public cache: Record<string, any> = {};
public msgId = 0;
@@ -54,7 +66,7 @@ export class ExternalMessaging {
* Send message to external app that expects a response.
* @param msg message to send
*/
public sendMessage<T>(msg: InternalMessage): Promise<T> {
public sendMessage<T>(msg: EMMessage): Promise<T> {
const msgId = ++this.msgId;
msg.id = msgId;
@@ -69,7 +81,9 @@ export class ExternalMessaging {
* Send message to external app without expecting a response.
* @param msg message to send
*/
public fireMessage(msg: InternalMessage) {
public fireMessage(
msg: EMMessage | EMMessageResultSuccess | EMMessageResultError
) {
if (!msg.id) {
msg.id = ++this.msgId;
}
@@ -82,6 +96,43 @@ export class ExternalMessaging {
console.log("Receiving message from external app", msg);
}
if (msg.type === "command") {
if (!this.connection) {
// eslint-disable-next-line no-console
console.warn("Received command without having connection set", msg);
this.fireMessage({
id: msg.id,
type: "result",
success: false,
error: {
code: "commands_not_init",
message: `Commands connection not set`,
},
});
} else if (msg.command === "restart") {
this.connection.socket.close();
this.fireMessage({
id: msg.id,
type: "result",
success: true,
result: null,
});
} else {
// eslint-disable-next-line no-console
console.warn("Received unknown command", msg.command, msg);
this.fireMessage({
id: msg.id,
type: "result",
success: false,
error: {
code: "unknown_command",
message: `Unknown command ${msg.command}`,
},
});
}
return;
}
const pendingCmd = this.commands[msg.id];
if (!pendingCmd) {
@@ -99,7 +150,7 @@ export class ExternalMessaging {
}
}
protected _sendExternal(msg: InternalMessage) {
protected _sendExternal(msg: EMMessage) {
if (__DEV__) {
// eslint-disable-next-line no-console
console.log("Sending message to external app", msg);

View File

@@ -10,6 +10,9 @@ export const demoConfig: HassConfig = {
mass: "kg",
temperature: "°C",
volume: "L",
pressure: "Pa",
wind_speed: "m/s",
accumulated_precipitation: "mm",
},
components: [
"notify.html5",

View File

@@ -201,6 +201,7 @@ export const provideHass = (
default_dark_theme: null,
themes: {},
darkMode: false,
theme: "default",
},
panels: demoPanels,
services: demoServices,

View File

@@ -127,7 +127,9 @@ export class HassRouterPage extends ReactiveElement {
// Update the url if we know where we're mounted.
if (route) {
navigate(`${route.prefix}/${result}`, { replace: true });
navigate(`${route.prefix}/${result}${location.search}`, {
replace: true,
});
}
}
}

View File

@@ -51,7 +51,9 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) {
const path = curPath();
if (["", "/"].includes(path)) {
navigate(`/${getStorageDefaultPanelUrlPath()}`, { replace: true });
navigate(`/${getStorageDefaultPanelUrlPath()}${location.search}`, {
replace: true,
});
}
this._route = {
prefix: "",

View File

@@ -133,17 +133,13 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) {
import("./particles");
}
if (matchMedia("(prefers-color-scheme: dark)").matches) {
applyThemesOnElement(
document.documentElement,
{
default_theme: "default",
default_dark_theme: null,
themes: {},
darkMode: false,
},
"default",
{ dark: true }
);
applyThemesOnElement(document.documentElement, {
default_theme: "default",
default_dark_theme: null,
themes: {},
darkMode: true,
theme: "default",
});
}
}

View File

@@ -95,8 +95,11 @@ class OnboardingCreateUser extends LitElement {
private _handleValueChanged(
ev: PolymerChangedEvent<HaFormDataContainer>
): void {
const nameChanged = ev.detail.value.name !== this._newUser.name;
this._newUser = ev.detail.value;
this._maybePopulateUsername();
if (nameChanged) {
this._maybePopulateUsername();
}
this._formError.password_confirm =
this._newUser.password !== this._newUser.password_confirm
? this.localize(

View File

@@ -35,6 +35,11 @@ import {
loadAreaRegistryDetailDialog,
showAreaRegistryDetailDialog,
} from "./show-dialog-area-registry-detail";
import { computeDomain } from "../../../common/entity/compute_domain";
import { SceneEntity } from "../../../data/scene";
import { ScriptEntity } from "../../../data/script";
import { AutomationEntity } from "../../../data/automation";
import { groupBy } from "../../../common/util/group-by";
@customElement("ha-config-area-page")
class HaConfigAreaPage extends LitElement {
@@ -131,6 +136,10 @@ class HaConfigAreaPage extends LitElement {
this.entities
);
const grouped = groupBy(entities, (entity) =>
computeDomain(entity.entity_id)
);
return html`
<hass-tabs-subpage
.hass=${this.hass}
@@ -221,19 +230,22 @@ class HaConfigAreaPage extends LitElement {
)}
>
${entities.length
? entities.map(
(entity) =>
html`
<paper-item
@click=${this._openEntity}
.entity=${entity}
>
<paper-item-body>
${computeEntityRegistryName(this.hass, entity)}
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
`
? entities.map((entity) =>
["scene", "script", "automation"].includes(
computeDomain(entity.entity_id)
)
? ""
: html`
<paper-item
@click=${this._openEntity}
.entity=${entity}
>
<paper-item-body>
${computeEntityRegistryName(this.hass, entity)}
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
`
)
: html`
<paper-item class="no-link"
@@ -251,48 +263,44 @@ class HaConfigAreaPage extends LitElement {
.header=${this.hass.localize(
"ui.panel.config.devices.automation.automations"
)}
>${this._related?.automation?.length
? this._related.automation.map((automation) => {
const entityState = this.hass.states[automation];
return entityState
? html`
<div>
<a
href=${ifDefined(
entityState.attributes.id
? `/config/automation/edit/${entityState.attributes.id}`
: undefined
)}
>
<paper-item
.disabled=${!entityState.attributes.id}
>
<paper-item-body>
${computeStateName(entityState)}
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
</a>
${!entityState.attributes.id
? html`
<paper-tooltip animation-delay="0">
${this.hass.localize(
"ui.panel.config.devices.cant_edit"
)}
</paper-tooltip>
`
: ""}
</div>
`
: "";
})
: html`
>
${grouped.automation?.length
? html`<h3>Assigned to this area:</h3>
${grouped.automation.map((entity) => {
const entityState = this.hass.states[
entity.entity_id
] as AutomationEntity | undefined;
return entityState
? this._renderAutomation(entityState)
: "";
})}`
: ""}
${this._related?.automation?.filter(
(entityId) =>
!grouped.automation?.find(
(entity) => entity.entity_id === entityId
)
).length
? html`<h3>Targeting this area:</h3>
${this._related.automation.map((scene) => {
const entityState = this.hass.states[scene] as
| AutomationEntity
| undefined;
return entityState
? this._renderAutomation(entityState)
: "";
})}`
: ""}
${!grouped.automation?.length &&
!this._related?.automation?.length
? html`
<paper-item class="no-link"
>${this.hass.localize(
"ui.panel.config.devices.automation.no_automations"
)}</paper-item
>
`}
`
: ""}
</ha-card>
`
: ""}
@@ -304,48 +312,40 @@ class HaConfigAreaPage extends LitElement {
.header=${this.hass.localize(
"ui.panel.config.devices.scene.scenes"
)}
>${this._related?.scene?.length
? this._related.scene.map((scene) => {
const entityState = this.hass.states[scene];
return entityState
? html`
<div>
<a
href=${ifDefined(
entityState.attributes.id
? `/config/scene/edit/${entityState.attributes.id}`
: undefined
)}
>
<paper-item
.disabled=${!entityState.attributes.id}
>
<paper-item-body>
${computeStateName(entityState)}
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
</a>
${!entityState.attributes.id
? html`
<paper-tooltip animation-delay="0">
${this.hass.localize(
"ui.panel.config.devices.cant_edit"
)}
</paper-tooltip>
`
: ""}
</div>
`
: "";
})
: html`
>
${grouped.scene?.length
? html`<h3>Assigned to this area:</h3>
${grouped.scene.map((entity) => {
const entityState =
this.hass.states[entity.entity_id];
return entityState
? this._renderScene(entityState)
: "";
})}`
: ""}
${this._related?.scene?.filter(
(entityId) =>
!grouped.scene?.find(
(entity) => entity.entity_id === entityId
)
).length
? html`<h3>Targeting this area:</h3>
${this._related.scene.map((scene) => {
const entityState = this.hass.states[scene];
return entityState
? this._renderScene(entityState)
: "";
})}`
: ""}
${!grouped.scene?.length && !this._related?.scene?.length
? html`
<paper-item class="no-link"
>${this.hass.localize(
"ui.panel.config.devices.scene.no_scenes"
)}</paper-item
>
`}
`
: ""}
</ha-card>
`
: ""}
@@ -355,31 +355,43 @@ class HaConfigAreaPage extends LitElement {
.header=${this.hass.localize(
"ui.panel.config.devices.script.scripts"
)}
>${this._related?.script?.length
? this._related.script.map((script) => {
const entityState = this.hass.states[script];
return entityState
? html`
<a
href=${`/config/script/edit/${entityState.entity_id}`}
>
<paper-item>
<paper-item-body>
${computeStateName(entityState)}
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
</a>
`
: "";
})
: html`
<paper-item class="no-link">
${this.hass.localize(
>
${grouped.script?.length
? html`<h3>Assigned to this area:</h3>
${grouped.script.map((entity) => {
const entityState = this.hass.states[
entity.entity_id
] as ScriptEntity | undefined;
return entityState
? this._renderScript(entityState)
: "";
})}`
: ""}
${this._related?.script?.filter(
(entityId) =>
!grouped.script?.find(
(entity) => entity.entity_id === entityId
)
).length
? html`<h3>Targeting this area:</h3>
${this._related.script.map((scene) => {
const entityState = this.hass.states[scene] as
| ScriptEntity
| undefined;
return entityState
? this._renderScript(entityState)
: "";
})}`
: ""}
${!grouped.script?.length && !this._related?.script?.length
? html`
<paper-item class="no-link"
>${this.hass.localize(
"ui.panel.config.devices.script.no_scripts"
)}</paper-item
>
`}
`
: ""}
</ha-card>
`
: ""}
@@ -389,6 +401,63 @@ class HaConfigAreaPage extends LitElement {
`;
}
private _renderScene(entityState: SceneEntity) {
return html`<div>
<a
href=${ifDefined(
entityState.attributes.id
? `/config/scene/edit/${entityState.attributes.id}`
: undefined
)}
>
<paper-item .disabled=${!entityState.attributes.id}>
<paper-item-body> ${computeStateName(entityState)} </paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
</a>
${!entityState.attributes.id
? html`
<paper-tooltip animation-delay="0">
${this.hass.localize("ui.panel.config.devices.cant_edit")}
</paper-tooltip>
`
: ""}
</div>`;
}
private _renderAutomation(entityState: AutomationEntity) {
return html`<div>
<a
href=${ifDefined(
entityState.attributes.id
? `/config/automation/edit/${entityState.attributes.id}`
: undefined
)}
>
<paper-item .disabled=${!entityState.attributes.id}>
<paper-item-body> ${computeStateName(entityState)} </paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
</a>
${!entityState.attributes.id
? html`
<paper-tooltip animation-delay="0">
${this.hass.localize("ui.panel.config.devices.cant_edit")}
</paper-tooltip>
`
: ""}
</div>`;
}
private _renderScript(entityState: ScriptEntity) {
return html`<a href=${`/config/script/edit/${entityState.entity_id}`}>
<paper-item>
<paper-item-body> ${computeStateName(entityState)} </paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
</a>`;
}
private async _findRelated() {
this._related = await findRelated(this.hass, "area", this.areaId);
}
@@ -457,6 +526,13 @@ class HaConfigAreaPage extends LitElement {
align-items: center;
}
h3 {
margin: 0;
padding: 0 16px;
font-weight: 500;
color: var(--secondary-text-color);
}
img {
border-radius: var(--ha-card-border-radius, 4px);
width: 100%;

View File

@@ -50,11 +50,8 @@ export class HaConfigAreasDashboard extends LitElement {
let noServicesInArea = 0;
let noEntitiesInArea = 0;
const devicesInArea = new Set();
for (const device of devices) {
if (device.area_id === area.area_id) {
devicesInArea.add(device.id);
if (device.entry_type === "service") {
noServicesInArea++;
} else {
@@ -64,11 +61,7 @@ export class HaConfigAreasDashboard extends LitElement {
}
for (const entity of entities) {
if (
entity.area_id
? entity.area_id === area.area_id
: devicesInArea.has(entity.device_id)
) {
if (entity.area_id === area.area_id) {
noEntitiesInArea++;
}
}

View File

@@ -138,7 +138,8 @@ export default class HaAutomationConditionEditor extends LitElement {
if (!ev.detail.isValid) {
return;
}
fireEvent(this, "value-changed", { value: ev.detail.value });
// @ts-ignore
fireEvent(this, "value-changed", { value: ev.detail.value, yaml: true });
}
static get styles(): CSSResultGroup {

View File

@@ -109,6 +109,7 @@ export default class HaAutomationConditionRow extends LitElement {
: ""}
<ha-automation-condition-editor
@ui-mode-not-available=${this._handleUiModeNotAvailable}
@value-changed=${this._handleChangeEvent}
.yamlMode=${this._yamlMode}
.hass=${this.hass}
.condition=${this.condition}
@@ -127,6 +128,12 @@ export default class HaAutomationConditionRow extends LitElement {
}
}
private _handleChangeEvent(ev: CustomEvent) {
if (ev.detail.yaml) {
this._warnings = undefined;
}
}
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:

View File

@@ -1,17 +1,27 @@
import "@polymer/paper-input/paper-input";
import { html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import { assert, literal, object, optional, string, union } from "superstruct";
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/entity/ha-entity-attribute-picker";
import "../../../../../components/entity/ha-entity-picker";
import "../../../../../components/ha-duration-input";
import { StateCondition } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types";
import { forDictStruct } from "../../structs";
import {
ConditionElement,
handleChangeEvent,
} from "../ha-automation-condition-row";
import "../../../../../components/ha-duration-input";
import { fireEvent } from "../../../../../common/dom/fire_event";
const stateConditionStruct = object({
condition: literal("state"),
entity_id: optional(string()),
attribute: optional(string()),
state: optional(string()),
for: optional(union([string(), forDictStruct])),
});
@customElement("ha-automation-condition-state")
export class HaStateCondition extends LitElement implements ConditionElement {
@@ -23,19 +33,14 @@ export class HaStateCondition extends LitElement implements ConditionElement {
return { entity_id: "", state: "" };
}
public willUpdate(changedProperties: PropertyValues): boolean {
if (
changedProperties.has("condition") &&
Array.isArray(this.condition?.state)
) {
fireEvent(
this,
"ui-mode-not-available",
Error(this.hass.localize("ui.errors.config.no_state_array_support"))
);
// We have to stop the update if state is an array.
// Otherwise the state will be changed to a comma-separated string by the input element.
return false;
public shouldUpdate(changedProperties: PropertyValues) {
if (changedProperties.has("condition")) {
try {
assert(this.condition, stateConditionStruct);
} catch (e: any) {
fireEvent(this, "ui-mode-not-available", e);
return false;
}
}
return true;
}

View File

@@ -1,24 +1,19 @@
import "@material/mwc-button";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { fireEvent } from "../../../common/dom/fire_event";
import { nextRender } from "../../../common/util/render-status";
import "../../../components/ha-blueprint-picker";
import "../../../components/ha-card";
import "../../../components/ha-circular-progress";
import { createCloseHeading } from "../../../components/ha-dialog";
import {
AutomationConfig,
showAutomationEditor,
} from "../../../data/automation";
import {
HassDialog,
replaceDialog,
} from "../../../dialogs/make-dialog-manager";
import { showAutomationEditor } from "../../../data/automation";
import { HassDialog } from "../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { showThingtalkDialog } from "./thingtalk/show-dialog-thingtalk";
import "@material/mwc-list/mwc-list-item";
import "../../../components/ha-icon-next";
import "@material/mwc-list/mwc-list";
@customElement("ha-dialog-new-automation")
class DialogNewAutomation extends LitElement implements HassDialog {
@@ -42,84 +37,52 @@ class DialogNewAutomation extends LitElement implements HassDialog {
return html`
<ha-dialog
open
hideActions
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.automation.dialog_new.header")
this.hass.localize("ui.panel.config.automation.dialog_new.how")
)}
>
<div>
${this.hass.localize("ui.panel.config.automation.dialog_new.how")}
<div class="container">
${isComponentLoaded(this.hass, "cloud")
? html`<ha-card outlined>
<div>
<h3>
${this.hass.localize(
"ui.panel.config.automation.dialog_new.thingtalk.header"
)}
</h3>
${this.hass.localize(
"ui.panel.config.automation.dialog_new.thingtalk.intro"
)}
<div class="side-by-side">
<paper-input
id="input"
.label=${this.hass.localize(
"ui.panel.config.automation.dialog_new.thingtalk.input_label"
)}
></paper-input>
<mwc-button @click=${this._thingTalk}
>${this.hass.localize(
"ui.panel.config.automation.dialog_new.thingtalk.create"
)}</mwc-button
>
</div>
</div>
</ha-card>`
: html``}
${isComponentLoaded(this.hass, "blueprint")
? html`<ha-card outlined>
<div>
<h3>
${this.hass.localize(
"ui.panel.config.automation.dialog_new.blueprint.use_blueprint"
)}
</h3>
<ha-blueprint-picker
@value-changed=${this._blueprintPicked}
.hass=${this.hass}
></ha-blueprint-picker>
</div>
</ha-card>`
: html``}
</div>
</div>
<mwc-button slot="primaryAction" @click=${this._blank}>
${this.hass.localize(
"ui.panel.config.automation.dialog_new.start_empty"
)}
</mwc-button>
<mwc-list>
<mwc-list-item twoline class="blueprint" @click=${this._blueprint}>
${this.hass.localize(
"ui.panel.config.automation.dialog_new.blueprint.use_blueprint"
)}
<span slot="secondary">
<ha-blueprint-picker
@value-changed=${this._blueprintPicked}
.hass=${this.hass}
></ha-blueprint-picker>
</span>
</mwc-list-item>
<li divider role="separator"></li>
<mwc-list-item hasmeta twoline @click=${this._blank}>
${this.hass.localize(
"ui.panel.config.automation.dialog_new.start_empty"
)}
<span slot="secondary">
${this.hass.localize(
"ui.panel.config.automation.dialog_new.start_empty_description"
)}
</span>
<ha-icon-next slot="meta"></ha-icon-next
></mwc-list-item>
</mwc-list>
</ha-dialog>
`;
}
private _thingTalk() {
replaceDialog();
showThingtalkDialog(this, {
callback: (config: Partial<AutomationConfig> | undefined) =>
showAutomationEditor(config),
input: this.shadowRoot!.querySelector("paper-input")!.value as string,
});
this.closeDialog();
}
private async _blueprintPicked(ev: CustomEvent) {
this.closeDialog();
await nextRender();
showAutomationEditor({ use_blueprint: { path: ev.detail.value } });
}
private async _blueprint() {
this.shadowRoot!.querySelector("ha-blueprint-picker")!.open();
}
private async _blank() {
this.closeDialog();
await nextRender();
@@ -131,38 +94,14 @@ class DialogNewAutomation extends LitElement implements HassDialog {
haStyle,
haStyleDialog,
css`
.container {
display: flex;
}
ha-card {
width: calc(50% - 8px);
margin: 4px;
}
ha-card div {
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
}
ha-card {
box-sizing: border-box;
padding: 8px;
mwc-list-item.blueprint {
height: 92px;
}
ha-blueprint-picker {
width: 100%;
margin-top: -16px;
}
.side-by-side {
display: flex;
flex-direction: row;
align-items: flex-end;
}
@media all and (max-width: 500px) {
.container {
flex-direction: column;
}
ha-card {
width: 100%;
}
ha-dialog {
--dialog-content-padding: 0;
}
`,
];

View File

@@ -315,10 +315,7 @@ class HaAutomationPicker extends LitElement {
};
private _createNew() {
if (
isComponentLoaded(this.hass, "cloud") ||
isComponentLoaded(this.hass, "blueprint")
) {
if (isComponentLoaded(this.hass, "blueprint")) {
showNewAutomationDialog(this);
} else {
navigate("/config/automation/edit/new");

View File

@@ -0,0 +1,13 @@
import { object, optional, number, string } from "superstruct";
export const baseTriggerStruct = object({
platform: string(),
id: optional(string()),
});
export const forDictStruct = object({
days: optional(number()),
hours: optional(number()),
minutes: optional(number()),
seconds: optional(number()),
});

View File

@@ -68,7 +68,7 @@ export const handleChangeEvent = (element: TriggerElement, ev: CustomEvent) => {
}
let newTrigger: Trigger;
if (!newVal) {
if (newVal === undefined || newVal === "") {
newTrigger = { ...element.trigger };
delete newTrigger[name];
} else {
@@ -291,6 +291,7 @@ export default class HaAutomationTriggerRow extends LitElement {
if (!ev.detail.isValid) {
return;
}
this._warnings = undefined;
fireEvent(this, "value-changed", { value: ev.detail.value });
}

View File

@@ -1,19 +1,41 @@
import "@polymer/paper-input/paper-input";
import { html, LitElement, PropertyValues } from "lit";
import { customElement, property } from "lit/decorators";
import {
assert,
assign,
literal,
object,
optional,
string,
union,
} from "superstruct";
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { hasTemplate } from "../../../../../common/string/has-template";
import "../../../../../components/entity/ha-entity-attribute-picker";
import "../../../../../components/entity/ha-entity-picker";
import "../../../../../components/ha-duration-input";
import { StateTrigger } from "../../../../../data/automation";
import { HomeAssistant } from "../../../../../types";
import "../../../../../components/ha-duration-input";
import { baseTriggerStruct, forDictStruct } from "../../structs";
import {
handleChangeEvent,
TriggerElement,
} from "../ha-automation-trigger-row";
const stateTriggerStruct = assign(
baseTriggerStruct,
object({
platform: literal("state"),
entity_id: optional(string()),
attribute: optional(string()),
from: optional(string()),
to: optional(string()),
for: optional(union([string(), forDictStruct])),
})
);
@customElement("ha-automation-trigger-state")
export class HaStateTrigger extends LitElement implements TriggerElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -24,9 +46,16 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
return { entity_id: "" };
}
public willUpdate(changedProperties: PropertyValues) {
public shouldUpdate(changedProperties: PropertyValues) {
if (!changedProperties.has("trigger")) {
return;
return true;
}
if (
this.trigger.for &&
typeof this.trigger.for === "object" &&
this.trigger.for.milliseconds === 0
) {
delete this.trigger.for.milliseconds;
}
// Check for templates in trigger. If found, revert to YAML mode.
if (this.trigger && hasTemplate(this.trigger)) {
@@ -35,7 +64,15 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
"ui-mode-not-available",
Error(this.hass.localize("ui.errors.config.no_template_editor_support"))
);
return false;
}
try {
assert(this.trigger, stateTriggerStruct);
} catch (e: any) {
fireEvent(this, "ui-mode-not-available", e);
return false;
}
return true;
}
protected render() {

View File

@@ -224,7 +224,7 @@ class HaBlueprintOverview extends LitElement {
.narrow=${this.narrow}
back-path="/config"
.route=${this.route}
.tabs=${configSections.automations}
.tabs=${configSections.blueprints}
.columns=${this._columns(this.narrow, this.hass.language)}
.data=${this._processedBlueprints(this.blueprints)}
id="entity_id"

View File

@@ -3,7 +3,7 @@ import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { formatDateTime } from "../../../../common/datetime/format_date_time";
import { fireEvent } from "../../../../common/dom/fire_event";
import { haStyle } from "../../../../resources/styles";
import { haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import type { CloudCertificateParams as CloudCertificateDialogParams } from "./show-dialog-cloud-certificate";
@@ -68,7 +68,7 @@ class DialogCloudCertificate extends LitElement {
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-max-width: 535px;

View File

@@ -1,4 +1,4 @@
import { mdiCellphoneCog, mdiCloudLock } from "@mdi/js";
import { mdiCloudLock } from "@mdi/js";
import "@polymer/app-layout/app-header/app-header";
import "@polymer/app-layout/app-toolbar/app-toolbar";
import {
@@ -11,7 +11,6 @@ import {
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { extractSearchParam } from "../../../common/url/search-params";
import "../../../components/ha-card";
import "../../../components/ha-icon-next";
import "../../../components/ha-menu-button";
@@ -111,32 +110,12 @@ class HaConfigDashboard extends LitElement {
></ha-config-navigation>
`
: ""}
${this._externalConfig?.hasSettingsScreen
? html`
<ha-config-navigation
.hass=${this.hass}
.narrow=${this.narrow}
.showAdvanced=${this.showAdvanced}
.pages=${[
{
path: "#external-app-configuration",
name: "Companion App",
description: "Location and notifications",
iconPath: mdiCellphoneCog,
iconColor: "#37474F",
core: true,
},
]}
@click=${this._handleExternalAppConfiguration}
></ha-config-navigation>
`
: ""}
<ha-config-navigation
.hass=${this.hass}
.narrow=${this.narrow}
.externalConfig=${this._externalConfig}
.showAdvanced=${this.showAdvanced}
.pages=${configSections.dashboard}
.focusedPath=${extractSearchParam("focusedPath")}
></ha-config-navigation>
</ha-card>`}
</ha-config-section>
@@ -144,13 +123,6 @@ class HaConfigDashboard extends LitElement {
`;
}
private _handleExternalAppConfiguration(ev: Event) {
ev.preventDefault();
this.hass.auth.external!.fireMessage({
type: "config_screen/show",
});
}
static get styles(): CSSResultGroup {
return [
haStyle,
@@ -159,7 +131,7 @@ class HaConfigDashboard extends LitElement {
border-bottom: var(--app-header-border-bottom);
--header-height: 55px;
}
ha-card:last-child {
:host(:not([narrow])) ha-card:last-child {
margin-bottom: 24px;
}
ha-config-section {
@@ -180,7 +152,7 @@ class HaConfigDashboard extends LitElement {
padding-bottom: 0;
}
:host([narrow]) ha-card {
background-color: var(--primary-background-color);
border-radius: 0;
box-shadow: unset;
}

View File

@@ -1,18 +1,12 @@
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item/paper-item-body";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property } from "lit/decorators";
import { canShowPage } from "../../../common/config/can_show_page";
import "../../../components/ha-card";
import "../../../components/ha-icon-next";
import { CloudStatus, CloudStatusLoggedIn } from "../../../data/cloud";
import { ExternalConfig } from "../../../external_app/external_config";
import { PageNavigation } from "../../../layouts/hass-tabs-subpage";
import { HomeAssistant } from "../../../types";
@@ -26,28 +20,19 @@ class HaConfigNavigation extends LitElement {
@property() public pages!: PageNavigation[];
@property() public focusedPath?: string | null;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
if (!this.focusedPath) {
return;
}
for (const a of this.shadowRoot!.querySelectorAll("a")) {
if (a.href.endsWith(this.focusedPath)) {
a.querySelector("paper-icon-item")?.focus();
break;
}
}
}
@property() public externalConfig?: ExternalConfig;
protected render(): TemplateResult {
return html`
${this.pages.map((page) =>
canShowPage(this.hass, page)
(
page.path === "#external-app-configuration"
? this.externalConfig?.hasSettingsScreen
: canShowPage(this.hass, page)
)
? html`
<a href=${page.path} aria-role="option" tabindex="-1">
<paper-icon-item>
<a href=${page.path} role="option" tabindex="-1">
<paper-icon-item @click=${this._entryClicked}>
<div
class=${page.iconColor ? "icon-background" : ""}
slot="item-icon"
@@ -58,8 +43,7 @@ class HaConfigNavigation extends LitElement {
<paper-item-body two-line>
${page.name ||
this.hass.localize(
page.translationKey ||
`ui.panel.config.${page.component}.caption`
`ui.panel.config.dashboard.${page.translationKey}.title`
)}
${page.component === "cloud" && (page.info as CloudStatus)
? page.info.logged_in
@@ -83,7 +67,7 @@ class HaConfigNavigation extends LitElement {
<div secondary>
${page.description ||
this.hass.localize(
`ui.panel.config.${page.component}.description`
`ui.panel.config.dashboard.${page.translationKey}.description`
)}
</div>
`}
@@ -97,6 +81,20 @@ class HaConfigNavigation extends LitElement {
`;
}
private _entryClicked(ev) {
ev.currentTarget.blur();
if (
ev.currentTarget.parentElement.href.endsWith(
"#external-app-configuration"
)
) {
ev.preventDefault();
this.hass.auth.external!.fireMessage({
type: "config_screen/show",
});
}
}
static get styles(): CSSResultGroup {
return css`
a {

View File

@@ -66,6 +66,17 @@ export class HaDeviceCard extends LitElement {
</div>
`
: ""}
${this.device.hw_version
? html`
<div class="extra-info">
${this.hass.localize(
"ui.panel.config.integrations.config_entry.hardware",
"version",
this.device.hw_version
)}
</div>
`
: ""}
<slot></slot>
</div>
<slot name="actions"></slot>

View File

@@ -17,6 +17,7 @@ import {
} from "../../../components/data-table/ha-data-table";
import "../../../components/entity/ha-battery-icon";
import "../../../components/ha-button-menu";
import "../../../components/ha-fab";
import "../../../components/ha-icon-button";
import { AreaRegistryEntry } from "../../../data/area_registry";
import { ConfigEntry } from "../../../data/config_entries";
@@ -35,6 +36,7 @@ import "../../../layouts/hass-tabs-subpage-data-table";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { configSections } from "../ha-panel-config";
import { showZWaveJSAddNodeDialog } from "../integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node";
interface DeviceRowData extends DeviceRegistryEntry {
device?: DeviceRowData;
@@ -170,7 +172,7 @@ export class HaConfigDeviceDashboard extends LitElement {
areaLookup[area.area_id] = area;
}
const filterDomains: string[] = [];
let filterConfigEntry: ConfigEntry | undefined;
filters.forEach((value, key) => {
if (key === "config_entry") {
@@ -178,10 +180,7 @@ export class HaConfigDeviceDashboard extends LitElement {
device.config_entries.includes(value)
);
startLength = outputDevices.length;
const configEntry = entries.find((entry) => entry.entry_id === value);
if (configEntry) {
filterDomains.push(configEntry.domain);
}
filterConfigEntry = entries.find((entry) => entry.entry_id === value);
}
});
@@ -220,7 +219,10 @@ export class HaConfigDeviceDashboard extends LitElement {
}));
this._numHiddenDevices = startLength - outputDevices.length;
return { devicesOutput: outputDevices, filteredDomains: filterDomains };
return {
devicesOutput: outputDevices,
filteredConfigEntry: filterConfigEntry,
};
}
);
@@ -352,16 +354,16 @@ export class HaConfigDeviceDashboard extends LitElement {
}
protected render(): TemplateResult {
const { devicesOutput, filteredDomains } = this._devicesAndFilterDomains(
this.devices,
this.entries,
this.entities,
this.areas,
this._searchParms,
this._showDisabled,
this.hass.localize
);
const includeZHAFab = filteredDomains.includes("zha");
const { devicesOutput, filteredConfigEntry } =
this._devicesAndFilterDomains(
this.devices,
this.entries,
this.entities,
this.areas,
this._searchParms,
this._showDisabled,
this.hass.localize
);
const activeFilters = this._activeFilters(
this.entries,
this._searchParms,
@@ -394,9 +396,25 @@ export class HaConfigDeviceDashboard extends LitElement {
@search-changed=${this._handleSearchChange}
@row-click=${this._handleRowClicked}
clickable
.hasFab=${includeZHAFab}
.hasFab=${filteredConfigEntry &&
(filteredConfigEntry.domain === "zha" ||
filteredConfigEntry.domain === "zwave_js")}
>
${includeZHAFab
${!filteredConfigEntry
? ""
: filteredConfigEntry.domain === "zwave_js"
? html`
<ha-fab
slot="fab"
.label=${this.hass.localize("ui.panel.config.zha.add_device")}
extended
?rtl=${computeRTL(this.hass)}
@click=${this._showZJSAddDeviceDialog}
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
`
: filteredConfigEntry.domain === "zha"
? html`<a href="/config/zha/add" slot="fab">
<ha-fab
.label=${this.hass.localize("ui.panel.config.zha.add_device")}
@@ -481,6 +499,22 @@ export class HaConfigDeviceDashboard extends LitElement {
this._showDisabled = true;
}
private _showZJSAddDeviceDialog() {
const { filteredConfigEntry } = this._devicesAndFilterDomains(
this.devices,
this.entries,
this.entities,
this.areas,
this._searchParms,
this._showDisabled,
this.hass.localize
);
showZWaveJSAddNodeDialog(this, {
entry_id: filteredConfigEntry!.entry_id,
});
}
static get styles(): CSSResultGroup {
return [
css`

View File

@@ -1,5 +1,6 @@
import "@material/mwc-button/mwc-button";
import "@polymer/paper-input/paper-input";
import type { PaperItemElement } from "@polymer/paper-item/paper-item";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
@@ -16,6 +17,7 @@ import { domainIcon } from "../../../common/entity/domain_icon";
import "../../../components/ha-area-picker";
import "../../../components/ha-expansion-panel";
import "../../../components/ha-icon-picker";
import "../../../components/ha-paper-dropdown-menu";
import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch";
import {
@@ -39,6 +41,11 @@ import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail";
const OVERRIDE_DEVICE_CLASSES = {
cover: ["window", "door", "garage"],
binary_sensor: ["window", "door", "garage_door", "opening"],
};
@customElement("entity-registry-settings")
export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -51,6 +58,8 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
@state() private _entityId!: string;
@state() private _deviceClass?: string;
@state() private _areaId?: string | null;
@state() private _disabledBy!: string | null;
@@ -85,6 +94,8 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
this._error = undefined;
this._name = this.entry.name || "";
this._icon = this.entry.icon || "";
this._deviceClass =
this.entry.device_class || this.entry.original_device_class;
this._origEntityId = this.entry.entity_id;
this._areaId = this.entry.area_id;
this._entityId = this.entry.entity_id;
@@ -102,9 +113,11 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
}
const stateObj: HassEntity | undefined =
this.hass.states[this.entry.entity_id];
const invalidDomainUpdate =
computeDomain(this._entityId.trim()) !==
computeDomain(this.entry.entity_id);
const domain = computeDomain(this.entry.entity_id);
const invalidDomainUpdate = computeDomain(this._entityId.trim()) !== domain;
return html`
${!stateObj
? html`
@@ -143,6 +156,31 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
: undefined}
.disabled=${this._submitting}
></ha-icon-picker>
${OVERRIDE_DEVICE_CLASSES[domain]?.includes(this._deviceClass) ||
(domain === "cover" && this.entry.original_device_class === null)
? html`<ha-paper-dropdown-menu
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.device_class"
)}
>
<paper-listbox
slot="dropdown-content"
attr-for-selected="item-value"
.selected=${this._deviceClass}
@selected-item-changed=${this._deviceClassChanged}
>
${OVERRIDE_DEVICE_CLASSES[domain].map(
(deviceClass: string) => html`
<paper-item .itemValue=${deviceClass}>
${this.hass.localize(
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}`
)}
</paper-item>
`
)}
</paper-listbox>
</ha-paper-dropdown-menu>`
: ""}
<paper-input
.value=${this._entityId}
@value-changed=${this._entityIdChanged}
@@ -264,6 +302,14 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
this._entityId = ev.detail.value;
}
private _deviceClassChanged(ev: PolymerChangedEvent<PaperItemElement>): void {
this._error = undefined;
if (ev.detail.value === null) {
return;
}
this._deviceClass = (ev.detail.value as any).itemValue;
}
private _areaPicked(ev: CustomEvent) {
this._error = undefined;
this._areaId = ev.detail.value;
@@ -289,6 +335,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
name: this._name.trim() || null,
icon: this._icon.trim() || null,
area_id: this._areaId || null,
device_class: this._deviceClass || null,
new_entity_id: this._entityId.trim(),
};
if (
@@ -378,6 +425,9 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
padding-bottom: max(env(safe-area-inset-bottom), 8px);
background-color: var(--mdc-theme-surface, #fff);
}
ha-paper-dropdown-menu {
width: 100%;
}
ha-switch {
margin-right: 16px;
}

View File

@@ -1,6 +1,7 @@
import {
mdiAccount,
mdiBadgeAccountHorizontal,
mdiCellphoneCog,
mdiCog,
mdiDevices,
mdiHomeAssistant,
@@ -48,73 +49,69 @@ export const configSections: { [name: string]: PageNavigation[] } = {
dashboard: [
{
path: "/config/integrations",
name: "Devices & Services",
description: "Integrations, devices, entities and areas",
translationKey: "devices",
iconPath: mdiDevices,
iconColor: "#0D47A1",
core: true,
},
{
path: "/config/automation",
name: "Automations & Scenes",
description: "Automations, blueprints, scenes and scripts",
translationKey: "automations",
iconPath: mdiRobot,
iconColor: "#518C43",
components: ["automation", "blueprint", "scene", "script"],
},
{
path: "/config/helpers",
name: "Automation Helpers",
description: "Elements that help build automations",
iconPath: mdiTools,
iconColor: "#4D2EA4",
core: true,
},
{
path: "/config/blueprint",
translationKey: "blueprints",
iconPath: mdiPaletteSwatch,
iconColor: "#64B5F6",
component: "blueprint",
},
{
path: "/hassio",
name: "Add-ons & Backups",
description: "Create backups, check logs or reboot your system",
translationKey: "supervisor",
iconPath: mdiHomeAssistant,
iconColor: "#4084CD",
component: "hassio",
},
{
path: "/config/lovelace/dashboards",
name: "Dashboards",
description: "Create customized sets of cards to control your home",
translationKey: "dashboards",
iconPath: mdiViewDashboard,
iconColor: "#B1345C",
component: "lovelace",
},
{
path: "/config/energy",
name: "Energy",
description: "Monitor your energy production and consumption",
translationKey: "energy",
iconPath: mdiLightningBolt,
iconColor: "#F1C447",
component: "energy",
},
{
path: "/config/tags",
name: "Tags",
description:
"Trigger automations when a NFC tag, QR code, etc. is scanned",
translationKey: "tags",
iconPath: mdiNfcVariant,
iconColor: "#616161",
component: "tag",
},
{
path: "/config/person",
name: "People & Zones",
description: "Manage the people and zones that Home Assistant tracks",
translationKey: "people",
iconPath: mdiAccount,
iconColor: "#E48629",
components: ["person", "zone", "users"],
},
{
path: "/config/core",
name: "Settings",
description: "Basic settings, server controls, logs and info",
path: "#external-app-configuration",
translationKey: "companion",
iconPath: mdiCellphoneCog,
iconColor: "#8E24AA",
},
{
path: "/config/server_control",
translationKey: "settings",
iconPath: mdiCog,
iconColor: "#4A5963",
core: true,
@@ -155,13 +152,6 @@ export const configSections: { [name: string]: PageNavigation[] } = {
},
],
automations: [
{
component: "blueprint",
path: "/config/blueprint",
translationKey: "ui.panel.config.blueprint.caption",
iconPath: mdiPaletteSwatch,
iconColor: "#518C43",
},
{
component: "automation",
path: "/config/automation",
@@ -183,8 +173,6 @@ export const configSections: { [name: string]: PageNavigation[] } = {
iconPath: mdiScriptText,
iconColor: "#518C43",
},
],
helpers: [
{
component: "helpers",
path: "/config/helpers",
@@ -194,6 +182,15 @@ export const configSections: { [name: string]: PageNavigation[] } = {
core: true,
},
],
blueprints: [
{
component: "blueprint",
path: "/config/blueprint",
translationKey: "ui.panel.config.blueprint.caption",
iconPath: mdiPaletteSwatch,
iconColor: "#518C43",
},
],
tags: [
{
component: "tag",
@@ -447,9 +444,19 @@ class HaPanelConfig extends HassRouterPage {
this.hass.loadBackendTranslation("title");
if (isComponentLoaded(this.hass, "cloud")) {
this._updateCloudStatus();
this.addEventListener("connection-status", (ev) => {
if (ev.detail === "connected") {
this._updateCloudStatus();
}
});
}
if (isComponentLoaded(this.hass, "hassio")) {
this._loadSupervisorUpdates();
this.addEventListener("connection-status", (ev) => {
if (ev.detail === "connected") {
this._loadSupervisorUpdates();
}
});
} else {
this._supervisorUpdates = null;
}

View File

@@ -132,7 +132,7 @@ export class HaConfigHelpers extends LitElement {
.narrow=${this.narrow}
back-path="/config"
.route=${this.route}
.tabs=${configSections.helpers}
.tabs=${configSections.automations}
.columns=${this._columns(this.narrow, this.hass.language)}
.data=${this._getItems(this._stateItems)}
@row-click=${this._openEditDialog}

View File

@@ -1,6 +1,7 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property } from "lit/decorators";
import "../../../layouts/hass-tabs-subpage";
import "../../../components/ha-logo-svg";
import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
@@ -40,13 +41,14 @@ class HaConfigInfo extends LitElement {
href=${documentationUrl(this.hass, "")}
target="_blank"
rel="noreferrer"
><img
src="/static/icons/favicon-192x192.png"
height="192"
alt=${this.hass.localize(
>
<ha-logo-svg
title=${this.hass.localize(
"ui.panel.config.info.home_assistant_logo"
)}
/></a>
>
</ha-logo-svg>
</a>
<br />
<h2>Home Assistant ${hass.connection.haVersion}</h2>
<p>
@@ -193,6 +195,11 @@ class HaConfigInfo extends LitElement {
margin: 0 auto;
padding-bottom: 16px;
}
ha-logo-svg {
padding: 12px;
height: 180px;
width: 180px;
}
`,
];
}

View File

@@ -95,7 +95,7 @@ class OZWConfigDashboard extends LitElement {
<ha-card>
<a
href="/config/ozw/network/${instance.ozw_instance}"
aria-role="option"
role="option"
tabindex="-1"
>
<paper-icon-item>

View File

@@ -129,7 +129,11 @@ class HaConfigZwave extends LocalizeMixin(EventsMixin(PolymerElement)) {
<span
>[[localize('ui.panel.config.zwave.node_management.header')]]</span
>
<ha-icon-button class="toggle-help-icon" on-click="toggleHelp">
<ha-icon-button
class="toggle-help-icon"
on-click="toggleHelp"
label="[[localize('ui.common.help')]]"
>
<ha-icon icon="hass:help-circle"></ha-icon>
</ha-icon-button>
</div>

View File

@@ -1,5 +1,4 @@
import "@material/mwc-button/mwc-button";
import type { TextField } from "@material/mwc-textfield/mwc-textfield";
import "@material/mwc-textfield/mwc-textfield";
import { mdiAlertCircle, mdiCheckCircle, mdiQrcodeScan } from "@mdi/js";
import "@polymer/paper-input/paper-input";
@@ -45,6 +44,8 @@ export interface ZWaveJSAddNodeDevice {
class DialogZWaveJSAddNode extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: ZWaveJSAddNodeDialogParams;
@state() private _entryId?: string;
@state() private _status?:
@@ -91,6 +92,7 @@ class DialogZWaveJSAddNode extends LitElement {
}
public async showDialog(params: ZWaveJSAddNodeDialogParams): Promise<void> {
this._params = params;
this._entryId = params.entry_id;
this._status = "loading";
this._checkSmartStartSupport();
@@ -176,21 +178,16 @@ class DialogZWaveJSAddNode extends LitElement {
Search device
</mwc-button>`
: this._status === "qr_scan"
? html`<ha-qr-scanner
? html`${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<ha-qr-scanner
.localize=${this.hass.localize}
@qr-code-scanned=${this._qrCodeScanned}
></ha-qr-scanner>
<p>
If scanning doesn't work, you can enter the QR code value
manually:
</p>
<mwc-textfield
.label=${this.hass.localize(
"ui.panel.config.zwave_js.add_node.enter_qr_code"
)}
.disabled=${this._qrProcessing}
@keydown=${this._qrKeyDown}
></mwc-textfield>`
<mwc-button slot="secondaryAction" @click=${this._startOver}>
${this.hass.localize("ui.panel.config.zwave_js.common.back")}
</mwc-button>`
: this._status === "validate_dsk_enter_pin"
? html`
<p>
@@ -200,9 +197,9 @@ class DialogZWaveJSAddNode extends LitElement {
</p>
${
this._error
? html`<ha-alert alert-type="error"
>${this._error}</ha-alert
>`
? html`<ha-alert alert-type="error">
${this._error}
</ha-alert>`
: ""
}
<div class="flex-container">
@@ -271,7 +268,7 @@ class DialogZWaveJSAddNode extends LitElement {
We have not found any device in inclusion mode. Make sure the
device is active and in inclusion mode.
</p>
<mwc-button slot="primaryAction" @click=${this._startInclusion}>
<mwc-button slot="primaryAction" @click=${this._startOver}>
Retry
</mwc-button>
`
@@ -370,7 +367,7 @@ class DialogZWaveJSAddNode extends LitElement {
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
${this.hass.localize("ui.common.close")}
</mwc-button>
`
: this._status === "failed"
@@ -507,15 +504,6 @@ class DialogZWaveJSAddNode extends LitElement {
this._status = "qr_scan";
}
private _qrKeyDown(ev: KeyboardEvent) {
if (this._qrProcessing) {
return;
}
if (ev.key === "Enter") {
this._handleQrCodeScanned((ev.target as TextField).value);
}
}
private _qrCodeScanned(ev: CustomEvent): void {
if (this._qrProcessing) {
return;
@@ -562,17 +550,16 @@ class DialogZWaveJSAddNode extends LitElement {
provisioningInfo
);
this._status = "provisioned";
if (this._params?.addedCallback) {
this._params.addedCallback();
}
} catch (err: any) {
this._error = err.message;
this._status = "failed";
}
} else if (provisioningInfo.version === 0) {
this._inclusionStrategy = InclusionStrategy.Security_S2;
// this._startInclusion(provisioningInfo);
this._startInclusion(undefined, undefined, {
dsk: "34673-15546-46480-39591-32400-22155-07715-45994",
security_classes: [0, 1, 7],
});
this._startInclusion(provisioningInfo);
} else {
this._error = "This QR code is not supported";
this._status = "failed";
@@ -632,6 +619,10 @@ class DialogZWaveJSAddNode extends LitElement {
).supported;
}
private _startOver(_ev: Event) {
this._startInclusion();
}
private _startInclusion(
qrProvisioningInformation?: QRProvisioningInformation,
qrCodeString?: string,
@@ -693,6 +684,9 @@ class DialogZWaveJSAddNode extends LitElement {
if (message.event === "interview completed") {
this._unsubscribe();
this._status = "finished";
if (this._params?.addedCallback) {
this._params.addedCallback();
}
}
if (message.event === "interview stage completed") {

View File

@@ -2,6 +2,7 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
export interface ZWaveJSAddNodeDialogParams {
entry_id: string;
addedCallback?: () => void;
}
export const loadAddNodeDialog = () => import("./dialog-zwave_js-add-node");

View File

@@ -1,10 +1,17 @@
import "@material/mwc-button/mwc-button";
import { mdiAlertCircle, mdiCheckCircle, mdiCircle, mdiRefresh } from "@mdi/js";
import {
mdiAlertCircle,
mdiCheckCircle,
mdiCircle,
mdiPlus,
mdiRefresh,
} from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import "../../../../../components/ha-card";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-fab";
import "../../../../../components/ha-icon-next";
import "../../../../../components/ha-svg-icon";
import { getSignedPath } from "../../../../../data/auth";
@@ -12,10 +19,12 @@ import {
fetchZwaveDataCollectionStatus,
fetchZwaveNetworkStatus,
fetchZwaveNodeStatus,
fetchZwaveProvisioningEntries,
NodeStatus,
setZwaveDataCollectionPreference,
ZWaveJSNetwork,
ZWaveJSNodeStatus,
ZwaveJSProvisioningEntry,
} from "../../../../../data/zwave_js";
import {
ConfigEntry,
@@ -36,6 +45,7 @@ import { showZWaveJSHealNetworkDialog } from "./show-dialog-zwave_js-heal-networ
import { showZWaveJSRemoveNodeDialog } from "./show-dialog-zwave_js-remove-node";
import { configTabs } from "./zwave_js-config-router";
import { showOptionsFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-options-flow";
import { computeRTL } from "../../../../../common/util/compute_rtl";
@customElement("zwave_js-config-dashboard")
class ZWaveJSConfigDashboard extends LitElement {
@@ -55,6 +65,8 @@ class ZWaveJSConfigDashboard extends LitElement {
@state() private _nodes?: ZWaveJSNodeStatus[];
@state() private _provisioningEntries?: ZwaveJSProvisioningEntry[];
@state() private _status = "unknown";
@state() private _icon = mdiCircle;
@@ -76,6 +88,9 @@ class ZWaveJSConfigDashboard extends LitElement {
return this._renderErrorScreen();
}
const notReadyDevices =
this._nodes?.filter((node) => !node.ready).length ?? 0;
return html`
<hass-tabs-subpage
.hass=${this.hass}
@@ -128,32 +143,25 @@ class ZWaveJSConfigDashboard extends LitElement {
${this.hass.localize(
`ui.panel.config.zwave_js.network_status.${this._status}`
)}<br />
<small
>${this._network.client.ws_server_url}</small
>
<small>
${this.hass.localize(
`ui.panel.config.zwave_js.dashboard.devices`,
{
count:
this._network.controller.nodes.length,
}
)}
${notReadyDevices > 0
? html`(${this.hass.localize(
`ui.panel.config.zwave_js.dashboard.not_ready`,
{ count: notReadyDevices }
)})`
: ""}
</small>
</div>
`
: ``}
</div>
<div class="secondary">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.driver_version"
)}:
${this._network.client.driver_version}<br />
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.server_version"
)}:
${this._network.client.server_version}<br />
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.home_id"
)}:
${this._network.controller.home_id}<br />
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.nodes_ready"
)}:
${this._nodes?.filter((node) => node.ready).length ?? 0} /
${this._network.controller.nodes.length}
</div>
</div>
<div class="card-actions">
<a
@@ -172,22 +180,66 @@ class ZWaveJSConfigDashboard extends LitElement {
)}
</mwc-button>
</a>
<mwc-button @click=${this._addNodeClicked}>
${this._provisioningEntries?.length
? html`<a
href=${`provisioned?config_entry=${this.configEntryId}`}
><mwc-button>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.provisioned_devices"
)}
</mwc-button></a
>`
: ""}
</div>
</ha-card>
<ha-card header="Diagnostics">
<div class="card-content">
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.driver_version"
)}:
${this._network.client.driver_version}<br />
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.server_version"
)}:
${this._network.client.server_version}<br />
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.home_id"
)}:
${this._network.controller.home_id}<br />
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.server_url"
)}:
${this._network.client.ws_server_url}<br />
</div>
<div class="card-actions">
<mwc-button
@click=${this._dumpDebugClicked}
.disabled=${this._status === "connecting"}
>
${this.hass.localize(
"ui.panel.config.zwave_js.common.add_node"
"ui.panel.config.zwave_js.dashboard.dump_debug"
)}
</mwc-button>
<mwc-button @click=${this._removeNodeClicked}>
<mwc-button
@click=${this._removeNodeClicked}
.disabled=${this._status === "connecting"}
>
${this.hass.localize(
"ui.panel.config.zwave_js.common.remove_node"
)}
</mwc-button>
<mwc-button @click=${this._healNetworkClicked}>
<mwc-button
@click=${this._healNetworkClicked}
.disabled=${this._status === "connecting"}
>
${this.hass.localize(
"ui.panel.config.zwave_js.common.heal_network"
)}
</mwc-button>
<mwc-button @click=${this._openOptionFlow}>
<mwc-button
@click=${this._openOptionFlow}
.disabled=${this._status === "connecting"}
>
${this.hass.localize(
"ui.panel.config.zwave_js.common.reconfigure_server"
)}
@@ -229,12 +281,19 @@ class ZWaveJSConfigDashboard extends LitElement {
</ha-card>
`
: ``}
<button class="link dump" @click=${this._dumpDebugClicked}>
${this.hass.localize(
"ui.panel.config.zwave_js.dashboard.dump_debug"
)}
</button>
</ha-config-section>
<ha-fab
slot="fab"
.label=${this.hass.localize(
"ui.panel.config.zwave_js.common.add_node"
)}
.disabled=${this._status === "connecting"}
extended
?rtl=${computeRTL(this.hass)}
@click=${this._addNodeClicked}
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
</hass-tabs-subpage>
`;
}
@@ -316,10 +375,14 @@ class ZWaveJSConfigDashboard extends LitElement {
return;
}
const [network, dataCollectionStatus] = await Promise.all([
fetchZwaveNetworkStatus(this.hass!, this.configEntryId),
fetchZwaveDataCollectionStatus(this.hass!, this.configEntryId),
]);
const [network, dataCollectionStatus, provisioningEntries] =
await Promise.all([
fetchZwaveNetworkStatus(this.hass!, this.configEntryId),
fetchZwaveDataCollectionStatus(this.hass!, this.configEntryId),
fetchZwaveProvisioningEntries(this.hass!, this.configEntryId),
]);
this._provisioningEntries = provisioningEntries;
this._network = network;
@@ -348,6 +411,7 @@ class ZWaveJSConfigDashboard extends LitElement {
private async _addNodeClicked() {
showZWaveJSAddNodeDialog(this, {
entry_id: this.configEntryId!,
addedCallback: () => this._fetchData(),
});
}
@@ -486,7 +550,6 @@ class ZWaveJSConfigDashboard extends LitElement {
.network-status div.heading {
display: flex;
align-items: center;
margin-bottom: 16px;
}
.network-status div.heading .icon {

View File

@@ -49,6 +49,10 @@ class ZWaveJSConfigRouter extends HassRouterPage {
tag: "zwave_js-logs",
load: () => import("./zwave_js-logs"),
},
provisioned: {
tag: "zwave_js-provisioned",
load: () => import("./zwave_js-provisioned"),
},
},
};

View File

@@ -327,6 +327,9 @@ class ZWaveJSNodeConfig extends SubscribeMixin(LitElement) {
if (!("states" in item.metadata)) {
return false;
}
if (Object.keys(item.metadata.states).length !== 2) {
return false;
}
if (!(0 in item.metadata.states) || !(1 in item.metadata.states)) {
return false;
}

View File

@@ -0,0 +1,153 @@
import { mdiCheckCircle, mdiCloseCircleOutline, mdiDelete } from "@mdi/js";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { DataTableColumnContainer } from "../../../../../components/data-table/ha-data-table";
import {
ZwaveJSProvisioningEntry,
fetchZwaveProvisioningEntries,
SecurityClass,
unprovisionZwaveSmartStartNode,
} from "../../../../../data/zwave_js";
import { showConfirmationDialog } from "../../../../../dialogs/generic/show-dialog-box";
import "../../../../../layouts/hass-tabs-subpage-data-table";
import { HomeAssistant, Route } from "../../../../../types";
import { configTabs } from "./zwave_js-config-router";
@customElement("zwave_js-provisioned")
class ZWaveJSProvisioned extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Object }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean;
@property() public configEntryId!: string;
@state() private _provisioningEntries: ZwaveJSProvisioningEntry[] = [];
protected render() {
return html`
<hass-tabs-subpage-data-table
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${configTabs}
.columns=${this._columns(this.narrow)}
.data=${this._provisioningEntries}
>
</hass-tabs-subpage-data-table>
`;
}
private _columns = memoizeOne(
(narrow: boolean): DataTableColumnContainer => ({
included: {
title: this.hass.localize(
"ui.panel.config.zwave_js.provisioned.included"
),
type: "icon",
width: "100px",
template: (_info, provisioningEntry: any) =>
provisioningEntry.additional_properties.nodeId
? html`
<ha-svg-icon
.label=${this.hass.localize(
"ui.panel.config.zwave_js.provisioned.included"
)}
.path=${mdiCheckCircle}
></ha-svg-icon>
`
: html`
<ha-svg-icon
.label=${this.hass.localize(
"ui.panel.config.zwave_js.provisioned.not_included"
)}
.path=${mdiCloseCircleOutline}
></ha-svg-icon>
`,
},
dsk: {
title: this.hass.localize("ui.panel.config.zwave_js.provisioned.dsk"),
sortable: true,
filterable: true,
grows: true,
},
security_classes: {
title: this.hass.localize(
"ui.panel.config.zwave_js.provisioned.security_classes"
),
width: "30%",
hidden: narrow,
filterable: true,
sortable: true,
template: (securityClasses: SecurityClass[]) =>
securityClasses
.map((secClass) =>
this.hass.localize(
`ui.panel.config.zwave_js.security_classes.${SecurityClass[secClass]}.title`
)
)
.join(", "),
},
unprovision: {
title: this.hass.localize(
"ui.panel.config.zwave_js.provisioned.unprovison"
),
type: "icon-button",
width: "100px",
template: (_info, provisioningEntry: any) => html`
<ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.zwave_js.provisioned.unprovison"
)}
.path=${mdiDelete}
.provisioningEntry=${provisioningEntry}
@click=${this._unprovision}
></ha-icon-button>
`,
},
})
);
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._fetchData();
}
private async _fetchData() {
this._provisioningEntries = await fetchZwaveProvisioningEntries(
this.hass!,
this.configEntryId
);
}
private _unprovision = async (ev) => {
const dsk = ev.currentTarget.provisioningEntry.dsk;
const confirm = await showConfirmationDialog(this, {
title: this.hass.localize(
"ui.panel.config.zwave_js.provisioned.confirm_unprovision_title"
),
text: this.hass.localize(
"ui.panel.config.zwave_js.provisioned.confirm_unprovision_text"
),
confirmText: this.hass.localize(
"ui.panel.config.zwave_js.provisioned.unprovison"
),
});
if (!confirm) {
return;
}
await unprovisionZwaveSmartStartNode(this.hass, this.configEntryId, dsk);
this._fetchData();
};
}
declare global {
interface HTMLElementTagNameMap {
"zwave_js-provisioned": ZWaveJSProvisioned;
}
}

View File

@@ -51,6 +51,8 @@ class DialogPersonDetail extends LitElement {
@state() private _isAdmin?: boolean;
@state() private _localOnly?: boolean;
@state() private _deviceTrackers!: string[];
@state() private _picture!: string | null;
@@ -83,12 +85,14 @@ class DialogPersonDetail extends LitElement {
? this._params.users.find((user) => user.id === this._userId)
: undefined;
this._isAdmin = this._user?.group_ids.includes(SYSTEM_GROUP_ID_ADMIN);
this._localOnly = this._user?.local_only;
} else {
this._personExists = false;
this._name = "";
this._userId = undefined;
this._user = undefined;
this._isAdmin = undefined;
this._localOnly = undefined;
this._deviceTrackers = [];
this._picture = null;
}
@@ -152,19 +156,31 @@ class DialogPersonDetail extends LitElement {
${this._user
? html`<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.person.detail.admin"
)}
.dir=${computeRTLDirection(this.hass)}
>
<ha-switch
.disabled=${this._user.system_generated ||
this._user.is_owner}
.checked=${this._isAdmin}
@change=${this._adminChanged}
.label=${this.hass.localize(
"ui.panel.config.person.detail.local_only"
)}
.dir=${computeRTLDirection(this.hass)}
>
</ha-switch>
</ha-formfield>`
<ha-switch
.checked=${this._localOnly}
@change=${this._localOnlyChanged}
>
</ha-switch>
</ha-formfield>
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.person.detail.admin"
)}
.dir=${computeRTLDirection(this.hass)}
>
<ha-switch
.disabled=${this._user.system_generated ||
this._user.is_owner}
.checked=${this._isAdmin}
@change=${this._adminChanged}
>
</ha-switch>
</ha-formfield>`
: ""}
${this._deviceTrackersAvailable(this.hass)
? html`
@@ -266,10 +282,14 @@ class DialogPersonDetail extends LitElement {
this._name = ev.detail.value;
}
private async _adminChanged(ev): Promise<void> {
private _adminChanged(ev): void {
this._isAdmin = ev.target.checked;
}
private _localOnlyChanged(ev): void {
this._localOnly = ev.target.checked;
}
private async _allowLoginChanged(ev): Promise<void> {
const target = ev.target;
if (target.checked) {
@@ -281,6 +301,7 @@ class DialogPersonDetail extends LitElement {
this._user = user;
this._userId = user.id;
this._isAdmin = user.group_ids.includes(SYSTEM_GROUP_ID_ADMIN);
this._localOnly = user.local_only;
this._params?.refreshUsers();
}
},
@@ -373,13 +394,16 @@ class DialogPersonDetail extends LitElement {
try {
if (
(this._userId && this._name !== this._params!.entry?.name) ||
this._isAdmin !== this._user?.group_ids.includes(SYSTEM_GROUP_ID_ADMIN)
this._isAdmin !==
this._user?.group_ids.includes(SYSTEM_GROUP_ID_ADMIN) ||
this._localOnly !== this._user?.local_only
) {
await updateUser(this.hass!, this._userId!, {
name: this._name.trim(),
group_ids: [
this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER,
],
local_only: this._localOnly,
});
this._params?.refreshUsers();
}

View File

@@ -5,6 +5,7 @@ import "@polymer/paper-input/paper-input";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { componentsWithService } from "../../../common/config/components_with_service";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/buttons/ha-call-service-button";
import "../../../components/ha-card";
import { checkCoreConfig } from "../../../data/core";
@@ -157,18 +158,20 @@ export class HaConfigServerControl extends LitElement {
"ui.panel.config.server_control.section.server_management.restart"
)}
</ha-call-service-button>
<ha-call-service-button
class="warning"
.hass=${this.hass}
domain="homeassistant"
service="stop"
confirmation=${this.hass.localize(
"ui.panel.config.server_control.section.server_management.confirm_stop"
)}
>${this.hass.localize(
"ui.panel.config.server_control.section.server_management.stop"
)}
</ha-call-service-button>
${!isComponentLoaded(this.hass, "hassio")
? html`<ha-call-service-button
class="warning"
.hass=${this.hass}
domain="homeassistant"
service="stop"
confirmation=${this.hass.localize(
"ui.panel.config.server_control.section.server_management.confirm_stop"
)}
>${this.hass.localize(
"ui.panel.config.server_control.section.server_management.stop"
)}
</ha-call-service-button>`
: ""}
</div>
</ha-card>

View File

@@ -48,6 +48,8 @@ export class DialogAddUser extends LitElement {
@state() private _isAdmin?: boolean;
@state() private _localOnly?: boolean;
@state() private _allowChangeName = true;
public showDialog(params: AddUserDialogParams) {
@@ -57,6 +59,7 @@ export class DialogAddUser extends LitElement {
this._password = "";
this._passwordConfirm = "";
this._isAdmin = false;
this._localOnly = false;
this._error = undefined;
this._loading = false;
@@ -153,14 +156,32 @@ export class DialogAddUser extends LitElement {
"ui.panel.config.users.add_user.password_not_match"
)}
></paper-input>
<ha-formfield
.label=${this.hass.localize("ui.panel.config.users.editor.admin")}
.dir=${computeRTLDirection(this.hass)}
>
<ha-switch .checked=${this._isAdmin} @change=${this._adminChanged}>
</ha-switch>
</ha-formfield>
<div class="row">
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.users.editor.local_only"
)}
.dir=${computeRTLDirection(this.hass)}
>
<ha-switch
.checked=${this._localOnly}
@change=${this._localOnlyChanged}
>
</ha-switch>
</ha-formfield>
</div>
<div class="row">
<ha-formfield
.label=${this.hass.localize("ui.panel.config.users.editor.admin")}
.dir=${computeRTLDirection(this.hass)}
>
<ha-switch
.checked=${this._isAdmin}
@change=${this._adminChanged}
>
</ha-switch>
</ha-formfield>
</div>
${!this._isAdmin
? html`
<br />
@@ -218,6 +239,10 @@ export class DialogAddUser extends LitElement {
this._isAdmin = ev.target.checked;
}
private _localOnlyChanged(ev): void {
this._localOnly = ev.target.checked;
}
private async _createUser(ev) {
ev.preventDefault();
if (!this._name || !this._username || !this._password) {
@@ -229,9 +254,12 @@ export class DialogAddUser extends LitElement {
let user: User;
try {
const userResponse = await createUser(this.hass, this._name, [
this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER,
]);
const userResponse = await createUser(
this.hass,
this._name,
[this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER],
this._localOnly
);
user = userResponse.user;
} catch (err: any) {
this._loading = false;
@@ -266,8 +294,9 @@ export class DialogAddUser extends LitElement {
--mdc-dialog-max-width: 500px;
--dialog-z-index: 10;
}
ha-switch {
margin-top: 8px;
.row {
display: flex;
padding: 8px 0;
}
`,
];

View File

@@ -30,6 +30,8 @@ class DialogUserDetail extends LitElement {
@state() private _isAdmin?: boolean;
@state() private _localOnly?: boolean;
@state() private _isActive?: boolean;
@state() private _error?: string;
@@ -43,6 +45,7 @@ class DialogUserDetail extends LitElement {
this._error = undefined;
this._name = params.entry.name || "";
this._isAdmin = params.entry.group_ids.includes(SYSTEM_GROUP_ID_ADMIN);
this._localOnly = params.entry.local_only;
this._isActive = params.entry.is_active;
await this.updateComplete;
}
@@ -95,6 +98,21 @@ class DialogUserDetail extends LitElement {
@value-changed=${this._nameChanged}
label=${this.hass!.localize("ui.panel.config.users.editor.name")}
></paper-input>
<div class="row">
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.users.editor.local_only"
)}
.dir=${computeRTLDirection(this.hass)}
>
<ha-switch
.disabled=${user.system_generated}
.checked=${this._localOnly}
@change=${this._localOnlyChanged}
>
</ha-switch>
</ha-formfield>
</div>
<div class="row">
<ha-formfield
.label=${this.hass.localize(
@@ -198,11 +216,15 @@ class DialogUserDetail extends LitElement {
this._name = ev.detail.value;
}
private async _adminChanged(ev): Promise<void> {
private _adminChanged(ev): void {
this._isAdmin = ev.target.checked;
}
private async _activeChanged(ev): Promise<void> {
private _localOnlyChanged(ev): void {
this._localOnly = ev.target.checked;
}
private _activeChanged(ev): void {
this._isActive = ev.target.checked;
}
@@ -215,6 +237,7 @@ class DialogUserDetail extends LitElement {
group_ids: [
this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER,
],
local_only: this._localOnly,
});
this._close();
} catch (err: any) {

View File

@@ -90,7 +90,7 @@ export class HaConfigUsers extends LitElement {
width: "80px",
template: (is_active) =>
is_active
? html`<ha-svg-icon .path=${mdiCheck}> </ha-svg-icon>`
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
: "",
},
system_generated: {
@@ -103,9 +103,20 @@ export class HaConfigUsers extends LitElement {
width: "160px",
template: (generated) =>
generated
? html`<ha-svg-icon .path=${mdiCheck}> </ha-svg-icon>`
? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>`
: "",
},
local_only: {
title: this.hass.localize(
"ui.panel.config.users.picker.headers.local"
),
type: "icon",
sortable: true,
filterable: true,
width: "160px",
template: (local) =>
local ? html`<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>` : "",
},
};
return columns;

View File

@@ -18,6 +18,7 @@ import "../../../components/ha-code-editor";
import "../../../components/ha-icon-button";
import "../../../components/ha-svg-icon";
import "../../../components/ha-checkbox";
import "../../../components/ha-expansion-panel";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import { EventsMixin } from "../../../mixins/events-mixin";
import LocalizeMixin from "../../../mixins/localize-mixin";
@@ -40,6 +41,10 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
padding: 16px;
}
ha-expansion-panel {
margin: 0 8px 16px;
}
.inputs {
width: 100%;
max-width: 400px;
@@ -135,72 +140,77 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
padding: 0;
}
</style>
<p>
[[localize('ui.panel.developer-tools.tabs.states.description1')]]<br />
[[localize('ui.panel.developer-tools.tabs.states.description2')]]
</p>
<div class="state-wrapper flex layout horizontal">
<div class="inputs">
<ha-entity-picker
autofocus
hass="[[hass]]"
value="{{_entityId}}"
on-change="entityIdChanged"
allow-custom-entity
></ha-entity-picker>
<paper-input
label="[[localize('ui.panel.developer-tools.tabs.states.state')]]"
required
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
value="{{_state}}"
class="state-input"
></paper-input>
<p>
[[localize('ui.panel.developer-tools.tabs.states.state_attributes')]]
</p>
<ha-code-editor
mode="yaml"
value="[[_stateAttributes]]"
error="[[!validJSON]]"
on-value-changed="_yamlChanged"
></ha-code-editor>
<div class="button-row">
<mwc-button
on-click="handleSetState"
disabled="[[!validJSON]]"
raised
>[[localize('ui.panel.developer-tools.tabs.states.set_state')]]</mwc-button
>
<ha-icon-button
on-click="entityIdChanged"
label="[[localize('ui.common.refresh')]]"
path="[[refreshIcon()]]"
></ha-icon-button>
</div>
</div>
<div class="info">
<template is="dom-if" if="[[_entity]]">
<p>
<b
>[[localize('ui.panel.developer-tools.tabs.states.last_changed')]]:</b
><br />[[lastChangedString(_entity)]]
</p>
<p>
<b
>[[localize('ui.panel.developer-tools.tabs.states.last_updated')]]:</b
><br />[[lastUpdatedString(_entity)]]
</p>
</template>
</div>
</div>
<h1>
[[localize('ui.panel.developer-tools.tabs.states.current_entities')]]
</h1>
<ha-expansion-panel
header="Set state"
outlined
expanded="[[_expanded]]"
on-expanded-changed="expandedChanged"
>
<p>
[[localize('ui.panel.developer-tools.tabs.states.description1')]]<br />
[[localize('ui.panel.developer-tools.tabs.states.description2')]]
</p>
<div class="state-wrapper flex layout horizontal">
<div class="inputs">
<ha-entity-picker
autofocus
hass="[[hass]]"
value="{{_entityId}}"
on-change="entityIdChanged"
allow-custom-entity
></ha-entity-picker>
<paper-input
label="[[localize('ui.panel.developer-tools.tabs.states.state')]]"
required
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
value="{{_state}}"
class="state-input"
></paper-input>
<p>
[[localize('ui.panel.developer-tools.tabs.states.state_attributes')]]
</p>
<ha-code-editor
mode="yaml"
value="[[_stateAttributes]]"
error="[[!validJSON]]"
on-value-changed="_yamlChanged"
></ha-code-editor>
<div class="button-row">
<mwc-button
on-click="handleSetState"
disabled="[[!validJSON]]"
raised
>[[localize('ui.panel.developer-tools.tabs.states.set_state')]]</mwc-button
>
<ha-icon-button
on-click="entityIdChanged"
label="[[localize('ui.common.refresh')]]"
path="[[refreshIcon()]]"
></ha-icon-button>
</div>
</div>
<div class="info">
<template is="dom-if" if="[[_entity]]">
<p>
<b
>[[localize('ui.panel.developer-tools.tabs.states.last_changed')]]:</b
><br />[[lastChangedString(_entity)]]
</p>
<p>
<b
>[[localize('ui.panel.developer-tools.tabs.states.last_updated')]]:</b
><br />[[lastUpdatedString(_entity)]]
</p>
</template>
</div>
</div>
</ha-expansion-panel>
<div class="table-wrapper">
<table class="entities">
<tr>
@@ -348,6 +358,11 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
"computeEntities(hass, _entityFilter, _stateFilter, _attributeFilter)",
},
_expanded: {
type: Boolean,
value: false,
},
narrow: {
type: Boolean,
reflectToAttribute: true,
@@ -371,6 +386,7 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
this._entity = state;
this._state = state.state;
this._stateAttributes = dump(state.attributes);
this._expanded = true;
ev.preventDefault();
}
@@ -388,6 +404,11 @@ class HaPanelDevState extends EventsMixin(LocalizeMixin(PolymerElement)) {
this._entity = state;
this._state = state.state;
this._stateAttributes = dump(state.attributes);
this._expanded = true;
}
expandedChanged(ev) {
this._expanded = ev.detail.expanded;
}
entityMoreInfo(ev) {

View File

@@ -26,7 +26,7 @@ import {
rgb2hex,
rgb2lab,
} from "../../../../common/color/convert-color";
import { labDarken } from "../../../../common/color/lab";
import { labBrighten, labDarken } from "../../../../common/color/lab";
import {
EnergyData,
getEnergyDataCollection,
@@ -247,10 +247,15 @@ export class HuiEnergyGasGraphCard
const data: ChartDataset<"bar" | "line">[] = [];
const entity = this.hass.states[source.stat_energy_from];
const borderColor =
const modifiedColor =
idx > 0
? rgb2hex(lab2rgb(labDarken(rgb2lab(hex2rgb(gasColor)), idx)))
: gasColor;
? this.hass.themes.darkMode
? labBrighten(rgb2lab(hex2rgb(gasColor)), idx)
: labDarken(rgb2lab(hex2rgb(gasColor)), idx)
: undefined;
const borderColor = modifiedColor
? rgb2hex(lab2rgb(modifiedColor))
: gasColor;
let prevValue: number | null = null;
let prevStart: string | null = null;

View File

@@ -1,9 +1,3 @@
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
import { classMap } from "lit/directives/class-map";
import "../../../../components/ha-card";
import {
ChartData,
ChartDataset,
@@ -17,16 +11,26 @@ import {
isToday,
startOfToday,
} from "date-fns";
import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types";
import { EnergySolarGraphCardConfig } from "../types";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import {
hex2rgb,
lab2rgb,
rgb2hex,
rgb2lab,
} from "../../../../common/color/convert-color";
import { labDarken } from "../../../../common/color/lab";
import { labBrighten, labDarken } from "../../../../common/color/lab";
import { formatTime } from "../../../../common/datetime/format_time";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import {
formatNumber,
numberFormatToLocale,
} from "../../../../common/number/format_number";
import "../../../../components/chart/ha-chart-base";
import "../../../../components/ha-card";
import {
EnergyData,
EnergySolarForecasts,
@@ -34,15 +38,11 @@ import {
getEnergySolarForecasts,
SolarSourceTypeEnergyPreference,
} from "../../../../data/energy";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import "../../../../components/chart/ha-chart-base";
import {
formatNumber,
numberFormatToLocale,
} from "../../../../common/number/format_number";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { FrontendLocaleData } from "../../../../data/translation";
import { formatTime } from "../../../../common/datetime/format_time";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../../../types";
import { LovelaceCard } from "../../types";
import { EnergySolarGraphCardConfig } from "../types";
@customElement("hui-energy-solar-graph-card")
export class HuiEnergySolarGraphCard
@@ -258,10 +258,15 @@ export class HuiEnergySolarGraphCard
const data: ChartDataset<"bar" | "line">[] = [];
const entity = this.hass.states[source.stat_energy_from];
const borderColor =
const modifiedColor =
idx > 0
? rgb2hex(lab2rgb(labDarken(rgb2lab(hex2rgb(solarColor)), idx)))
: solarColor;
? this.hass.themes.darkMode
? labBrighten(rgb2lab(hex2rgb(solarColor)), idx)
: labDarken(rgb2lab(hex2rgb(solarColor)), idx)
: undefined;
const borderColor = modifiedColor
? rgb2hex(lab2rgb(modifiedColor))
: solarColor;
let prevValue: number | null = null;
let prevStart: string | null = null;

View File

@@ -17,7 +17,7 @@ import {
rgb2lab,
hex2rgb,
} from "../../../../common/color/convert-color";
import { labDarken } from "../../../../common/color/lab";
import { labBrighten, labDarken } from "../../../../common/color/lab";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import { formatNumber } from "../../../../common/number/format_number";
import "../../../../components/chart/statistics-chart";
@@ -170,12 +170,17 @@ export class HuiEnergySourcesTableCard
this._data!.stats[source.stat_energy_from]
) || 0;
totalSolar += energy;
const color =
const modifiedColor =
idx > 0
? rgb2hex(
lab2rgb(labDarken(rgb2lab(hex2rgb(solarColor)), idx))
)
: solarColor;
? this.hass.themes.darkMode
? labBrighten(rgb2lab(hex2rgb(solarColor)), idx)
: labDarken(rgb2lab(hex2rgb(solarColor)), idx)
: undefined;
const color = modifiedColor
? rgb2hex(lab2rgb(modifiedColor))
: solarColor;
return html`<tr class="mdc-data-table__row">
<td class="mdc-data-table__cell cell-bullet">
<div
@@ -229,22 +234,26 @@ export class HuiEnergySourcesTableCard
this._data!.stats[source.stat_energy_to]
) || 0;
totalBattery += energyFrom - energyTo;
const fromColor =
const modifiedFromColor =
idx > 0
? rgb2hex(
lab2rgb(
labDarken(rgb2lab(hex2rgb(batteryFromColor)), idx)
)
)
: batteryFromColor;
const toColor =
? this.hass.themes.darkMode
? labBrighten(rgb2lab(hex2rgb(batteryFromColor)), idx)
: labDarken(rgb2lab(hex2rgb(batteryFromColor)), idx)
: undefined;
const fromColor = modifiedFromColor
? rgb2hex(lab2rgb(modifiedFromColor))
: batteryFromColor;
const modifiedToColor =
idx > 0
? rgb2hex(
lab2rgb(
labDarken(rgb2lab(hex2rgb(batteryToColor)), idx)
)
)
: batteryToColor;
? this.hass.themes.darkMode
? labBrighten(rgb2lab(hex2rgb(batteryToColor)), idx)
: labDarken(rgb2lab(hex2rgb(batteryToColor)), idx)
: undefined;
const toColor = modifiedToColor
? rgb2hex(lab2rgb(modifiedToColor))
: batteryToColor;
return html`<tr class="mdc-data-table__row">
<td class="mdc-data-table__cell cell-bullet">
<div
@@ -331,14 +340,17 @@ export class HuiEnergySourcesTableCard
if (cost !== null) {
totalGridCost += cost;
}
const color =
const modifiedColor =
idx > 0
? rgb2hex(
lab2rgb(
labDarken(rgb2lab(hex2rgb(consumptionColor)), idx)
)
)
: consumptionColor;
? this.hass.themes.darkMode
? labBrighten(rgb2lab(hex2rgb(consumptionColor)), idx)
: labDarken(rgb2lab(hex2rgb(consumptionColor)), idx)
: undefined;
const color = modifiedColor
? rgb2hex(lab2rgb(modifiedColor))
: consumptionColor;
return html`<tr class="mdc-data-table__row">
<td class="mdc-data-table__cell cell-bullet">
<div
@@ -391,12 +403,17 @@ export class HuiEnergySourcesTableCard
if (cost !== null) {
totalGridCost += cost;
}
const color =
const modifiedColor =
idx > 0
? rgb2hex(
lab2rgb(labDarken(rgb2lab(hex2rgb(returnColor)), idx))
)
: returnColor;
? this.hass.themes.darkMode
? labBrighten(rgb2lab(hex2rgb(returnColor)), idx)
: labDarken(rgb2lab(hex2rgb(returnColor)), idx)
: undefined;
const color = modifiedColor
? rgb2hex(lab2rgb(modifiedColor))
: returnColor;
return html`<tr class="mdc-data-table__row">
<td class="mdc-data-table__cell cell-bullet">
<div
@@ -473,12 +490,17 @@ export class HuiEnergySourcesTableCard
if (cost !== null) {
totalGasCost += cost;
}
const color =
const modifiedColor =
idx > 0
? rgb2hex(
lab2rgb(labDarken(rgb2lab(hex2rgb(gasColor)), idx))
)
: gasColor;
? this.hass.themes.darkMode
? labBrighten(rgb2lab(hex2rgb(gasColor)), idx)
: labDarken(rgb2lab(hex2rgb(gasColor)), idx)
: undefined;
const color = modifiedColor
? rgb2hex(lab2rgb(modifiedColor))
: gasColor;
return html`<tr class="mdc-data-table__row">
<td class="mdc-data-table__cell cell-bullet">
<div

View File

@@ -1,10 +1,10 @@
import { ChartData, ChartDataset, ChartOptions } from "chart.js";
import {
startOfToday,
addHours,
differenceInDays,
endOfToday,
isToday,
differenceInDays,
addHours,
startOfToday,
} from "date-fns";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
@@ -17,7 +17,7 @@ import {
rgb2hex,
rgb2lab,
} from "../../../../common/color/convert-color";
import { labDarken } from "../../../../common/color/lab";
import { labBrighten, labDarken } from "../../../../common/color/lab";
import { formatTime } from "../../../../common/datetime/format_time";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import {
@@ -477,10 +477,16 @@ export class HuiEnergyUsageGraphCard
Object.entries(sources).forEach(([statId, source], idx) => {
const data: ChartDataset<"bar">[] = [];
const entity = this.hass.states[statId];
const borderColor =
const modifiedColor =
idx > 0
? rgb2hex(lab2rgb(labDarken(rgb2lab(hex2rgb(colors[type])), idx)))
: colors[type];
? this.hass.themes.darkMode
? labBrighten(rgb2lab(hex2rgb(colors[type])), idx)
: labDarken(rgb2lab(hex2rgb(colors[type])), idx)
: undefined;
const borderColor = modifiedColor
? rgb2hex(lab2rgb(modifiedColor))
: colors[type];
data.push({
label:

View File

@@ -1,4 +1,12 @@
import "@material/mwc-ripple";
import {
mdiLightbulbMultiple,
mdiLightbulbMultipleOff,
mdiRun,
mdiToggleSwitch,
mdiToggleSwitchOff,
mdiWaterPercent,
} from "@mdi/js";
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
@@ -10,13 +18,14 @@ import {
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { STATES_OFF } from "../../../common/const";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { domainIcon } from "../../../common/entity/domain_icon";
import { navigate } from "../../../common/navigate";
import { formatNumber } from "../../../common/number/format_number";
import { subscribeOne } from "../../../common/util/subscribe-one";
import "../../../components/entity/state-badge";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
@@ -30,31 +39,40 @@ import {
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../../../data/device_registry";
import { UNAVAILABLE_STATES } from "../../../data/entity";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../../data/entity_registry";
import { forwardHaptic } from "../../../data/haptics";
import { ActionHandlerEvent } from "../../../data/lovelace";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { toggleEntity } from "../common/entity/toggle-entity";
import "../components/hui-warning";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { AreaCardConfig } from "./types";
const SENSOR_DOMAINS = new Set(["sensor", "binary_sensor"]);
const SENSOR_DOMAINS = ["sensor"];
const SENSOR_DEVICE_CLASSES = new Set([
"temperature",
"humidity",
"motion",
"door",
"aqi",
]);
const ALERT_DOMAINS = ["binary_sensor"];
const TOGGLE_DOMAINS = new Set(["light", "fan", "switch"]);
const TOGGLE_DOMAINS = ["light", "switch", "fan"];
const OTHER_DOMAINS = ["camera"];
const DEVICE_CLASSES = {
sensor: ["temperature"],
binary_sensor: ["motion"],
};
const DOMAIN_ICONS = {
light: { on: mdiLightbulbMultiple, off: mdiLightbulbMultipleOff },
switch: { on: mdiToggleSwitch, off: mdiToggleSwitchOff },
fan: { on: domainIcon("fan"), off: domainIcon("fan") },
sensor: { humidity: mdiWaterPercent },
binary_sensor: {
motion: mdiRun,
},
};
@customElement("hui-area-card")
export class HuiAreaCard
@@ -66,8 +84,11 @@ export class HuiAreaCard
return document.createElement("hui-area-card-editor");
}
public static getStubConfig(): AreaCardConfig {
return { type: "area", area: "" };
public static async getStubConfig(
hass: HomeAssistant
): Promise<AreaCardConfig> {
const areas = await subscribeOne(hass.connection, subscribeAreaRegistry);
return { type: "area", area: areas[0]?.area_id || "" };
}
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -80,7 +101,7 @@ export class HuiAreaCard
@state() private _areas?: AreaRegistryEntry[];
private _memberships = memoizeOne(
private _entitiesByDomain = memoizeOne(
(
areaId: string,
devicesInArea: Set<string>,
@@ -97,44 +118,104 @@ export class HuiAreaCard
)
.map((entry) => entry.entity_id);
const sensorEntities: HassEntity[] = [];
const entitiesToggle: HassEntity[] = [];
const entitiesByDomain: { [domain: string]: HassEntity[] } = {};
for (const entity of entitiesInArea) {
const domain = computeDomain(entity);
if (!TOGGLE_DOMAINS.has(domain) && !SENSOR_DOMAINS.has(domain)) {
if (
!TOGGLE_DOMAINS.includes(domain) &&
!SENSOR_DOMAINS.includes(domain) &&
!ALERT_DOMAINS.includes(domain) &&
!OTHER_DOMAINS.includes(domain)
) {
continue;
}
const stateObj: HassEntity | undefined = states[entity];
if (!stateObj) {
continue;
}
if (entitiesToggle.length < 3 && TOGGLE_DOMAINS.has(domain)) {
entitiesToggle.push(stateObj);
if (
(SENSOR_DOMAINS.includes(domain) || ALERT_DOMAINS.includes(domain)) &&
!DEVICE_CLASSES[domain].includes(
stateObj.attributes.device_class || ""
)
) {
continue;
}
if (
sensorEntities.length < 3 &&
SENSOR_DOMAINS.has(domain) &&
stateObj.attributes.device_class &&
SENSOR_DEVICE_CLASSES.has(stateObj.attributes.device_class)
) {
sensorEntities.push(stateObj);
}
if (sensorEntities.length === 3 && entitiesToggle.length === 3) {
break;
if (!(domain in entitiesByDomain)) {
entitiesByDomain[domain] = [];
}
entitiesByDomain[domain].push(stateObj);
}
return { sensorEntities, entitiesToggle };
return entitiesByDomain;
}
);
private _isOn(domain: string, deviceClass?: string): boolean | undefined {
const entities = this._entitiesByDomain(
this._config!.area,
this._devicesInArea(this._config!.area, this._devices!),
this._entities!,
this.hass.states
)[domain];
if (!entities) {
return undefined;
}
return (
deviceClass
? entities.filter(
(entity) => entity.attributes.device_class === deviceClass
)
: entities
).some(
(entity) =>
!UNAVAILABLE_STATES.includes(entity.state) &&
!STATES_OFF.includes(entity.state)
);
}
private _average(domain: string, deviceClass?: string): string | undefined {
const entities = this._entitiesByDomain(
this._config!.area,
this._devicesInArea(this._config!.area, this._devices!),
this._entities!,
this.hass.states
)[domain].filter((entity) =>
deviceClass ? entity.attributes.device_class === deviceClass : true
);
if (!entities) {
return undefined;
}
let uom;
const values = entities.filter((entity) => {
if (
!entity.attributes.unit_of_measurement ||
isNaN(Number(entity.state))
) {
return false;
}
if (!uom) {
uom = entity.attributes.unit_of_measurement;
return true;
}
return entity.attributes.unit_of_measurement === uom;
});
if (!values.length) {
return undefined;
}
const sum = values.reduce(
(total, entity) => total + Number(entity.state),
0
);
return `${formatNumber(sum / values.length, this.hass!.locale, {
maximumFractionDigits: 1,
})} ${uom}`;
}
private _area = memoizeOne(
(areaId: string | undefined, areas: AreaRegistryEntry[]) =>
areas.find((area) => area.area_id === areaId) || null
@@ -212,22 +293,18 @@ export class HuiAreaCard
return false;
}
const { sensorEntities, entitiesToggle } = this._memberships(
const entities = this._entitiesByDomain(
this._config.area,
this._devicesInArea(this._config.area, this._devices),
this._entities,
this.hass.states
);
for (const stateObj of sensorEntities) {
if (oldHass!.states[stateObj.entity_id] !== stateObj) {
return true;
}
}
for (const stateObj of entitiesToggle) {
if (oldHass!.states[stateObj.entity_id] !== stateObj) {
return true;
for (const domainEntities of Object.values(entities)) {
for (const stateObj of domainEntities) {
if (oldHass!.states[stateObj.entity_id] !== stateObj) {
return true;
}
}
}
@@ -245,13 +322,12 @@ export class HuiAreaCard
return html``;
}
const { sensorEntities, entitiesToggle } = this._memberships(
const entitiesByDomain = this._entitiesByDomain(
this._config.area,
this._devicesInArea(this._config.area, this._devices),
this._entities,
this.hass.states
);
const area = this._area(this._config.area, this._areas);
if (area === null) {
@@ -262,62 +338,98 @@ export class HuiAreaCard
`;
}
const sensors: TemplateResult[] = [];
SENSOR_DOMAINS.forEach((domain) => {
if (!(domain in entitiesByDomain)) {
return;
}
DEVICE_CLASSES[domain].forEach((deviceClass) => {
if (
entitiesByDomain[domain].some(
(entity) => entity.attributes.device_class === deviceClass
)
) {
sensors.push(html`
${DOMAIN_ICONS[domain][deviceClass]
? html`<ha-svg-icon
.path=${DOMAIN_ICONS[domain][deviceClass]}
></ha-svg-icon>`
: ""}
${this._average(domain, deviceClass)}
`);
}
});
});
let cameraEntityId: string | undefined;
if (this._config.show_camera && "camera" in entitiesByDomain) {
cameraEntityId = entitiesByDomain.camera[0].entity_id;
}
return html`
<ha-card
style=${styleMap({
"background-image": `url(${this.hass.hassUrl(area.picture)})`,
})}
>
<div class="container">
<div class="sensors">
${sensorEntities.map(
(stateObj) => html`
<span
.entity=${stateObj.entity_id}
@click=${this._handleMoreInfo}
>
<ha-state-icon .state=${stateObj}></ha-state-icon>
${computeDomain(stateObj.entity_id) === "binary_sensor"
? ""
: html`
${computeStateDisplay(
this.hass!.localize,
stateObj,
this.hass!.locale
)}
`}
</span>
`
)}
<ha-card class=${area.picture || cameraEntityId ? "image" : ""}>
${area.picture || cameraEntityId
? html`<hui-image
.config=${this._config}
.hass=${this.hass}
.image=${area.picture
? this.hass.hassUrl(area.picture)
: undefined}
.cameraImage=${cameraEntityId}
aspectRatio="16:9"
></hui-image>`
: ""}
<div
class="container ${classMap({
navigate: this._config.navigation_path !== undefined,
})}"
@click=${this._handleNavigation}
>
<div class="alerts">
${ALERT_DOMAINS.map((domain) => {
if (!(domain in entitiesByDomain)) {
return "";
}
return DEVICE_CLASSES[domain].map((deviceClass) =>
this._isOn(domain, deviceClass)
? html`
${DOMAIN_ICONS[domain][deviceClass]
? html`<ha-svg-icon
.path=${DOMAIN_ICONS[domain][deviceClass]}
></ha-svg-icon>`
: ""}
`
: ""
);
})}
</div>
<div class="bottom">
<div
class="name ${this._config.navigation_path ? "navigate" : ""}"
@click=${this._handleNavigation}
>
${area.name}
<div>
<div class="name">${area.name}</div>
${sensors.length
? html`<div class="sensors">${sensors}</div>`
: ""}
</div>
<div class="buttons">
${entitiesToggle.map(
(stateObj) => html`
<ha-icon-button
class=${classMap({
off: stateObj.state === "off",
})}
.entity=${stateObj.entity_id}
.actionHandler=${actionHandler({
hasHold: true,
})}
@action=${this._handleAction}
>
<state-badge
.hass=${this.hass}
.stateObj=${stateObj}
stateColor
></state-badge>
</ha-icon-button>
`
)}
${TOGGLE_DOMAINS.map((domain) => {
if (!(domain in entitiesByDomain)) {
return "";
}
const on = this._isOn(domain)!;
return TOGGLE_DOMAINS.includes(domain)
? html`
<ha-icon-button
class=${on ? "on" : "off"}
.path=${DOMAIN_ICONS[domain][on ? "on" : "off"]}
.domain=${domain}
@click=${this._toggle}
>
</ha-icon-button>
`
: "";
})}
</div>
</div>
</div>
@@ -343,25 +455,26 @@ export class HuiAreaCard
}
}
private _handleMoreInfo(ev) {
const entity = (ev.currentTarget as any).entity;
fireEvent(this, "hass-more-info", { entityId: entity });
}
private _handleNavigation() {
if (this._config!.navigation_path) {
navigate(this._config!.navigation_path);
}
}
private _handleAction(ev: ActionHandlerEvent) {
const entity = (ev.currentTarget as any).entity as string;
if (ev.detail.action === "hold") {
fireEvent(this, "hass-more-info", { entityId: entity });
} else if (ev.detail.action === "tap") {
toggleEntity(this.hass, entity);
forwardHaptic("light");
private _toggle(ev: Event) {
ev.stopPropagation();
const domain = (ev.currentTarget as any).domain as string;
if (TOGGLE_DOMAINS.includes(domain)) {
this.hass.callService(
domain,
this._isOn(domain) ? "turn_off" : "turn_on",
undefined,
{
area_id: this._config!.area,
}
);
}
forwardHaptic("light");
}
static get styles(): CSSResultGroup {
@@ -373,24 +486,52 @@ export class HuiAreaCard
background-size: cover;
}
ha-card.image {
padding-bottom: 0;
}
.container {
display: flex;
flex-direction: column;
justify-content: space-between;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.4);
background: linear-gradient(
0,
rgba(33, 33, 33, 0.9) 0%,
rgba(33, 33, 33, 0) 45%
);
}
ha-card:not(.image) .container::before {
position: absolute;
content: "";
width: 100%;
height: 100%;
background-color: var(--sidebar-selected-icon-color);
opacity: 0.12;
}
.sensors {
color: white;
font-size: 18px;
flex: 1;
color: #e3e3e3;
font-size: 16px;
--mdc-icon-size: 24px;
opacity: 0.6;
margin-top: 8px;
}
.alerts {
padding: 16px;
--mdc-icon-size: 28px;
cursor: pointer;
}
.alerts ha-svg-icon {
background: var(--accent-color);
color: var(--text-accent-color, var(--text-primary-color));
padding: 8px;
border-radius: 50%;
}
.name {
@@ -402,24 +543,23 @@ export class HuiAreaCard
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 8px 8px 16px;
padding: 16px;
}
.name.navigate {
.navigate {
cursor: pointer;
}
state-badge {
--ha-icon-display: inline;
}
ha-icon-button {
color: white;
background-color: var(--area-button-color, rgb(175, 175, 175, 0.5));
background-color: var(--area-button-color, #727272b2);
border-radius: 50%;
margin-left: 8px;
--mdc-icon-button-size: 44px;
}
.on {
color: var(--paper-item-icon-active-color, #fdd835);
}
`;
}
}

View File

@@ -134,7 +134,10 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
}
if (this._config.header) {
this._headerElement = createHeaderFooterElement(this._config.header);
this._headerElement = createHeaderFooterElement(
this._config.header
) as LovelaceHeaderFooter;
this._headerElement.type = "header";
if (this._hass) {
this._headerElement.hass = this._hass;
}
@@ -143,7 +146,10 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
}
if (this._config.footer) {
this._footerElement = createHeaderFooterElement(this._config.footer);
this._footerElement = createHeaderFooterElement(
this._config.footer
) as LovelaceHeaderFooter;
this._footerElement.type = "footer";
if (this._hass) {
this._footerElement.hass = this._hass;
}

View File

@@ -17,7 +17,6 @@ import "../../../components/ha-card";
import "../../../components/ha-icon-button";
import { fetchRecent } from "../../../data/history";
import { HomeAssistant } from "../../../types";
import "../../../components/map/ha-entity-marker";
import { findEntities } from "../common/find-entities";
import { processConfigEntities } from "../common/process-config-entities";
import { EntityConfig } from "../entity-rows/types";

View File

@@ -235,6 +235,9 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
<div>
<ha-icon-button
.path=${mdiDotsVertical}
.label=${this.hass.localize(
"ui.panel.lovelace.cards.show_more_info"
)}
class="more-info"
@click=${this._handleMoreInfo}
></ha-icon-button>

View File

@@ -19,7 +19,7 @@ import {
svg,
TemplateResult,
} from "lit";
import { customElement, property, state, query } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { UNIT_F } from "../../../common/const";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
@@ -427,6 +427,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
@click=${this._handleAction}
tabindex="0"
.path=${modeIcons[mode]}
.label=${this.hass!.localize(`component.climate.state._.${mode}`)}
>
</ha-icon-button>
`;

View File

@@ -79,6 +79,7 @@ export interface EntitiesCardConfig extends LovelaceCardConfig {
export interface AreaCardConfig extends LovelaceCardConfig {
area: string;
navigation_path?: string;
show_camera?: boolean;
}
export interface ButtonCardConfig extends LovelaceCardConfig {

View File

@@ -9,6 +9,8 @@ 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 "../../../components/ha-chip";
import { haStyleScrollbar } from "../../../resources/styles";
@customElement("hui-buttons-base")
export class HuiButtonsBase extends LitElement {
@@ -18,40 +20,47 @@ export class HuiButtonsBase extends LitElement {
protected render(): TemplateResult {
return html`
${(this.configEntities || []).map((entityConf) => {
const stateObj = this.hass.states[entityConf.entity];
<div class="ha-scrollbar">
${(this.configEntities || []).map((entityConf) => {
const stateObj = this.hass.states[entityConf.entity];
return html`
<div
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(entityConf.hold_action),
hasDoubleClick: hasAction(entityConf.double_tap_action),
})}
.config=${entityConf}
tabindex="0"
>
${entityConf.show_icon !== false
? html`
<state-badge
title=${computeTooltip(this.hass, entityConf)}
.hass=${this.hass}
.stateObj=${stateObj}
.overrideIcon=${entityConf.icon}
.overrideImage=${entityConf.image}
stateColor
></state-badge>
`
: ""}
<span>
${(entityConf.show_name && stateObj) ||
(entityConf.name && entityConf.show_name !== false)
? entityConf.name || computeStateName(stateObj)
const name =
(entityConf.show_name && stateObj) ||
(entityConf.name && entityConf.show_name !== false)
? entityConf.name || computeStateName(stateObj)
: "";
return html`
<ha-chip
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(entityConf.hold_action),
hasDoubleClick: hasAction(entityConf.double_tap_action),
})}
.config=${entityConf}
tabindex="0"
.hasIcon=${entityConf.show_icon !== false}
.noText=${!name}
>
${entityConf.show_icon !== false
? html`
<state-badge
title=${computeTooltip(this.hass, entityConf)}
.hass=${this.hass}
.stateObj=${stateObj}
.overrideIcon=${entityConf.icon}
.overrideImage=${entityConf.image}
class=${name ? "" : "no-text"}
stateColor
slot="icon"
></state-badge>
`
: ""}
</span>
</div>
`;
})}
${name}
</ha-chip>
`;
})}
</div>
`;
}
@@ -61,20 +70,48 @@ export class HuiButtonsBase extends LitElement {
}
static get styles(): CSSResultGroup {
return css`
:host {
display: flex;
justify-content: space-evenly;
flex-wrap: wrap;
padding: 0 8px;
}
div {
cursor: pointer;
align-items: center;
display: inline-flex;
outline: none;
}
`;
return [
haStyleScrollbar,
css`
.ha-scrollbar {
padding: 8px;
padding-top: var(--padding-top, 8px);
padding-bottom: var(--padding-bottom, 8px);
width: 100%;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
box-sizing: border-box;
display: flex;
flex-wrap: wrap;
}
state-badge {
display: inline-flex;
line-height: inherit;
color: var(--secondary-text-color);
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
margin-left: -4px;
margin-top: -2px;
}
state-badge.no-text {
width: 26px;
height: 26px;
margin-left: -3px;
margin-top: -3px;
}
ha-chip {
padding: 4px;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
.ha-scrollbar {
flex-wrap: nowrap;
}
}
`,
];
}
}

View File

@@ -9,7 +9,7 @@ import {
import { property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { DOMAINS_HIDE_MORE_INFO } from "../../../common/const";
import { DOMAINS_INPUT_ROW } from "../../../common/const";
import { toggleAttribute } from "../../../common/dom/toggle_attribute";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
@@ -31,6 +31,8 @@ class HuiGenericEntityRow extends LitElement {
@property() public secondaryText?: string;
@property({ type: Boolean }) public hideName = false;
protected render(): TemplateResult {
if (!this.hass || !this.config) {
return html``;
@@ -47,10 +49,10 @@ class HuiGenericEntityRow extends LitElement {
`;
}
const pointer =
(this.config.tap_action && this.config.tap_action.action !== "none") ||
(this.config.entity &&
!DOMAINS_HIDE_MORE_INFO.includes(computeDomain(this.config.entity)));
const domain = computeDomain(this.config.entity);
const pointer = !(
this.config.tap_action && this.config.tap_action.action !== "none"
);
const hasSecondary = this.secondaryText || this.config.secondary_info;
const name = this.config.name ?? computeStateName(stateObj);
@@ -72,75 +74,90 @@ class HuiGenericEntityRow extends LitElement {
})}
tabindex=${ifDefined(pointer ? "0" : undefined)}
></state-badge>
<div
class="info ${classMap({
pointer,
"text-content": !hasSecondary,
})}"
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this.config!.hold_action),
hasDoubleClick: hasAction(this.config!.double_tap_action),
})}
.title=${name}
>
${name}
${hasSecondary
? html`
<div class="secondary">
${this.secondaryText ||
(this.config.secondary_info === "entity-id"
? stateObj.entity_id
: this.config.secondary_info === "last-changed"
? html`
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.last_changed}
capitalize
></ha-relative-time>
`
: this.config.secondary_info === "last-updated"
? html`
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.last_updated}
capitalize
></ha-relative-time>
`
: this.config.secondary_info === "last-triggered"
? stateObj.attributes.last_triggered
? html`
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.attributes.last_triggered}
capitalize
></ha-relative-time>
`
: this.hass.localize(
"ui.panel.lovelace.cards.entities.never_triggered"
)
: this.config.secondary_info === "position" &&
stateObj.attributes.current_position !== undefined
? `${this.hass.localize("ui.card.cover.position")}: ${
stateObj.attributes.current_position
}`
: this.config.secondary_info === "tilt-position" &&
stateObj.attributes.current_tilt_position !== undefined
? `${this.hass.localize("ui.card.cover.tilt_position")}: ${
stateObj.attributes.current_tilt_position
}`
: this.config.secondary_info === "brightness" &&
stateObj.attributes.brightness
? html`${Math.round(
(stateObj.attributes.brightness / 255) * 100
)}
%`
: "")}
</div>
`
: ""}
</div>
<slot></slot>
${!this.hideName
? html` <div
class="info ${classMap({
pointer,
"text-content": !hasSecondary,
})}"
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this.config!.hold_action),
hasDoubleClick: hasAction(this.config!.double_tap_action),
})}
.title=${name}
>
${this.config.name || computeStateName(stateObj)}
${hasSecondary
? html`
<div class="secondary">
${this.secondaryText ||
(this.config.secondary_info === "entity-id"
? stateObj.entity_id
: this.config.secondary_info === "last-changed"
? html`
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.last_changed}
capitalize
></ha-relative-time>
`
: this.config.secondary_info === "last-updated"
? html`
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.last_updated}
capitalize
></ha-relative-time>
`
: this.config.secondary_info === "last-triggered"
? stateObj.attributes.last_triggered
? html`
<ha-relative-time
.hass=${this.hass}
.datetime=${stateObj.attributes.last_triggered}
capitalize
></ha-relative-time>
`
: this.hass.localize(
"ui.panel.lovelace.cards.entities.never_triggered"
)
: this.config.secondary_info === "position" &&
stateObj.attributes.current_position !== undefined
? `${this.hass.localize("ui.card.cover.position")}: ${
stateObj.attributes.current_position
}`
: this.config.secondary_info === "tilt-position" &&
stateObj.attributes.current_tilt_position !== undefined
? `${this.hass.localize(
"ui.card.cover.tilt_position"
)}: ${stateObj.attributes.current_tilt_position}`
: this.config.secondary_info === "brightness" &&
stateObj.attributes.brightness
? html`${Math.round(
(stateObj.attributes.brightness / 255) * 100
)}
%`
: "")}
</div>
`
: ""}
</div>`
: html``}
${!DOMAINS_INPUT_ROW.includes(domain)
? html` <div
class="text-content ${classMap({
pointer,
})}"
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(this.config!.hold_action),
hasDoubleClick: hasAction(this.config!.double_tap_action),
})}
>
<slot></slot>
</div>`
: html`<slot></slot>`}
`;
}

View File

@@ -1,7 +1,7 @@
import "@polymer/paper-input/paper-input";
import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, object, optional, string } from "superstruct";
import { assert, assign, boolean, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-area-picker";
import { HomeAssistant } from "../../../../types";
@@ -11,6 +11,8 @@ import { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { EditorTarget } from "../types";
import { configElementStyle } from "./config-elements-style";
import "../../../../components/ha-formfield";
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
@@ -18,6 +20,7 @@ const cardConfigStruct = assign(
area: optional(string()),
navigation_path: optional(string()),
theme: optional(string()),
show_camera: optional(boolean()),
})
);
@@ -47,6 +50,10 @@ export class HuiAreaCardEditor
return this._config!.theme || "";
}
get _show_camera(): boolean {
return this._config!.show_camera || false;
}
protected render(): TemplateResult {
if (!this.hass || !this._config) {
return html``;
@@ -59,9 +66,23 @@ export class HuiAreaCardEditor
.value=${this._area}
.placeholder=${this._area}
.configValue=${"area"}
.label=${this.hass.localize("ui.dialogs.entity_registry.editor.area")}
.label=${this.hass.localize(
"ui.panel.lovelace.editor.card.area.name"
)}
@value-changed=${this._valueChanged}
></ha-area-picker>
<ha-formfield
.label=${this.hass.localize(
"ui.panel.lovelace.editor.card.area.show_camera"
)}
.dir=${computeRTLDirection(this.hass)}
>
<ha-switch
.checked=${this._show_camera}
.configValue=${"show_camera"}
@change=${this._valueChanged}
></ha-switch>
</ha-formfield>
<paper-input
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.action-editor.navigation_path"
@@ -86,7 +107,8 @@ export class HuiAreaCardEditor
return;
}
const target = ev.target! as EditorTarget;
const value = ev.detail.value;
const value =
target.checked !== undefined ? target.checked : ev.detail.value;
if (this[`_${target.configValue}`] === value) {
return;

View File

@@ -13,7 +13,7 @@ export const getCardStubConfig = async (
const elClass = await getCardElementClass(type);
if (elClass && elClass.getStubConfig) {
const classStubConfig = elClass.getStubConfig(
const classStubConfig = await elClass.getStubConfig(
hass,
entities,
entitiesFallback

Some files were not shown because too many files have changed in this diff Show More