mirror of
https://github.com/home-assistant/frontend.git
synced 2025-08-29 02:59:25 +00:00
Compare commits
77 Commits
20211202.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 |
@@ -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,
|
||||
|
@@ -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
|
||||
@@ -224,6 +221,9 @@ class UpdateAvailableCard extends LitElement {
|
||||
}
|
||||
|
||||
get _shouldCreateBackup(): boolean {
|
||||
if (this._updateType && !["core", "addon"].includes(this._updateType)) {
|
||||
return false;
|
||||
}
|
||||
const checkbox = this.shadowRoot?.querySelector("ha-checkbox");
|
||||
if (checkbox) {
|
||||
return checkbox.checked;
|
||||
@@ -310,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") {
|
||||
@@ -349,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;
|
||||
}
|
||||
}
|
||||
|
2
setup.py
2
setup.py
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name="home-assistant-frontend",
|
||||
version="20211202.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) {
|
||||
|
@@ -208,6 +208,7 @@ export const DOMAINS_HIDE_DEFAULT_MORE_INFO = [
|
||||
export const DOMAINS_INPUT_ROW = [
|
||||
"cover",
|
||||
"fan",
|
||||
"group",
|
||||
"humidifier",
|
||||
"input_boolean",
|
||||
"input_datetime",
|
||||
@@ -221,6 +222,7 @@ export const DOMAINS_INPUT_ROW = [
|
||||
"scene",
|
||||
"script",
|
||||
"select",
|
||||
"switch",
|
||||
];
|
||||
|
||||
/** Domains that should have the history hidden in the more info dialog. */
|
||||
|
@@ -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);
|
||||
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -18,13 +18,9 @@ export class HaChip extends LitElement {
|
||||
|
||||
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 ${this.noText
|
||||
? "no-text"
|
||||
: ""}"
|
||||
>
|
||||
? html`<div class="mdc-chip__icon mdc-chip__icon--leading">
|
||||
<slot name="icon"></slot>
|
||||
</div>`
|
||||
: null}
|
||||
@@ -49,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));
|
||||
}
|
||||
@@ -57,8 +57,8 @@ export class HaChip extends LitElement {
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--ha-chip-icon-color, var(--ha-chip-text-color));
|
||||
}
|
||||
.mdc-chip
|
||||
.mdc-chip__icon--leading:not(.mdc-chip__icon--leading-hidden).no-text {
|
||||
.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,6 +1,7 @@
|
||||
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
|
||||
@@ -13,6 +14,7 @@ export class HaFormfield extends Formfield {
|
||||
case "HA-CHECKBOX":
|
||||
case "HA-RADIO":
|
||||
(input as any).checked = !(input as any).checked;
|
||||
fireEvent(input, "change");
|
||||
break;
|
||||
default:
|
||||
input.click();
|
||||
|
@@ -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>
|
||||
`;
|
||||
}
|
||||
@@ -392,12 +402,8 @@ class HaSidebar extends LitElement {
|
||||
) {
|
||||
return html`
|
||||
<a
|
||||
aria-role="option"
|
||||
href=${`/${
|
||||
urlPath === "hassio"
|
||||
? "config/dashboard/?focusedPath=hassio"
|
||||
: urlPath
|
||||
}`}
|
||||
role="option"
|
||||
href=${`/${urlPath}`}
|
||||
data-panel=${urlPath}
|
||||
tabindex="-1"
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@@ -501,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>
|
||||
@@ -532,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}
|
||||
@@ -551,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 (
|
||||
|
@@ -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>
|
||||
`
|
||||
|
@@ -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);
|
||||
|
@@ -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;
|
||||
}
|
||||
|
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,27 +20,18 @@ class HaConfigNavigation extends LitElement {
|
||||
|
||||
@property() public pages!: PageNavigation[];
|
||||
|
||||
@property() public focusedPath?: string | null;
|
||||
|
||||
protected updated(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">
|
||||
<a href=${page.path} role="option" tabindex="-1">
|
||||
<paper-icon-item @click=${this._entryClicked}>
|
||||
<div
|
||||
class=${page.iconColor ? "icon-background" : ""}
|
||||
@@ -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>
|
||||
`}
|
||||
@@ -99,6 +83,16 @@ 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 {
|
||||
|
@@ -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",
|
||||
iconPath: mdiPuzzle,
|
||||
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:
|
||||
|
@@ -25,6 +25,7 @@ import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
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";
|
||||
@@ -83,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;
|
||||
@@ -188,7 +192,10 @@ export class HuiAreaCard
|
||||
}
|
||||
let uom;
|
||||
const values = entities.filter((entity) => {
|
||||
if (!entity.attributes.unit_of_measurement) {
|
||||
if (
|
||||
!entity.attributes.unit_of_measurement ||
|
||||
isNaN(Number(entity.state))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (!uom) {
|
||||
@@ -200,7 +207,10 @@ export class HuiAreaCard
|
||||
if (!values.length) {
|
||||
return undefined;
|
||||
}
|
||||
const sum = values.reduce((a, b) => a + Number(b.state), 0);
|
||||
const sum = values.reduce(
|
||||
(total, entity) => total + Number(entity.state),
|
||||
0
|
||||
);
|
||||
return `${formatNumber(sum / values.length, this.hass!.locale, {
|
||||
maximumFractionDigits: 1,
|
||||
})} ${uom}`;
|
||||
@@ -352,12 +362,12 @@ export class HuiAreaCard
|
||||
});
|
||||
|
||||
let cameraEntityId: string | undefined;
|
||||
if ("camera" in entitiesByDomain) {
|
||||
if (this._config.show_camera && "camera" in entitiesByDomain) {
|
||||
cameraEntityId = entitiesByDomain.camera[0].entity_id;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-card class=${area.picture ? "image" : ""}>
|
||||
<ha-card class=${area.picture || cameraEntityId ? "image" : ""}>
|
||||
${area.picture || cameraEntityId
|
||||
? html`<hui-image
|
||||
.config=${this._config}
|
||||
|
@@ -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 {
|
||||
|
@@ -50,6 +50,7 @@ export class HuiButtonsBase extends LitElement {
|
||||
.stateObj=${stateObj}
|
||||
.overrideIcon=${entityConf.icon}
|
||||
.overrideImage=${entityConf.image}
|
||||
class=${name ? "" : "no-text"}
|
||||
stateColor
|
||||
slot="icon"
|
||||
></state-badge>
|
||||
@@ -85,9 +86,21 @@ export class HuiButtonsBase extends LitElement {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
state-badge {
|
||||
display: inline-flex;
|
||||
line-height: inherit;
|
||||
text-align: start;
|
||||
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;
|
||||
|
@@ -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
|
||||
|
@@ -13,7 +13,7 @@ export const getHeaderFooterStubConfig = async (
|
||||
const elClass = await getHeaderFooterElementClass(type);
|
||||
|
||||
if (elClass && elClass.getStubConfig) {
|
||||
const classStubConfig = elClass.getStubConfig(
|
||||
const classStubConfig = await elClass.getStubConfig(
|
||||
hass,
|
||||
entities,
|
||||
entitiesFallback
|
||||
|
@@ -91,6 +91,7 @@ export const coreCards: Card[] = [
|
||||
},
|
||||
{
|
||||
type: "area",
|
||||
showElement: true,
|
||||
},
|
||||
{
|
||||
type: "conditional",
|
||||
|
@@ -110,7 +110,7 @@ class HuiInputSelectEntityRow extends LitElement implements LovelaceRow {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
hui-generic-entity-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||
import { html, LitElement, PropertyValues, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import { UNAVAILABLE, UNAVAILABLE_STATES } from "../../../data/entity";
|
||||
import { setValue } from "../../../data/input_text";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
@@ -67,6 +67,12 @@ class HuiInputTextEntityRow extends LitElement implements LovelaceRow {
|
||||
const element = this._inputEl;
|
||||
const stateObj = this.hass!.states[this._config!.entity];
|
||||
|
||||
// Filter out invalid text states
|
||||
if (element.value && UNAVAILABLE_STATES.includes(element.value)) {
|
||||
element.value = stateObj.state;
|
||||
return;
|
||||
}
|
||||
|
||||
if (element.value !== stateObj.state) {
|
||||
setValue(this.hass!, stateObj.entity_id, element.value!);
|
||||
}
|
||||
|
@@ -30,6 +30,7 @@ import "../../../components/ha-slider";
|
||||
import { UNAVAILABLE, UNAVAILABLE_STATES, UNKNOWN } from "../../../data/entity";
|
||||
import {
|
||||
computeMediaDescription,
|
||||
ControlButton,
|
||||
MediaPlayerEntity,
|
||||
SUPPORT_NEXT_TRACK,
|
||||
SUPPORT_PAUSE,
|
||||
@@ -108,6 +109,7 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
|
||||
}
|
||||
|
||||
const entityState = stateObj.state;
|
||||
const controlButton = this._computeControlButton(stateObj);
|
||||
|
||||
const buttons = html`
|
||||
${!this._narrow &&
|
||||
@@ -116,6 +118,9 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${mdiSkipPrevious}
|
||||
.label=${this.hass.localize(
|
||||
"ui.card.media_player.media_previous_track"
|
||||
)}
|
||||
@click=${this._previousTrack}
|
||||
></ha-icon-button>
|
||||
`
|
||||
@@ -130,7 +135,10 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
|
||||
supportsFeature(stateObj, SUPPORT_PAUSE)))
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${this._computeControlIcon(stateObj)}
|
||||
.path=${controlButton.icon}
|
||||
.label=${this.hass.localize(
|
||||
`ui.card.media_player.${controlButton.action}`
|
||||
)}
|
||||
@click=${this._playPauseStop}
|
||||
></ha-icon-button>
|
||||
`
|
||||
@@ -140,6 +148,9 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${mdiSkipNext}
|
||||
.label=${this.hass.localize(
|
||||
"ui.card.media_player.media_next_track"
|
||||
)}
|
||||
@click=${this._nextTrack}
|
||||
></ha-icon-button>
|
||||
`
|
||||
@@ -162,6 +173,7 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${mdiPower}
|
||||
.label=${this.hass.localize("ui.card.media_player.turn_on")}
|
||||
@click=${this._togglePower}
|
||||
></ha-icon-button>
|
||||
`
|
||||
@@ -175,6 +187,7 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${mdiPower}
|
||||
.label=${this.hass.localize("ui.card.media_player.turn_off")}
|
||||
@click=${this._togglePower}
|
||||
></ha-icon-button>
|
||||
`
|
||||
@@ -193,6 +206,13 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
|
||||
.path=${stateObj.attributes.is_volume_muted
|
||||
? mdiVolumeOff
|
||||
: mdiVolumeHigh}
|
||||
.label=${this.hass.localize(
|
||||
`ui.card.media_player.${
|
||||
stateObj.attributes.is_volume_muted
|
||||
? "media_volume_mute"
|
||||
: "media_volume_unmute"
|
||||
}`
|
||||
)}
|
||||
@click=${this._toggleMute}
|
||||
></ha-icon-button>
|
||||
`
|
||||
@@ -214,10 +234,16 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
|
||||
? html`
|
||||
<ha-icon-button
|
||||
.path=${mdiVolumeMinus}
|
||||
.label=${this.hass.localize(
|
||||
"ui.card.media_player.media_volume_down"
|
||||
)}
|
||||
@click=${this._volumeDown}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
.path=${mdiVolumePlus}
|
||||
.label=${this.hass.localize(
|
||||
"ui.card.media_player.media_volume_up"
|
||||
)}
|
||||
@click=${this._volumeUp}
|
||||
></ha-icon-button>
|
||||
`
|
||||
@@ -249,14 +275,14 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
|
||||
this._veryNarrow = (this.clientWidth || 0) < 225;
|
||||
}
|
||||
|
||||
private _computeControlIcon(stateObj: HassEntity): string {
|
||||
private _computeControlButton(stateObj: HassEntity): ControlButton {
|
||||
return stateObj.state === "on"
|
||||
? mdiPlayPause
|
||||
? { icon: mdiPlayPause, action: "media_play_pause" }
|
||||
: stateObj.state !== "playing"
|
||||
? mdiPlay
|
||||
? { icon: mdiPlay, action: "media_play" }
|
||||
: supportsFeature(stateObj, SUPPORT_PAUSE)
|
||||
? mdiPause
|
||||
: mdiStop;
|
||||
? { icon: mdiPause, action: "media_pause" }
|
||||
: { icon: mdiStop, action: "media_stop" };
|
||||
}
|
||||
|
||||
private _togglePower(): void {
|
||||
|
@@ -118,7 +118,7 @@ class HuiSelectEntityRow extends LitElement implements LovelaceRow {
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
hui-generic-entity-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
@@ -698,7 +698,7 @@ class HUIRoot extends LitElement {
|
||||
|
||||
private _navigateToView(path: string | number, replace?: boolean) {
|
||||
if (!this.lovelace!.editMode) {
|
||||
navigate(`${this.route!.prefix}/${path}`, { replace });
|
||||
navigate(`${this.route!.prefix}/${path}${location.search}`, { replace });
|
||||
return;
|
||||
}
|
||||
navigate(`${this.route!.prefix}/${path}?${addSearchParam({ edit: "1" })}`, {
|
||||
|
@@ -95,6 +95,7 @@ export const derivedStyles = {
|
||||
"mdc-theme-text-disabled-on-light": "var(--disabled-text-color)",
|
||||
"mdc-theme-text-primary-on-background": "var(--primary-text-color)",
|
||||
"mdc-theme-text-secondary-on-background": "var(--secondary-text-color)",
|
||||
"mdc-theme-text-hint-on-background": "var(--secondary-text-color)",
|
||||
"mdc-theme-text-icon-on-background": "var(--secondary-text-color)",
|
||||
"mdc-theme-error": "var(--error-color)",
|
||||
"app-header-text-color": "var(--text-primary-color)",
|
||||
|
15
src/state/external-mixin.ts
Normal file
15
src/state/external-mixin.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Constructor } from "../types";
|
||||
import { HassBaseEl } from "./hass-base-mixin";
|
||||
|
||||
export const ExternalMixin = <T extends Constructor<HassBaseEl>>(
|
||||
superClass: T
|
||||
) =>
|
||||
class extends superClass {
|
||||
protected hassConnected() {
|
||||
super.hassConnected();
|
||||
|
||||
if (this.hass!.auth.external) {
|
||||
this.hass!.auth.external.connection = this.hass!.connection;
|
||||
}
|
||||
}
|
||||
};
|
@@ -6,6 +6,7 @@ import DisconnectToastMixin from "./disconnect-toast-mixin";
|
||||
import { hapticMixin } from "./haptic-mixin";
|
||||
import { HassBaseEl } from "./hass-base-mixin";
|
||||
import { loggingMixin } from "./logging-mixin";
|
||||
import { ExternalMixin } from "./external-mixin";
|
||||
import MoreInfoMixin from "./more-info-mixin";
|
||||
import NotificationMixin from "./notification-mixin";
|
||||
import { panelTitleMixin } from "./panel-title-mixin";
|
||||
@@ -31,4 +32,5 @@ export class HassElement extends ext(HassBaseEl, [
|
||||
hapticMixin,
|
||||
panelTitleMixin,
|
||||
loggingMixin,
|
||||
ExternalMixin,
|
||||
]) {}
|
||||
|
@@ -38,17 +38,13 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
});
|
||||
mql.addListener((ev) => this._applyTheme(ev.matches));
|
||||
if (!this._themeApplied && mql.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",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +85,9 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
}
|
||||
|
||||
themeSettings = { ...this.hass.selectedTheme, dark: darkMode };
|
||||
this._updateHass({
|
||||
themes: { ...this.hass.themes!, theme: themeName },
|
||||
});
|
||||
|
||||
applyThemesOnElement(
|
||||
document.documentElement,
|
||||
@@ -132,5 +131,7 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
(themeMeta.getAttribute("default-content") as string);
|
||||
themeMeta.setAttribute("content", themeColor);
|
||||
}
|
||||
|
||||
this.hass!.auth.external?.fireMessage({ type: "theme-update" });
|
||||
}
|
||||
};
|
||||
|
@@ -195,8 +195,14 @@
|
||||
"turn_off": "Turn off",
|
||||
"media_play": "Play",
|
||||
"media_play_pause": "Play/pause",
|
||||
"media_next_track": "Next",
|
||||
"media_previous_track": "Previous",
|
||||
"media_pause": "Pause",
|
||||
"media_stop": "Stop",
|
||||
"media_next_track": "Next track",
|
||||
"media_previous_track": "Previous track",
|
||||
"media_volume_up": "Volume up",
|
||||
"media_volume_down": "Volume down",
|
||||
"media_volume_mute": "Volume mute",
|
||||
"media_volume_unmute": "Volume unmute",
|
||||
"text_to_speak": "Text to speak"
|
||||
},
|
||||
"persistent_notification": {
|
||||
@@ -291,12 +297,14 @@
|
||||
"undo": "Undo",
|
||||
"move": "Move",
|
||||
"save": "Save",
|
||||
"submit": "Submit",
|
||||
"rename": "Rename",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"not_now": "Not now",
|
||||
"skip": "Skip",
|
||||
"menu": "Menu",
|
||||
"overflow_menu": "Overflow menu",
|
||||
"help": "Help",
|
||||
"successfully_saved": "Successfully saved",
|
||||
"successfully_deleted": "Successfully deleted",
|
||||
@@ -421,6 +429,7 @@
|
||||
}
|
||||
},
|
||||
"related-filter-menu": {
|
||||
"filter": "Filter",
|
||||
"filter_by_entity": "Filter by entity",
|
||||
"filter_by_device": "Filter by device",
|
||||
"filter_by_area": "Filter by area",
|
||||
@@ -538,6 +547,13 @@
|
||||
},
|
||||
"attributes": {
|
||||
"expansion_header": "Attributes"
|
||||
},
|
||||
"qr-scanner": {
|
||||
"select_camera": "Select camera",
|
||||
"only_https_supported": "You can only use your camera to scan a QR code when using HTTPS.",
|
||||
"not_supported": "Your browser doesn't support QR scanning.",
|
||||
"manual_input": "You can scan the QR code with another QR scanner and paste the code in the input below",
|
||||
"enter_qr_code": "Enter QR code value"
|
||||
}
|
||||
},
|
||||
"dialogs": {
|
||||
@@ -678,8 +694,8 @@
|
||||
"open_cover": "Open cover",
|
||||
"close_cover": "Close cover",
|
||||
"open_tilt_cover": "Open cover tilt",
|
||||
"close_tile_cover": "Close cover tilt",
|
||||
"stop_cover": "Stop cover from moving"
|
||||
"close_tilt_cover": "Close cover tilt",
|
||||
"stop_cover": "Stop cover"
|
||||
}
|
||||
},
|
||||
"entity_registry": {
|
||||
@@ -694,6 +710,20 @@
|
||||
"icon": "Icon",
|
||||
"icon_error": "Icons should be in the format 'prefix:iconname', e.g. 'mdi:home'",
|
||||
"entity_id": "Entity ID",
|
||||
"device_class": "Show as",
|
||||
"device_classes": {
|
||||
"binary_sensor": {
|
||||
"door": "Door",
|
||||
"garage_door": "Garage door",
|
||||
"window": "Window",
|
||||
"opening": "Other"
|
||||
},
|
||||
"cover": {
|
||||
"door": "Door",
|
||||
"garage": "Garage door",
|
||||
"window": "Window"
|
||||
}
|
||||
},
|
||||
"unavailable": "This entity is unavailable.",
|
||||
"enabled_label": "Enable entity",
|
||||
"enabled_cause": "Disabled by {cause}.",
|
||||
@@ -864,8 +894,7 @@
|
||||
"key_missing": "Required key ''{key}'' is missing.",
|
||||
"key_not_expected": "Key ''{key}'' is not expected or not supported by the visual editor.",
|
||||
"key_wrong_type": "The provided value for ''{key}'' is not supported by the visual editor. We support ({type_correct}) but received ({type_wrong}).",
|
||||
"no_template_editor_support": "Templates not supported in visual editor",
|
||||
"no_state_array_support": "Multiple state values not supported in visual editor"
|
||||
"no_template_editor_support": "Templates not supported in visual editor"
|
||||
},
|
||||
"supervisor": {
|
||||
"title": "Could not load the Supervisor panel!",
|
||||
@@ -899,6 +928,7 @@
|
||||
"dismiss": "Dismiss"
|
||||
},
|
||||
"sidebar": {
|
||||
"external_app_configuration": "App Configuration",
|
||||
"sidebar_toggle": "Sidebar Toggle",
|
||||
"done": "Done",
|
||||
"hide_panel": "Hide panel",
|
||||
@@ -915,9 +945,47 @@
|
||||
},
|
||||
"config": {
|
||||
"header": "Configure Home Assistant",
|
||||
"advanced_mode": {
|
||||
"hint_enable": "Missing config options? Enable advanced mode on",
|
||||
"link_profile_page": "your profile page"
|
||||
"dashboard": {
|
||||
"devices": {
|
||||
"title": "Devices & Services",
|
||||
"description": "Integrations, devices, entities and areas"
|
||||
},
|
||||
"automations": {
|
||||
"title": "Automations & Scenes",
|
||||
"description": "Manage automations, scenes, scripts and helpers"
|
||||
},
|
||||
"blueprints": {
|
||||
"title": "Blueprints",
|
||||
"description": "Pre-made automations and scripts by the community"
|
||||
},
|
||||
"supervisor": {
|
||||
"title": "Add-ons, Backups & Supervisor",
|
||||
"description": "Create backups, check logs or reboot your system"
|
||||
},
|
||||
"dashboards": {
|
||||
"title": "Dashboards",
|
||||
"description": "Create customized sets of cards to control your home"
|
||||
},
|
||||
"energy": {
|
||||
"title": "Energy",
|
||||
"description": "Monitor your energy production and consumption"
|
||||
},
|
||||
"tags": {
|
||||
"title": "Tags",
|
||||
"description": "Trigger automations when a NFC tag, QR code, etc. is scanned"
|
||||
},
|
||||
"people": {
|
||||
"title": "People & Zones",
|
||||
"description": "Manage the people and zones that Home Assistant tracks"
|
||||
},
|
||||
"companion": {
|
||||
"title": "Companion App",
|
||||
"description": "Location and notifications"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"description": "Basic settings, server controls, logs and info"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
"editor": {
|
||||
@@ -1095,7 +1163,7 @@
|
||||
"cost_number": "Use a static price",
|
||||
"cost_number_input": "Price per {unit}",
|
||||
"gas_usage": "Gas usage",
|
||||
"m3_or_kWh": "m³ or kWh"
|
||||
"m3_or_kWh": "ft³, m³, Wh, kWh or MWh"
|
||||
}
|
||||
},
|
||||
"device_consumption": {
|
||||
@@ -1136,19 +1204,19 @@
|
||||
},
|
||||
"entity_unexpected_unit_energy": {
|
||||
"title": "Unexpected unit of measurement",
|
||||
"description": "The following entities do not have the expected units of measurement 'kWh' or 'Wh':"
|
||||
"description": "The following entities do not have the expected units of measurement 'Wh', 'kWh' or 'MWh':"
|
||||
},
|
||||
"entity_unexpected_unit_gas": {
|
||||
"title": "Unexpected unit of measurement",
|
||||
"description": "The following entities do not have the expected units of measurement 'kWh', 'm³' or 'ft³':"
|
||||
"description": "The following entities do not have the expected units of measurement 'Wh', 'kWh' or 'MWh' for an energy sensor or 'm³' or 'ft³' for a gas sensor:"
|
||||
},
|
||||
"entity_unexpected_unit_energy_price": {
|
||||
"title": "Unexpected unit of measurement",
|
||||
"description": "The following entities do not have the expected units of measurement ''{currency}/kWh'' or ''{currency}/Wh'':"
|
||||
"description": "The following entities do not have the expected units of measurement ''{currency}/kWh'', ''{currency}/Wh'' or ''{currency}/MWh'':"
|
||||
},
|
||||
"entity_unexpected_unit_gas_price": {
|
||||
"title": "Unexpected unit of measurement",
|
||||
"description": "The following entities do not have the expected units of measurement ''{currency}/kWh'', ''{currency}/Wh'', ''{currency}/m³'' or ''{currency}/ft³'':"
|
||||
"description": "The following entities do not have the expected units of measurement ''{currency}/kWh'', ''{currency}/Wh'', ''{currency}/MWh'', ''{currency}/m³'' or ''{currency}/ft³'':"
|
||||
},
|
||||
"entity_unexpected_state_class": {
|
||||
"title": "Unexpected state class",
|
||||
@@ -1500,7 +1568,7 @@
|
||||
"extra_fields": {
|
||||
"above": "Above",
|
||||
"below": "Below",
|
||||
"for": "Duration",
|
||||
"for": "Duration (optional)",
|
||||
"zone": "[%key:ui::panel::config::automation::editor::triggers::type::zone::label%]"
|
||||
}
|
||||
},
|
||||
@@ -1523,9 +1591,9 @@
|
||||
"state": {
|
||||
"label": "State",
|
||||
"attribute": "Attribute (optional)",
|
||||
"from": "From",
|
||||
"for": "For",
|
||||
"to": "To"
|
||||
"from": "From (optional)",
|
||||
"for": "For (optional)",
|
||||
"to": "To (optional)"
|
||||
},
|
||||
"homeassistant": {
|
||||
"label": "Home Assistant",
|
||||
@@ -2283,6 +2351,7 @@
|
||||
"update": "Update",
|
||||
"confirm_delete_user": "Are you sure you want to delete the user account for {name}? You can still track the user, but the person will no longer be able to login.",
|
||||
"admin": "[%key:ui::panel::config::users::editor::admin%]",
|
||||
"local_only": "[%key:ui::panel::config::users::editor::local_only%]",
|
||||
"allow_login": "Allow person to login"
|
||||
}
|
||||
},
|
||||
@@ -2375,6 +2444,7 @@
|
||||
"manuf": "by {manufacturer}",
|
||||
"via": "Connected via",
|
||||
"firmware": "Firmware: {version}",
|
||||
"hardware": "Hardware: {version}",
|
||||
"unnamed_entry": "Unnamed entry",
|
||||
"unknown_via_device": "Unknown device",
|
||||
"area": "In {area}",
|
||||
@@ -2442,7 +2512,8 @@
|
||||
"group": "Group",
|
||||
"system": "System generated",
|
||||
"is_active": "Active",
|
||||
"is_owner": "Owner"
|
||||
"is_owner": "Owner",
|
||||
"local": "Local only"
|
||||
},
|
||||
"add_user": "Add user"
|
||||
},
|
||||
@@ -2462,6 +2533,7 @@
|
||||
"admin": "Administrator",
|
||||
"group": "Group",
|
||||
"active": "Active",
|
||||
"local_only": "Can only log in from the local network",
|
||||
"system_generated": "System generated",
|
||||
"system_generated_users_not_removable": "Unable to remove system generated users.",
|
||||
"system_generated_users_not_editable": "Unable to update system generated users.",
|
||||
@@ -2474,6 +2546,7 @@
|
||||
"password": "Password",
|
||||
"password_confirm": "Confirm Password",
|
||||
"password_not_match": "Passwords don't match",
|
||||
"local_only": "Local only",
|
||||
"create": "Create"
|
||||
}
|
||||
},
|
||||
@@ -2796,7 +2869,7 @@
|
||||
"node_id": "Device ID",
|
||||
"home_id": "Home ID",
|
||||
"source": "Source",
|
||||
"close": "Close",
|
||||
"back": "Back",
|
||||
"add_node": "Add device",
|
||||
"remove_node": "Remove device",
|
||||
"reconfigure_server": "Re-configure Server",
|
||||
@@ -2808,8 +2881,11 @@
|
||||
"driver_version": "Driver Version",
|
||||
"server_version": "Server Version",
|
||||
"home_id": "Home ID",
|
||||
"nodes_ready": "Devices ready",
|
||||
"dump_debug": "Download a dump of your network to help diagnose issues",
|
||||
"server_url": "Server URL",
|
||||
"devices": "{count} {count, plural,\n one {device}\n other {devices}\n}",
|
||||
"provisioned_devices": "Provisioned devices",
|
||||
"not_ready": "{count} not ready",
|
||||
"dump_debug": "Download data",
|
||||
"dump_dead_nodes_title": "Some of your devices are dead",
|
||||
"dump_dead_nodes_text": "Some of your devices didn't respond and are assumed dead. These will not be fully exported.",
|
||||
"dump_not_ready_title": "Not all devices are ready yet",
|
||||
@@ -2862,8 +2938,6 @@
|
||||
"qr_code": "QR Code",
|
||||
"qr_code_paragraph": "If your device supports SmartStart you can scan the QR code for easy pairing.",
|
||||
"scan_qr_code": "Scan QR code",
|
||||
"enter_qr_code": "Enter QR code value",
|
||||
"select_camera": "Select camera",
|
||||
"inclusion_failed": "The device could not be added.",
|
||||
"check_logs": "Please check the logs for more information.",
|
||||
"inclusion_finished": "The device has been added.",
|
||||
@@ -2872,6 +2946,15 @@
|
||||
"interview_started": "The device is being interviewed. This may take some time.",
|
||||
"interview_failed": "The device interview failed. Additional information may be available in the logs."
|
||||
},
|
||||
"provisioned": {
|
||||
"dsk": "DSK",
|
||||
"security_classes": "Security classes",
|
||||
"unprovison": "Unprovison",
|
||||
"included": "Included",
|
||||
"not_included": "Not Included",
|
||||
"confirm_unprovision_title": "Are you sure you want to unprovision the device?",
|
||||
"confirm_unprovision_text": "If you unprovision the device it will not be added to Home Assistant when it is powered on. If it is already added to Home Assistant, removing the provisioned device will not remove it from Home Assistant."
|
||||
},
|
||||
"security_classes": {
|
||||
"None": {
|
||||
"title": "None"
|
||||
@@ -2948,7 +3031,8 @@
|
||||
"title": "Z-Wave JS Logs",
|
||||
"log_level": "Log Level",
|
||||
"subscribed_to_logs": "Subscribed to Z-Wave JS Log Messages…",
|
||||
"log_level_changed": "Log Level changed to: {level}"
|
||||
"log_level_changed": "Log Level changed to: {level}",
|
||||
"download_logs": "Download logs"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -3223,11 +3307,12 @@
|
||||
"alarm-panel": {
|
||||
"name": "Alarm Panel",
|
||||
"available_states": "Available States",
|
||||
"description": "The Alarm Panel card allows you to Arm and Disarm your alarm control panel integrations."
|
||||
"description": "The Alarm Panel card allows you to arm and disarm your alarm control panel integrations."
|
||||
},
|
||||
"area": {
|
||||
"name": "Area",
|
||||
"description": "The Area card automatically displays entities of a specific area."
|
||||
"description": "The Area card automatically displays entities of a specific area.",
|
||||
"show_camera": "Show camera feed instead of area picture"
|
||||
},
|
||||
"calendar": {
|
||||
"name": "Calendar",
|
||||
@@ -4192,7 +4277,8 @@
|
||||
"create_backup": "Create backup before updating",
|
||||
"description": "You have {version} installed. Click update to update to version {newest_version}",
|
||||
"updating": "Updating {name} to version {version}",
|
||||
"creating_backup": "Creating backup of {name}"
|
||||
"creating_backup": "Creating backup of {name}",
|
||||
"no_update": "No update available for {name}"
|
||||
},
|
||||
"confirm": {
|
||||
"restart": {
|
||||
|
@@ -84,9 +84,12 @@ export interface CurrentUser {
|
||||
}
|
||||
|
||||
// Currently selected theme and its settings. These are the values stored in local storage.
|
||||
// Note: These values are not meant to be used at runtime to check whether dark mode is active
|
||||
// or which theme name to use, as this interface represents the config data for the theme picker.
|
||||
// The actually active dark mode and theme name can be read from hass.themes.
|
||||
export interface ThemeSettings {
|
||||
theme: string;
|
||||
// Radio box selection for theme picker. Do not use in cards as
|
||||
// Radio box selection for theme picker. Do not use in Lovelace rendering as
|
||||
// it can be undefined == auto.
|
||||
// Property hass.themes.darkMode carries effective current mode.
|
||||
dark?: boolean;
|
||||
|
@@ -39,6 +39,7 @@ const hassAttributeUtil = {
|
||||
"vibration",
|
||||
"window",
|
||||
],
|
||||
button: ["restart", "update"],
|
||||
cover: [
|
||||
"awning",
|
||||
"blind",
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user