mirror of
https://github.com/home-assistant/frontend.git
synced 2025-09-27 05:49:39 +00:00
Compare commits
92 Commits
20211201.0
...
Move-parti
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a0b11eb357 | ||
![]() |
6f9b2ee569 | ||
![]() |
4ebdca2a46 | ||
![]() |
fc700fdaf0 | ||
![]() |
d8e12f4280 | ||
![]() |
86114758c3 | ||
![]() |
792278cf17 | ||
![]() |
b8832f2121 | ||
![]() |
76339c90f7 | ||
![]() |
b3d4451035 | ||
![]() |
dc58481918 | ||
![]() |
14af735507 | ||
![]() |
a7b558b64a | ||
![]() |
b7665bef6f | ||
![]() |
5ec37a35f1 | ||
![]() |
91bb2ddcc4 | ||
![]() |
85168b3a35 | ||
![]() |
942150cda2 | ||
![]() |
2606d55895 | ||
![]() |
1f671198aa | ||
![]() |
deb65e7108 | ||
![]() |
cd00f7f874 | ||
![]() |
2b0359edba | ||
![]() |
35e9687170 | ||
![]() |
b730676914 | ||
![]() |
2890192c05 | ||
![]() |
bfb84a834f | ||
![]() |
ca6fd6c770 | ||
![]() |
585648ac4c | ||
![]() |
bec5c564b6 | ||
![]() |
48c66e6349 | ||
![]() |
cea40610c0 | ||
![]() |
0c3fd8f3ad | ||
![]() |
02bdeebc82 | ||
![]() |
60c7669d8f | ||
![]() |
919bf94a03 | ||
![]() |
ead5e288eb | ||
![]() |
add8a702cc | ||
![]() |
39774c0e02 | ||
![]() |
149f381bc3 | ||
![]() |
faccb12430 | ||
![]() |
7039bae9be | ||
![]() |
0a7b703d57 | ||
![]() |
24e8028e8f | ||
![]() |
8412cd71cb | ||
![]() |
5c78b74005 | ||
![]() |
2459477ec4 | ||
![]() |
a065740c91 | ||
![]() |
f3104d3c93 | ||
![]() |
1916c179b4 | ||
![]() |
e8b9766eb6 | ||
![]() |
ff7a2c8cb7 | ||
![]() |
7ccde2cb41 | ||
![]() |
d6b9b16f02 | ||
![]() |
66df15007a | ||
![]() |
f164d21c44 | ||
![]() |
911d322aac | ||
![]() |
419879ee7a | ||
![]() |
c3e1a2edf0 | ||
![]() |
8f5751d5bb | ||
![]() |
4095450476 | ||
![]() |
e61f587c51 | ||
![]() |
d43d19190e | ||
![]() |
a283acaabf | ||
![]() |
ea18fc0078 | ||
![]() |
1df11e9bf1 | ||
![]() |
c71b2e6b9d | ||
![]() |
db4aa05bf4 | ||
![]() |
a54a2a54f8 | ||
![]() |
0bcb4d0e09 | ||
![]() |
95dbc811d3 | ||
![]() |
e28a11964e | ||
![]() |
46a9e36516 | ||
![]() |
e99f20c4f3 | ||
![]() |
2100603cdc | ||
![]() |
da4942aca3 | ||
![]() |
7c78fb314e | ||
![]() |
5bc2468cbc | ||
![]() |
a580904c52 | ||
![]() |
48d12ceafe | ||
![]() |
60ce805b3b | ||
![]() |
251416b51d | ||
![]() |
c41c6eedd8 | ||
![]() |
6877fd9e00 | ||
![]() |
4cc104a99f | ||
![]() |
6494177821 | ||
![]() |
cea1a62867 | ||
![]() |
a6b5262d02 | ||
![]() |
2a5fc5181e | ||
![]() |
2fe8f5ff27 | ||
![]() |
0c75d5afc9 | ||
![]() |
cf062bf0f4 |
@@ -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) {
|
||||
|
@@ -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() {
|
||||
|
@@ -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,
|
||||
|
@@ -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}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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",
|
||||
|
2
setup.py
2
setup.py
@@ -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",
|
||||
|
@@ -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) {
|
||||
|
@@ -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)
|
||||
);
|
||||
}
|
||||
|
@@ -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"];
|
||||
|
||||
|
@@ -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 };
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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({
|
||||
|
@@ -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;
|
||||
|
@@ -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"] = [];
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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 [];
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -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()}
|
||||
|
@@ -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}
|
||||
|
@@ -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>`;
|
||||
|
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -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}
|
||||
|
@@ -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,
|
||||
|
@@ -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> `
|
||||
)}
|
||||
`}
|
||||
|
@@ -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, {
|
||||
|
@@ -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;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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}"`
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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 }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@@ -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 (
|
||||
|
@@ -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 "%";
|
||||
|
@@ -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) =>
|
||||
|
@@ -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,
|
||||
|
@@ -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>
|
||||
`
|
||||
|
@@ -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";
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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 };
|
||||
}
|
||||
};
|
||||
|
@@ -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);
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@@ -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);
|
||||
|
@@ -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",
|
||||
|
@@ -201,6 +201,7 @@ export const provideHass = (
|
||||
default_dark_theme: null,
|
||||
themes: {},
|
||||
darkMode: false,
|
||||
theme: "default",
|
||||
},
|
||||
panels: demoPanels,
|
||||
services: demoServices,
|
||||
|
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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: "",
|
||||
|
@@ -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",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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(
|
||||
|
@@ -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%;
|
||||
|
@@ -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++;
|
||||
}
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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:
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -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");
|
||||
|
13
src/panels/config/automation/structs.ts
Normal file
13
src/panels/config/automation/structs.ts
Normal 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()),
|
||||
});
|
@@ -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 });
|
||||
}
|
||||
|
||||
|
@@ -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() {
|
||||
|
@@ -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"
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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 {
|
||||
|
@@ -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>
|
||||
|
@@ -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`
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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}
|
||||
|
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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") {
|
||||
|
@@ -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");
|
||||
|
@@ -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 {
|
||||
|
@@ -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"),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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();
|
||||
}
|
||||
|
@@ -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>
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
@@ -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) {
|
||||
|
@@ -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;
|
||||
|
@@ -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) {
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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
|
||||
|
@@ -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:
|
||||
|
@@ -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);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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";
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
`;
|
||||
|
@@ -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 {
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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>`}
|
||||
`;
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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
Reference in New Issue
Block a user