Compare commits

...

19 Commits

Author SHA1 Message Date
Paulus Schoutsen
94215dc50b dev tools dialog 2021-12-05 17:40:15 -08:00
Philip Allgaier
8f5751d5bb Use correct label in area card editor (#10799) 2021-12-05 12:09:06 -06:00
Philip Allgaier
4095450476 Add switch to input row domains to fix mobile focus issue (#10792) 2021-12-04 11:43:14 +01:00
Bram Kragten
e61f587c51 Bumped version to 20211203.0 2021-12-03 18:07:07 +01:00
Bram Kragten
d43d19190e Fix entity marker (#10787) 2021-12-03 17:04:50 +00:00
Bram Kragten
a283acaabf safari doesnt support overflow-wrap: anywhere 2021-12-03 18:04:03 +01:00
Philip Allgaier
ea18fc0078 Ensure we always have an active theme name (fixes dark theme issues) (#10780)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-12-03 17:02:54 +00:00
Bram Kragten
1df11e9bf1 Use groupBy (#10786) 2021-12-03 08:42:23 -08:00
Bram Kragten
c71b2e6b9d Add provisioned device overview to zwave js (#10785) 2021-12-03 08:34:26 -08:00
Bram Kragten
db4aa05bf4 Differentiate between assigned and targeting scene/automations/script (#10781) 2021-12-03 08:21:26 -08:00
Bram Kragten
a54a2a54f8 Add support for local only users (#10784)
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
2021-12-03 16:34:34 +01:00
Philip Allgaier
0bcb4d0e09 Restore flex alignment for select and input-select rows (#10783) 2021-12-03 16:19:00 +01:00
Bram Kragten
95dbc811d3 Allow overriding device class (#10777) 2021-12-03 16:07:49 +01:00
Philip Allgaier
e28a11964e Use correct styling for cloud certificate dialog (#10782) 2021-12-03 15:08:49 +01:00
Bram Kragten
46a9e36516 Guard for non numeric states (#10775)
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
2021-12-03 12:53:50 +01:00
Paulus Schoutsen
e99f20c4f3 Tweak ZJS dashboard (#10772) 2021-12-03 10:51:50 +01:00
Joakim Sørensen
2100603cdc Remove handling of the supervisor panel from the sidebar (#10773) 2021-12-03 10:49:42 +01:00
Joakim Sørensen
da4942aca3 Add default icons for button entities (#10774) 2021-12-03 09:11:29 +01:00
Paulus Schoutsen
7c78fb314e Show add devices fab on devices page for ZJS (#10771) 2021-12-03 08:42:39 +01:00
44 changed files with 992 additions and 356 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -221,6 +221,7 @@ export const DOMAINS_INPUT_ROW = [
"scene",
"script",
"select",
"switch",
];
/** Domains that should have the history hidden in the more info dialog. */

View File

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

View File

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

View File

@@ -121,6 +121,7 @@ class HaAlert extends LitElement {
}
.main-content {
overflow-wrap: anywhere;
word-break: break-word;
margin-left: 8px;
margin-right: 0;
}

View File

@@ -8,7 +8,6 @@ import {
mdiCog,
mdiFormatListBulletedType,
mdiHammer,
mdiHomeAssistant,
mdiLightningBolt,
mdiMenu,
mdiMenuOpen,
@@ -53,7 +52,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 +62,6 @@ const SORT_VALUE_URL_PATHS = {
logbook: 3,
history: 4,
"developer-tools": 9,
hassio: 10,
config: 11,
};
@@ -72,7 +70,6 @@ const PANEL_ICONS = {
config: mdiCog,
"developer-tools": mdiHammer,
energy: mdiLightningBolt,
hassio: mdiHomeAssistant,
history: mdiChartBox,
logbook: mdiFormatListBulletedType,
lovelace: mdiViewDashboard,
@@ -340,10 +337,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;
@@ -393,11 +388,7 @@ class HaSidebar extends LitElement {
return html`
<a
aria-role="option"
href=${`/${
urlPath === "hassio"
? "config/dashboard/?focusedPath=hassio"
: urlPath
}`}
href=${`/${urlPath}`}
data-panel=${urlPath}
tabindex="-1"
@mouseenter=${this._itemMouseEnter}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -205,6 +205,16 @@ export const enum NodeStatus {
Alive,
}
export interface ZwaveJSProvisioningEntry {
/** The device specific key (DSK) in the form aaaaa-bbbbb-ccccc-ddddd-eeeee-fffff-11111-22222 */
dsk: string;
securityClasses: SecurityClass[];
/**
* Additional properties to be stored in this provisioning entry, e.g. the device ID from a scanned QR code
*/
[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<any> =>
hass.callWS({
type: "zwave_js/get_provisioning_entries",
entry_id,
});
export const subscribeAddZwaveNode = (
hass: HomeAssistant,
entry_id: string,
@@ -350,6 +369,19 @@ export const provisionZwaveSmartStartNode = (
planned_provisioning_entry,
});
export const unprovisionZwaveSmartStartNode = (
hass: HomeAssistant,
entry_id: string,
dsk?: string,
node_id?: number
): Promise<QRProvisioningInformation> =>
hass.callWS({
type: "zwave_js/unprovision_smart_start_node",
entry_id,
dsk,
node_id,
});
export const fetchZwaveNodeStatus = (
hass: HomeAssistant,
entry_id: string,

View File

@@ -0,0 +1,138 @@
import { mdiClose } from "@mdi/js";
import "@polymer/paper-tabs";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant, Route } from "../../types";
import "../../components/ha-dialog";
import "../../components/ha-tabs";
import "../../components/ha-icon-button";
import "../../panels/developer-tools/developer-tools-router";
import type { HaDialog } from "../../components/ha-dialog";
import "@material/mwc-button/mwc-button";
@customElement("ha-developer-tools-dialog")
export class HaDeveloperToolsDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _opened = false;
@state() private _route: Route = {
prefix: "/developer-tools",
path: "/state",
};
@query("ha-dialog", true) private _dialog!: HaDialog;
public async showDialog(): Promise<void> {
this._opened = true;
}
public async closeDialog(): Promise<void> {
this._opened = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._opened) {
return html``;
}
return html`
<ha-dialog open @closed=${this.closeDialog}>
<div class="header">
<ha-tabs
scrollable
attr-for-selected="page-name"
.selected=${this._route.path.substr(1)}
@iron-activate=${this.handlePageSelected}
>
<paper-tab page-name="state">
${this.hass.localize(
"ui.panel.developer-tools.tabs.states.title"
)}
</paper-tab>
<paper-tab page-name="service">
${this.hass.localize(
"ui.panel.developer-tools.tabs.services.title"
)}
</paper-tab>
<paper-tab page-name="template">
${this.hass.localize(
"ui.panel.developer-tools.tabs.templates.title"
)}
</paper-tab>
<paper-tab page-name="event">
${this.hass.localize(
"ui.panel.developer-tools.tabs.events.title"
)}
</paper-tab>
<paper-tab page-name="statistics">
${this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.title"
)}
</paper-tab>
</ha-tabs>
<ha-icon-button
.path=${mdiClose}
@click=${this.closeDialog}
></ha-icon-button>
</div>
<developer-tools-router
.route=${this._route}
.narrow=${document.body.clientWidth < 600}
.hass=${this.hass}
></developer-tools-router>
</ha-dialog>
`;
}
protected firstUpdated(changedProps: PropertyValues) {
super.updated(changedProps);
this.hass.loadBackendTranslation("title");
this.hass.loadFragmentTranslation("developer-tools");
}
private handlePageSelected(ev) {
const newPage = ev.detail.item.getAttribute("page-name");
if (newPage !== this._route.path.substr(1)) {
this._route = {
prefix: "/developer-tools",
path: `/${newPage}`,
};
} else {
// scrollTo(0, 0);
}
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-min-width: 100vw;
--mdc-dialog-min-height: 100vh;
}
.header {
display: flex;
}
ha-tabs {
flex: 1;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-developer-tools-dialog": HaDeveloperToolsDialog;
}
}

View File

@@ -0,0 +1,12 @@
import { fireEvent } from "../../common/dom/fire_event";
export const loadDeveloperToolDialog = () =>
import("./ha-developer-tools-dialog");
export const showDeveloperToolDialog = (element: HTMLElement): void => {
fireEvent(element, "show-dialog", {
dialogTag: "ha-developer-tools-dialog",
dialogImport: loadDeveloperToolDialog,
dialogParams: {},
});
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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";
@@ -136,7 +135,6 @@ class HaConfigDashboard extends LitElement {
.narrow=${this.narrow}
.showAdvanced=${this.showAdvanced}
.pages=${configSections.dashboard}
.focusedPath=${extractSearchParam("focusedPath")}
></ha-config-navigation>
</ha-card>`}
</ha-config-section>

View File

@@ -1,13 +1,6 @@
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";
@@ -26,21 +19,6 @@ 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;
}
}
}
protected render(): TemplateResult {
return html`
${this.pages.map((page) =>

View File

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

View File

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

View File

@@ -72,9 +72,9 @@ export const configSections: { [name: string]: PageNavigation[] } = {
},
{
path: "/hassio",
name: "Add-ons & Backups",
name: "Add-ons & Backups (Supervisor)",
description: "Create backups, check logs or reboot your system",
iconPath: mdiPuzzle,
iconPath: mdiHomeAssistant,
iconColor: "#4084CD",
component: "hassio",
},

View File

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

View File

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

View File

@@ -0,0 +1,128 @@
import { 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 => ({
dsk: {
title: this.hass.localize("ui.panel.config.zwave_js.provisioned.dsk"),
sortable: true,
filterable: true,
grows: true,
},
securityClasses: {
title: this.hass.localize(
"ui.panel.config.zwave_js.provisioned.security_classes"
),
width: "15%",
hidden: narrow,
filterable: true,
sortable: true,
template: (securityClasses: SecurityClass[]) =>
securityClasses
.map((secClass) =>
this.hass.localize(
`ui.panel.config.zwave_js.security_classes.${SecurityClass[secClass]}`
)
)
.join(", "),
},
unprovision: {
title: this.hass.localize(
"ui.panel.config.zwave_js.provisioned.unprovison"
),
type: "icon-button",
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 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,
ev.currentTarget.provisioningEntry.dsk
);
};
}
declare global {
interface HTMLElementTagNameMap {
"zwave_js-provisioned": ZWaveJSProvisioned;
}
}

View File

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

View File

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

View File

@@ -30,6 +30,8 @@ class DialogUserDetail extends LitElement {
@state() private _isAdmin?: boolean;
@state() private _localOnly?: boolean;
@state() private _isActive?: boolean;
@state() private _error?: string;
@@ -43,6 +45,7 @@ class DialogUserDetail extends LitElement {
this._error = undefined;
this._name = params.entry.name || "";
this._isAdmin = params.entry.group_ids.includes(SYSTEM_GROUP_ID_ADMIN);
this._localOnly = params.entry.local_only;
this._isActive = params.entry.is_active;
await this.updateComplete;
}
@@ -95,6 +98,20 @@ 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
.checked=${this._localOnly}
@change=${this._localOnlyChanged}
>
</ha-switch>
</ha-formfield>
</div>
<div class="row">
<ha-formfield
.label=${this.hass.localize(
@@ -198,11 +215,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 +236,7 @@ class DialogUserDetail extends LitElement {
group_ids: [
this._isAdmin ? SYSTEM_GROUP_ID_ADMIN : SYSTEM_GROUP_ID_USER,
],
local_only: this._localOnly,
});
this._close();
} catch (err: any) {

View File

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

View File

@@ -188,7 +188,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 +203,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}`;

View File

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

View File

@@ -59,7 +59,9 @@ 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>
<paper-input

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -1,5 +1,6 @@
import type { PropertyValues } from "lit";
import tinykeys from "tinykeys";
import { showDeveloperToolDialog } from "../dialogs/developert-tools/show-dialog-developer-tools";
import {
QuickBarParams,
showQuickBar,
@@ -32,6 +33,7 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
tinykeys(window, {
e: (ev) => this._showQuickBar(ev),
c: (ev) => this._showQuickBar(ev, true),
d: () => showDeveloperToolDialog(this),
});
}

View File

@@ -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,

View File

@@ -694,6 +694,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}.",
@@ -2283,6 +2297,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"
}
},
@@ -2442,7 +2457,8 @@
"group": "Group",
"system": "System generated",
"is_active": "Active",
"is_owner": "Owner"
"is_owner": "Owner",
"local": "Local only"
},
"add_user": "Add user"
},
@@ -2462,6 +2478,7 @@
"admin": "Administrator",
"group": "Group",
"active": "Active",
"local_only": "Can only login 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 +2491,7 @@
"password": "Password",
"password_confirm": "Confirm Password",
"password_not_match": "Passwords don't match",
"local_only": "Local only",
"create": "Create"
}
},
@@ -2808,8 +2826,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",
@@ -2872,6 +2893,13 @@
"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",
"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"
@@ -3223,7 +3251,7 @@
"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",

View File

@@ -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;

View File

@@ -39,6 +39,7 @@ const hassAttributeUtil = {
"vibration",
"window",
],
button: ["restart", "update"],
cover: [
"awning",
"blind",