Compare commits

..

7 Commits

Author SHA1 Message Date
Simon Lamon
b70d1492b3 Remove duplicated example 2025-11-03 18:45:59 +00:00
Simon Lamon
fba38cc6ec Merge branch 'dev' into dropdown 2025-11-03 19:32:12 +01:00
Simon Lamon
a8b0291a9a Documentation 2025-11-03 18:27:47 +00:00
Simon Lamon
a240bd2634 Update src/resources/theme/color/wa.globals.ts
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-11-03 18:56:17 +01:00
Simon Lamon
9d4bf30753 Merge branch 'dev' into dropdown 2025-10-11 11:55:22 +02:00
Simon Lamon
10b99433ea Shadow and fixes 2025-10-11 09:52:22 +00:00
Simon Lamon
f0d4c9cb72 Introduce ha-dropdown 2025-10-08 17:24:10 +00:00
65 changed files with 930 additions and 1592 deletions

View File

@@ -18,16 +18,16 @@ module.exports.sourceMapURL = () => {
module.exports.ignorePackages = () => [];
// Files from NPM packages that we should replace with empty file
module.exports.emptyPackages = ({ isHassioBuild, isLandingPageBuild }) =>
module.exports.emptyPackages = ({ isHassioBuild }) =>
[
require.resolve("@vaadin/vaadin-material-styles/typography.js"),
require.resolve("@vaadin/vaadin-material-styles/font-icons.js"),
// Icons in supervisor conflict with icons in HA so we don't load.
(isHassioBuild || isLandingPageBuild) &&
isHassioBuild &&
require.resolve(
path.resolve(paths.root_dir, "src/components/ha-icon.ts")
),
(isHassioBuild || isLandingPageBuild) &&
isHassioBuild &&
require.resolve(
path.resolve(paths.root_dir, "src/components/ha-icon-picker.ts")
),
@@ -337,7 +337,6 @@ module.exports.config = {
publicPath: publicPath(latestBuild),
isProdBuild,
latestBuild,
isLandingPageBuild: true,
};
},
};

View File

@@ -41,7 +41,6 @@ const createRspackConfig = ({
isStatsBuild,
isTestBuild,
isHassioBuild,
isLandingPageBuild,
dontHash,
}) => {
if (!dontHash) {
@@ -169,9 +168,7 @@ const createRspackConfig = ({
},
}),
new rspack.NormalModuleReplacementPlugin(
new RegExp(
bundle.emptyPackages({ isHassioBuild, isLandingPageBuild }).join("|")
),
new RegExp(bundle.emptyPackages({ isHassioBuild }).join("|")),
path.resolve(paths.root_dir, "src/util/empty.js")
),
!isProdBuild && new LogStartCompilePlugin(),

View File

@@ -0,0 +1,55 @@
---
title: Dropdown
---
# Dropdown `<ha-dropdown>`
## Implementation
A compact, accessible dropdown menu for choosing actions or settings. `ha-dropdown` supports composed menu items (`<ha-dropdown-item>`) for icons, submenus, checkboxes, disabled entries, and destructive variants. Use composition with `slot="trigger"` to control the trigger button and use `<ha-dropdown-item>` for rich item content.
### Example usage (composition)
```html
<ha-dropdown open>
<ha-button slot="trigger" with-caret>Dropdown</ha-button>
<ha-dropdown-item>
<ha-svg-icon .path="${mdiContentCut}" slot="icon"></ha-svg-icon>
Cut
</ha-dropdown-item>
<ha-dropdown-item>
<ha-svg-icon .path="${mdiContentCopy}" slot="icon"></ha-svg-icon>
Copy
</ha-dropdown-item>
<ha-dropdown-item disabled>
<ha-svg-icon .path="${mdiContentPaste}" slot="icon"></ha-svg-icon>
Paste
</ha-dropdown-item>
<ha-dropdown-item>
Show images
<ha-dropdown-item slot="submenu" value="show-all-images"
>Show all images</ha-dropdown-item
>
<ha-dropdown-item slot="submenu" value="show-thumbnails"
>Show thumbnails</ha-dropdown-item
>
</ha-dropdown-item>
<ha-dropdown-item type="checkbox" checked>Emoji shortcuts</ha-dropdown-item>
<ha-dropdown-item type="checkbox" checked>Word wrap</ha-dropdown-item>
<ha-dropdown-item variant="danger">
<ha-svg-icon .path="${mdiDelete}" slot="icon"></ha-svg-icon>
Delete
</ha-dropdown-item>
</ha-dropdown>
```
### API
This component is based on the webawesome dropdown component.
Check the [webawesome documentation](https://webawesome.com/docs/components/dropdown/) for more details.

View File

@@ -0,0 +1,133 @@
import {
mdiContentCopy,
mdiContentCut,
mdiContentPaste,
mdiDelete,
} from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement } from "lit/decorators";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-dropdown-item";
import "@home-assistant/webawesome/dist/components/icon/icon";
import "@home-assistant/webawesome/dist/components/button/button";
import "@home-assistant/webawesome/dist/components/dropdown/dropdown";
import "../../../../src/components/ha-dropdown";
import "@home-assistant/webawesome/dist/components/popup/popup";
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
import "../../../../src/components/ha-icon-button";
@customElement("demo-components-ha-dropdown")
export class DemoHaDropdown extends LitElement {
protected render(): TemplateResult {
return html`
${["light", "dark"].map(
(mode) => html`
<div class=${mode}>
<ha-card header="ha-button in ${mode}">
<div class="card-content">
<ha-dropdown open>
<ha-button slot="trigger" with-caret>Dropdown</ha-button>
<ha-dropdown-item>
<ha-svg-icon
.path=${mdiContentCut}
slot="icon"
></ha-svg-icon>
Cut
</ha-dropdown-item>
<ha-dropdown-item>
<ha-svg-icon
.path=${mdiContentCopy}
slot="icon"
></ha-svg-icon>
Copy
</ha-dropdown-item>
<ha-dropdown-item disabled>
<ha-svg-icon
.path=${mdiContentPaste}
slot="icon"
></ha-svg-icon>
Paste
</ha-dropdown-item>
<ha-dropdown-item>
Show images
<ha-dropdown-item slot="submenu" value="show-all-images"
>Show All Images</ha-dropdown-item
>
<ha-dropdown-item slot="submenu" value="show-thumbnails"
>Show Thumbnails</ha-dropdown-item
>
</ha-dropdown-item>
<ha-dropdown-item type="checkbox" checked
>Emoji Shortcuts</ha-dropdown-item
>
<ha-dropdown-item type="checkbox" checked
>Word Wrap</ha-dropdown-item
>
<ha-dropdown-item variant="danger">
<ha-svg-icon .path=${mdiDelete} slot="icon"></ha-svg-icon>
Delete
</ha-dropdown-item>
</ha-dropdown>
</div>
</ha-card>
</div>
`
)}
`;
}
firstUpdated(changedProps) {
super.firstUpdated(changedProps);
applyThemesOnElement(
this.shadowRoot!.querySelector(".dark"),
{
default_theme: "default",
default_dark_theme: "default",
themes: {},
darkMode: true,
theme: "default",
},
undefined,
undefined,
true
);
}
static styles = css`
:host {
display: flex;
justify-content: center;
}
.dark,
.light {
display: block;
background-color: var(--primary-background-color);
padding: 0 50px;
}
.button {
padding: unset;
}
ha-card {
margin: 24px auto;
}
.card-content {
display: flex;
flex-direction: column;
gap: 24px;
}
.card-content div {
display: flex;
gap: 8px;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-dropdown": DemoHaDropdown;
}
}

View File

@@ -1,25 +1,22 @@
import "@material/mwc-linear-progress";
import { mdiOpenInNew } from "@mdi/js";
import { css, html, nothing, type PropertyValues } from "lit";
import { type PropertyValues, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { extractSearchParam } from "../../src/common/url/search-params";
import "../../src/components/ha-alert";
import "../../src/components/ha-button";
import "../../src/components/ha-fade-in";
import "../../src/components/ha-spinner";
import "../../src/components/ha-svg-icon";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import "../../src/onboarding/onboarding-welcome-links";
import { onBoardingStyles } from "../../src/onboarding/styles";
import { haStyle } from "../../src/resources/styles";
import "./components/landing-page-logs";
import "../../src/onboarding/onboarding-welcome-links";
import "./components/landing-page-network";
import "./components/landing-page-logs";
import { extractSearchParam } from "../../src/common/url/search-params";
import { onBoardingStyles } from "../../src/onboarding/styles";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import { LandingPageBaseElement } from "./landing-page-base-element";
import {
getSupervisorNetworkInfo,
pingSupervisor,
type NetworkInfo,
} from "./data/supervisor";
import { LandingPageBaseElement } from "./landing-page-base-element";
export const ASSUME_CORE_START_SECONDS = 60;
const SCHEDULE_CORE_CHECK_SECONDS = 1;
@@ -97,21 +94,16 @@ class HaLandingPage extends LandingPageBaseElement {
<ha-language-picker
.value=${this.language}
.label=${""}
button-style
native-name
@value-changed=${this._languageChanged}
inline-arrow
></ha-language-picker>
<ha-button
appearance="plain"
variant="neutral"
<a
href="https://www.home-assistant.io/getting-started/onboarding/"
target="_blank"
rel="noreferrer noopener"
>${this.localize("ui.panel.page-onboarding.help")}</a
>
${this.localize("ui.panel.page-onboarding.help")}
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
</ha-button>
</div>
`;
}
@@ -226,8 +218,26 @@ class HaLandingPage extends LandingPageBaseElement {
ha-alert p {
text-align: unset;
}
.footer ha-svg-icon {
--mdc-icon-size: var(--ha-space-5);
ha-language-picker {
display: block;
width: 200px;
border-radius: var(--ha-border-radius-sm);
overflow: hidden;
--ha-select-height: 40px;
--mdc-select-fill-color: none;
--mdc-select-label-ink-color: var(--primary-text-color, #212121);
--mdc-select-ink-color: var(--primary-text-color, #212121);
--mdc-select-idle-line-color: transparent;
--mdc-select-hover-line-color: transparent;
--mdc-select-dropdown-icon-color: var(--primary-text-color, #212121);
--mdc-shape-small: 0;
}
a {
text-decoration: none;
color: var(--primary-text-color);
margin-right: 16px;
margin-inline-end: 16px;
margin-inline-start: initial;
}
ha-fade-in {
min-height: calc(100vh - 64px - 88px);

View File

@@ -111,7 +111,7 @@
"fuse.js": "7.1.0",
"google-timezones-json": "1.2.0",
"gulp-zopfli-green": "6.0.2",
"hls.js": "1.6.14",
"hls.js": "1.6.13",
"home-assistant-js-websocket": "9.5.0",
"idb-keyval": "6.2.2",
"intl-messageformat": "10.7.18",
@@ -156,7 +156,7 @@
"@lokalise/node-api": "15.3.1",
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.0.3",
"@octokit/rest": "22.0.1",
"@octokit/rest": "22.0.0",
"@rsdoctor/rspack-plugin": "1.3.7",
"@rspack/core": "1.6.0",
"@rspack/dev-server": "1.1.4",
@@ -231,7 +231,7 @@
"clean-css": "5.3.3",
"@lit/reactive-element": "2.1.1",
"@fullcalendar/daygrid": "6.1.19",
"globals": "16.5.0",
"globals": "16.4.0",
"tslib": "2.8.1",
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
},

View File

@@ -1,5 +1,4 @@
/* eslint-disable lit/prefer-static-styles */
import { mdiOpenInNew } from "@mdi/js";
import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -7,8 +6,6 @@ import punycode from "punycode";
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
import { extractSearchParamsObject } from "../common/url/search-params";
import "../components/ha-alert";
import "../components/ha-button";
import "../components/ha-svg-icon";
import type { AuthProvider, AuthUrlSearchParams } from "../data/auth";
import { fetchAuthProviders } from "../data/auth";
import { litLocalizeLiteMixin } from "../mixins/lit-localize-lite-mixin";
@@ -136,8 +133,25 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
justify-content: space-between;
align-items: center;
}
.footer ha-svg-icon {
--mdc-icon-size: var(--ha-space-5);
ha-language-picker {
width: 200px;
border-radius: var(--ha-border-radius-sm);
overflow: hidden;
--ha-select-height: 40px;
--mdc-select-fill-color: none;
--mdc-select-label-ink-color: var(--primary-text-color, #212121);
--mdc-select-ink-color: var(--primary-text-color, #212121);
--mdc-select-idle-line-color: transparent;
--mdc-select-hover-line-color: transparent;
--mdc-select-dropdown-icon-color: var(--primary-text-color, #212121);
--mdc-shape-small: 0;
}
.footer a {
text-decoration: none;
color: var(--primary-text-color);
margin-right: 16px;
margin-inline-end: 16px;
margin-inline-start: initial;
}
h1 {
font-size: var(--ha-font-size-3xl);
@@ -191,21 +205,16 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
<ha-language-picker
.value=${this.language}
.label=${""}
button-style
native-name
@value-changed=${this._languageChanged}
inline-arrow
></ha-language-picker>
<ha-button
appearance="plain"
variant="neutral"
<a
href="https://www.home-assistant.io/docs/authentication/"
target="_blank"
rel="noreferrer noopener"
>${this.localize("ui.panel.page-authorize.help")}</a
>
${this.localize("ui.panel.page-authorize.help")}
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
</ha-button>
</div>
`;
}

View File

@@ -9,9 +9,9 @@ type EntityCategory = "none" | "config" | "diagnostic";
export interface EntityFilter {
domain?: string | string[];
device_class?: string | string[];
device?: string | null | (string | null)[];
area?: string | null | (string | null)[];
floor?: string | null | (string | null)[];
device?: string | string[];
area?: string | string[];
floor?: string | string[];
label?: string | string[];
entity_category?: EntityCategory | EntityCategory[];
hidden_platform?: string | string[];
@@ -19,18 +19,6 @@ export interface EntityFilter {
export type EntityFilterFunc = (entityId: string) => boolean;
const normalizeFilterArray = <T>(
value: T | null | T[] | (T | null)[] | undefined
): Set<T | null> | undefined => {
if (value === undefined) {
return undefined;
}
if (value === null) {
return new Set([null]);
}
return new Set(ensureArray(value));
};
export const generateEntityFilter = (
hass: HomeAssistant,
filter: EntityFilter
@@ -41,9 +29,11 @@ export const generateEntityFilter = (
const deviceClasses = filter.device_class
? new Set(ensureArray(filter.device_class))
: undefined;
const floors = normalizeFilterArray(filter.floor);
const areas = normalizeFilterArray(filter.area);
const devices = normalizeFilterArray(filter.device);
const floors = filter.floor ? new Set(ensureArray(filter.floor)) : undefined;
const areas = filter.area ? new Set(ensureArray(filter.area)) : undefined;
const devices = filter.device
? new Set(ensureArray(filter.device))
: undefined;
const entityCategories = filter.entity_category
? new Set(ensureArray(filter.entity_category))
: undefined;
@@ -83,20 +73,23 @@ export const generateEntityFilter = (
}
if (floors) {
const floorId = floor?.floor_id ?? null;
if (!floors.has(floorId)) {
if (!floor || !floors.has(floor.floor_id)) {
return false;
}
}
if (areas) {
const areaId = area?.area_id ?? null;
if (!areas.has(areaId)) {
if (!area) {
return false;
}
if (!areas.has(area.area_id)) {
return false;
}
}
if (devices) {
const deviceId = device?.id ?? null;
if (!devices.has(deviceId)) {
if (!device) {
return false;
}
if (!devices.has(device.id)) {
return false;
}
}

View File

@@ -0,0 +1,33 @@
import DropdownItem from "@home-assistant/webawesome/dist/components/dropdown-item/dropdown-item";
import { css, type CSSResultGroup } from "lit";
import { customElement } from "lit/decorators";
/**
* Home Assistant dropdown item component
*
* @element ha-dropdown-item
* @extends {DropdownItem}
*
* @summary
* A stylable dropdown item component supporting Home Assistant theming, variants, and appearances based on webawesome dropdown item.
*
*/
@customElement("ha-dropdown-item")
export class HaDropdownItem extends DropdownItem {
static get styles(): CSSResultGroup {
return [
DropdownItem.styles,
css`
:host {
min-height: 40px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-dropdown-item": HaDropdownItem;
}
}

View File

@@ -0,0 +1,45 @@
import Dropdown from "@home-assistant/webawesome/dist/components/dropdown/dropdown";
import { css, type CSSResultGroup } from "lit";
import { customElement, property } from "lit/decorators";
/**
* Home Assistant dropdown component
*
* @element ha-dropdown
* @extends {Dropdown}
*
* @summary
* A stylable dropdown component supporting Home Assistant theming, variants, and appearances based on webawesome dropdown.
*
*/
@customElement("ha-dropdown")
export class HaDropdown extends Dropdown {
@property({ attribute: false }) dropdownTag = "ha-dropdown";
@property({ attribute: false }) dropdownItemTag = "ha-dropdown-item";
static get styles(): CSSResultGroup {
return [
Dropdown.styles,
css`
:host {
--wa-color-surface-border: var(--ha-color-border-normal);
--wa-color-surface-raised: var(
--card-background-color,
var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff)),
);
}
#menu {
--wa-shadow-m: 0px 4px 8px 0px var(--ha-color-shadow);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-dropdown": HaDropdown;
}
}

View File

@@ -10,6 +10,7 @@ import type { HomeAssistant } from "../types";
import "./ha-bottom-sheet";
import "./ha-button";
import "./ha-combo-box-item";
import "./ha-icon-button";
import "./ha-input-helper-text";
import "./ha-picker-combo-box";
import type {

View File

@@ -1,7 +1,6 @@
import { mdiMenuDown } from "@mdi/js";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { formatLanguageCode } from "../common/language/format_language";
@@ -9,10 +8,10 @@ import { caseInsensitiveStringCompare } from "../common/string/compare";
import type { FrontendLocaleData } from "../data/translation";
import { translationMetadata } from "../resources/translations-metadata";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import "./ha-button";
import "./ha-generic-picker";
import type { HaGenericPicker } from "./ha-generic-picker";
import "./ha-list-item";
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
import "./ha-select";
export const getLanguageOptions = (
languages: string[],
@@ -76,9 +75,6 @@ export class HaLanguagePicker extends LitElement {
@property({ attribute: "native-name", type: Boolean })
public nativeName = false;
@property({ type: Boolean, attribute: "button-style" })
public buttonStyle = false;
@property({ attribute: "no-sort", type: Boolean }) public noSort = false;
@property({ attribute: "inline-arrow", type: Boolean })
@@ -86,8 +82,6 @@ export class HaLanguagePicker extends LitElement {
@state() _defaultLanguages: string[] = [];
@query("ha-generic-picker", true) public genericPicker!: HaGenericPicker;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._computeDefaultLanguageOptions();
@@ -107,13 +101,12 @@ export class HaLanguagePicker extends LitElement {
this.hass?.locale
);
private _getLanguageName = (lang?: string) =>
this._getItems().find((language) => language.id === lang)?.primary;
private _valueRenderer = (value) =>
html`<span slot="headline"
>${this._getLanguageName(value) ?? value}</span
> `;
private _valueRenderer = (value) => {
const language = this._getItems().find(
(lang) => lang.id === value
)?.primary;
return html`<span slot="headline">${language ?? value}</span> `;
};
protected render() {
const value =
@@ -137,28 +130,10 @@ export class HaLanguagePicker extends LitElement {
.getItems=${this._getItems}
@value-changed=${this._changed}
hide-clear-icon
>
${this.buttonStyle
? html`<ha-button
slot="field"
.disabled=${this.disabled}
@click=${this._openPicker}
appearance="plain"
variant="neutral"
>
${this._getLanguageName(value)}
<ha-svg-icon slot="end" .path=${mdiMenuDown}></ha-svg-icon>
</ha-button>`
: nothing}
</ha-generic-picker>
></ha-generic-picker>
`;
}
private _openPicker(ev: Event) {
ev.stopPropagation();
this.genericPicker.open();
}
static styles = css`
ha-generic-picker {
width: 100%;

View File

@@ -87,208 +87,166 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
protected render() {
if (this.addOnTop) {
return html` ${this._renderPicker()} ${this._renderItems()} `;
return html` ${this._renderChips()} ${this._renderItems()} `;
}
return html` ${this._renderItems()} ${this._renderPicker()} `;
return html` ${this._renderItems()} ${this._renderChips()} `;
}
private _renderValueChips() {
const entityIds = this.value?.entity_id
? ensureArray(this.value.entity_id)
: [];
const deviceIds = this.value?.device_id
? ensureArray(this.value.device_id)
: [];
const areaIds = this.value?.area_id ? ensureArray(this.value.area_id) : [];
const floorIds = this.value?.floor_id
? ensureArray(this.value.floor_id)
: [];
const labelIds = this.value?.label_id
? ensureArray(this.value.label_id)
: [];
if (
!entityIds.length &&
!deviceIds.length &&
!areaIds.length &&
!floorIds.length &&
!labelIds.length
) {
return nothing;
}
return html`
<div class="mdc-chip-set items">
${floorIds.length
? floorIds.map(
(floor_id) => html`
<ha-target-picker-value-chip
.hass=${this.hass}
type="floor"
.itemId=${floor_id}
@remove-target-item=${this._handleRemove}
@expand-target-item=${this._handleExpand}
></ha-target-picker-value-chip>
`
)
: nothing}
${areaIds.length
? areaIds.map(
(area_id) => html`
<ha-target-picker-value-chip
.hass=${this.hass}
type="area"
.itemId=${area_id}
@remove-target-item=${this._handleRemove}
@expand-target-item=${this._handleExpand}
></ha-target-picker-value-chip>
`
)
: nothing}
${deviceIds.length
? deviceIds.map(
(device_id) => html`
<ha-target-picker-value-chip
.hass=${this.hass}
type="device"
.itemId=${device_id}
@remove-target-item=${this._handleRemove}
@expand-target-item=${this._handleExpand}
></ha-target-picker-value-chip>
`
)
: nothing}
${entityIds.length
? entityIds.map(
(entity_id) => html`
<ha-target-picker-value-chip
.hass=${this.hass}
type="entity"
.itemId=${entity_id}
@remove-target-item=${this._handleRemove}
@expand-target-item=${this._handleExpand}
></ha-target-picker-value-chip>
`
)
: nothing}
${labelIds.length
? labelIds.map(
(label_id) => html`
<ha-target-picker-value-chip
.hass=${this.hass}
type="label"
.itemId=${label_id}
@remove-target-item=${this._handleRemove}
@expand-target-item=${this._handleExpand}
></ha-target-picker-value-chip>
`
)
: nothing}
</div>
`;
return html`<div class="mdc-chip-set items">
${this.value?.floor_id
? ensureArray(this.value.floor_id).map(
(floor_id) => html`
<ha-target-picker-value-chip
.hass=${this.hass}
type="floor"
.itemId=${floor_id}
@remove-target-item=${this._handleRemove}
@expand-target-item=${this._handleExpand}
></ha-target-picker-value-chip>
`
)
: nothing}
${this.value?.area_id
? ensureArray(this.value.area_id).map(
(area_id) => html`
<ha-target-picker-value-chip
.hass=${this.hass}
type="area"
.itemId=${area_id}
@remove-target-item=${this._handleRemove}
@expand-target-item=${this._handleExpand}
></ha-target-picker-value-chip>
`
)
: nothing}
${this.value?.device_id
? ensureArray(this.value.device_id).map(
(device_id) => html`
<ha-target-picker-value-chip
.hass=${this.hass}
type="device"
.itemId=${device_id}
@remove-target-item=${this._handleRemove}
@expand-target-item=${this._handleExpand}
></ha-target-picker-value-chip>
`
)
: nothing}
${this.value?.entity_id
? ensureArray(this.value.entity_id).map(
(entity_id) => html`
<ha-target-picker-value-chip
.hass=${this.hass}
type="entity"
.itemId=${entity_id}
@remove-target-item=${this._handleRemove}
@expand-target-item=${this._handleExpand}
></ha-target-picker-value-chip>
`
)
: nothing}
${this.value?.label_id
? ensureArray(this.value.label_id).map(
(label_id) => html`
<ha-target-picker-value-chip
.hass=${this.hass}
type="label"
.itemId=${label_id}
@remove-target-item=${this._handleRemove}
@expand-target-item=${this._handleExpand}
></ha-target-picker-value-chip>
`
)
: nothing}
</div>`;
}
private _renderValueGroups() {
const entityIds = this.value?.entity_id
? ensureArray(this.value.entity_id)
: [];
const deviceIds = this.value?.device_id
? ensureArray(this.value.device_id)
: [];
const areaIds = this.value?.area_id ? ensureArray(this.value.area_id) : [];
const floorIds = this.value?.floor_id
? ensureArray(this.value.floor_id)
: [];
const labelIds = this.value?.label_id
? ensureArray(this.value?.label_id)
: [];
return html`<div class="item-groups">
${this.value?.entity_id
? html`
<ha-target-picker-item-group
@remove-target-item=${this._handleRemove}
type="entity"
.hass=${this.hass}
.items=${{ entity: ensureArray(this.value?.entity_id) }}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.includeDomains=${this.includeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
>
</ha-target-picker-item-group>
`
: nothing}
${this.value?.device_id
? html`
<ha-target-picker-item-group
@remove-target-item=${this._handleRemove}
type="device"
.hass=${this.hass}
.items=${{ device: ensureArray(this.value?.device_id) }}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.includeDomains=${this.includeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
>
</ha-target-picker-item-group>
`
: nothing}
${this.value?.floor_id || this.value?.area_id
? html`
<ha-target-picker-item-group
@remove-target-item=${this._handleRemove}
type="area"
.hass=${this.hass}
.items=${{
floor: ensureArray(this.value?.floor_id),
area: ensureArray(this.value?.area_id),
}}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.includeDomains=${this.includeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
>
</ha-target-picker-item-group>
`
: nothing}
${this.value?.label_id
? html`
<ha-target-picker-item-group
@remove-target-item=${this._handleRemove}
type="label"
.hass=${this.hass}
.items=${{ label: ensureArray(this.value?.label_id) }}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.includeDomains=${this.includeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
>
</ha-target-picker-item-group>
`
: nothing}
</div>`;
}
private _renderItems() {
if (
!entityIds.length &&
!deviceIds.length &&
!areaIds.length &&
!floorIds.length &&
!labelIds.length
!this.value?.floor_id &&
!this.value?.area_id &&
!this.value?.device_id &&
!this.value?.entity_id &&
!this.value?.label_id
) {
return nothing;
}
return html`
<div class="item-groups">
${entityIds.length
? html`
<ha-target-picker-item-group
@remove-target-item=${this._handleRemove}
type="entity"
.hass=${this.hass}
.items=${{ entity: entityIds }}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.includeDomains=${this.includeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
>
</ha-target-picker-item-group>
`
: nothing}
${deviceIds.length
? html`
<ha-target-picker-item-group
@remove-target-item=${this._handleRemove}
type="device"
.hass=${this.hass}
.items=${{ device: deviceIds }}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.includeDomains=${this.includeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
>
</ha-target-picker-item-group>
`
: nothing}
${floorIds.length || areaIds.length
? html`
<ha-target-picker-item-group
@remove-target-item=${this._handleRemove}
type="area"
.hass=${this.hass}
.items=${{
floor: floorIds,
area: areaIds,
}}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.includeDomains=${this.includeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
>
</ha-target-picker-item-group>
`
: nothing}
${labelIds.length
? html`
<ha-target-picker-item-group
@remove-target-item=${this._handleRemove}
type="label"
.hass=${this.hass}
.items=${{ label: labelIds }}
.deviceFilter=${this.deviceFilter}
.entityFilter=${this.entityFilter}
.includeDomains=${this.includeDomains}
.includeDeviceClasses=${this.includeDeviceClasses}
>
</ha-target-picker-item-group>
`
: nothing}
</div>
`;
}
private _renderItems() {
return html`
${this.compact ? this._renderValueChips() : this._renderValueGroups()}
`;
}
private _renderPicker() {
private _renderChips() {
return html`
<div class="add-target-wrapper">
<ha-button
@@ -389,8 +347,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
this._pickerFilter = filter;
};
private _hidePicker(ev) {
ev.stopPropagation();
private _hidePicker() {
this._open = false;
this._pickerWrapperOpen = false;

View File

@@ -9,7 +9,7 @@ export class HaTooltip extends Tooltip {
@property({ attribute: "show-delay", type: Number }) showDelay = 150;
/** The amount of time to wait before hiding the tooltip when the user mouses out.. */
@property({ attribute: "hide-delay", type: Number }) hideDelay = 150;
@property({ attribute: "hide-delay", type: Number }) hideDelay = 400;
static get styles(): CSSResultGroup {
return [

View File

@@ -6,7 +6,6 @@ import {
mdiLabel,
mdiTextureBox,
} from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
@@ -20,12 +19,9 @@ import { computeDomain } from "../../common/entity/compute_domain";
import { computeEntityName } from "../../common/entity/compute_entity_name";
import { getEntityContext } from "../../common/entity/context/get_entity_context";
import { computeRTL } from "../../common/util/compute_rtl";
import type { AreaRegistryEntry } from "../../data/area_registry";
import { getConfigEntry } from "../../data/config_entries";
import { labelsContext } from "../../data/context";
import type { DeviceRegistryEntry } from "../../data/device_registry";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
import type { FloorRegistryEntry } from "../../data/floor_registry";
import { domainToName } from "../../data/integration";
import type { LabelRegistryEntry } from "../../data/label_registry";
import {
@@ -115,10 +111,10 @@ export class HaTargetPickerItemRow extends LitElement {
}
protected render() {
const { name, context, iconPath, fallbackIconPath, stateObject, notFound } =
const { name, context, iconPath, fallbackIconPath, stateObject } =
this._itemData(this.type, this.itemId);
const showEntities = this.type !== "entity" && !notFound;
const showEntities = this.type !== "entity";
const entries = this.parentEntries || this._entries;
@@ -132,7 +128,7 @@ export class HaTargetPickerItemRow extends LitElement {
}
return html`
<ha-md-list-item type="text" class=${notFound ? "error" : ""}>
<ha-md-list-item type="text">
<div class="icon" slot="start">
${this.subEntry
? html`
@@ -152,15 +148,11 @@ export class HaTargetPickerItemRow extends LitElement {
/>`
: fallbackIconPath
? html`<ha-svg-icon .path=${fallbackIconPath}></ha-svg-icon>`
: this.type === "entity"
: stateObject
? html`
<ha-state-icon
.hass=${this.hass}
.stateObj=${stateObject ||
({
entity_id: this.itemId,
attributes: {},
} as HassEntity)}
.stateObj=${stateObject}
>
</ha-state-icon>
`
@@ -168,14 +160,8 @@ export class HaTargetPickerItemRow extends LitElement {
</div>
<div slot="headline">${name}</div>
${notFound || (context && !this.hideContext)
? html`<span slot="supporting-text"
>${notFound
? this.hass.localize(
`ui.components.target-picker.${this.type}_not_found`
)
: context}</span
>`
${context && !this.hideContext
? html`<span slot="supporting-text">${context}</span>`
: nothing}
${this._domainName && this.subEntry
? html`<span slot="supporting-text" class="domain"
@@ -488,28 +474,26 @@ export class HaTargetPickerItemRow extends LitElement {
private _itemData = memoizeOne((type: TargetType, item: string) => {
if (type === "floor") {
const floor: FloorRegistryEntry | undefined = this.hass.floors?.[item];
const floor = this.hass.floors?.[item];
return {
name: floor?.name || item,
iconPath: floor?.icon,
fallbackIconPath: floor ? floorDefaultIconPath(floor) : mdiHome,
notFound: !floor,
};
}
if (type === "area") {
const area: AreaRegistryEntry | undefined = this.hass.areas?.[item];
const area = this.hass.areas?.[item];
return {
name: area?.name || item,
context: area?.floor_id && this.hass.floors?.[area.floor_id]?.name,
context: area.floor_id && this.hass.floors?.[area.floor_id]?.name,
iconPath: area?.icon,
fallbackIconPath: mdiTextureBox,
notFound: !area,
};
}
if (type === "device") {
const device: DeviceRegistryEntry | undefined = this.hass.devices?.[item];
const device = this.hass.devices?.[item];
if (device?.primary_config_entry) {
if (device.primary_config_entry) {
this._getDeviceDomain(device.primary_config_entry);
}
@@ -517,25 +501,24 @@ export class HaTargetPickerItemRow extends LitElement {
name: device ? computeDeviceNameDisplay(device, this.hass) : item,
context: device?.area_id && this.hass.areas?.[device.area_id]?.name,
fallbackIconPath: mdiDevices,
notFound: !device,
};
}
if (type === "entity") {
this._setDomainName(computeDomain(item));
const stateObject: HassEntity | undefined = this.hass.states[item];
const entityName = stateObject
? computeEntityName(stateObject, this.hass.entities, this.hass.devices)
: item;
const { area, device } = stateObject
? getEntityContext(
stateObject,
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
)
: { area: undefined, device: undefined };
const stateObject = this.hass.states[item];
const entityName = computeEntityName(
stateObject,
this.hass.entities,
this.hass.devices
);
const { area, device } = getEntityContext(
stateObject,
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
const deviceName = device ? computeDeviceName(device) : undefined;
const areaName = area ? computeAreaName(area) : undefined;
const context = [areaName, entityName ? deviceName : undefined]
@@ -545,19 +528,15 @@ export class HaTargetPickerItemRow extends LitElement {
name: entityName || deviceName || item,
context,
stateObject,
notFound: !stateObject,
};
}
// type label
const label: LabelRegistryEntry | undefined = this._labelRegistry.find(
(lab) => lab.label_id === item
);
const label = this._labelRegistry.find((lab) => lab.label_id === item);
return {
name: label?.name || item,
iconPath: label?.icon,
fallbackIconPath: mdiLabel,
notFound: !label,
};
});
@@ -618,27 +597,17 @@ export class HaTargetPickerItemRow extends LitElement {
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
}
.error {
background: var(--ha-color-fill-warning-quiet-resting);
}
.error [slot="supporting-text"] {
color: var(--ha-color-on-warning-normal);
}
state-badge {
color: var(--ha-color-on-neutral-quiet);
}
.icon {
width: 24px;
display: flex;
}
img {
width: 24px;
height: 24px;
z-index: 1;
}
ha-icon-button {
--mdc-icon-button-size: 32px;

View File

@@ -705,7 +705,7 @@ export class HaTargetPickerSelector extends LitElement {
) as EntityComboBoxItem[];
}
if (!filterType && entities.length) {
if (!filterType) {
// show group title
items.push(
this.hass.localize("ui.components.target-picker.type.entities")
@@ -733,7 +733,7 @@ export class HaTargetPickerSelector extends LitElement {
devices = this._filterGroup("device", devices);
}
if (!filterType && devices.length) {
if (!filterType) {
// show group title
items.push(
this.hass.localize("ui.components.target-picker.type.devices")
@@ -769,7 +769,7 @@ export class HaTargetPickerSelector extends LitElement {
) as FloorComboBoxItem[];
}
if (!filterType && areasAndFloors.length) {
if (!filterType) {
// show group title
items.push(
this.hass.localize("ui.components.target-picker.type.areas")
@@ -811,7 +811,7 @@ export class HaTargetPickerSelector extends LitElement {
labels = this._filterGroup("label", labels);
}
if (!filterType && labels.length) {
if (!filterType) {
// show group title
items.push(
this.hass.localize("ui.components.target-picker.type.labels")

View File

@@ -12,7 +12,6 @@ import {
mdiChatSleep,
mdiClipboardList,
mdiClock,
mdiCodeBraces,
mdiCog,
mdiCommentAlert,
mdiCounter,
@@ -114,7 +113,6 @@ export const FALLBACK_DOMAIN_ICONS = {
text: mdiFormTextbox,
time: mdiClock,
timer: mdiTimerOutline,
template: mdiCodeBraces,
todo: mdiClipboardList,
tts: mdiSpeakerMessage,
vacuum: mdiRobotVacuum,

View File

@@ -1,152 +0,0 @@
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../components/ha-alert";
import "../../components/ha-icon";
import "../../components/ha-list-item";
import "../../components/ha-spinner";
import type {
ExternalEntityAddToActions,
ExternalEntityAddToAction,
} from "../../external_app/external_messaging";
import { showToast } from "../../util/toast";
import type { HomeAssistant } from "../../types";
@customElement("ha-more-info-add-to")
export class HaMoreInfoAddTo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entityId!: string;
@state() private _externalActions?: ExternalEntityAddToActions = {
actions: [],
};
@state() private _loading = true;
private async _loadExternalActions() {
if (this.hass.auth.external?.config.hasEntityAddTo) {
this._externalActions =
await this.hass.auth.external?.sendMessage<"entity/add_to/get_actions">(
{
type: "entity/add_to/get_actions",
payload: { entity_id: this.entityId },
}
);
}
}
private async _actionSelected(ev: CustomEvent) {
const action = (ev.currentTarget as any)
.action as ExternalEntityAddToAction;
if (!action.enabled) {
return;
}
try {
await this.hass.auth.external!.fireMessage({
type: "entity/add_to",
payload: {
entity_id: this.entityId,
app_payload: action.app_payload,
},
});
} catch (err: any) {
showToast(this, {
message: this.hass.localize(
"ui.dialogs.more_info_control.add_to.action_failed",
{
error: err.message || err,
}
),
});
}
}
protected async firstUpdated() {
await this._loadExternalActions();
this._loading = false;
}
protected render() {
if (this._loading) {
return html`
<div class="loading">
<ha-spinner></ha-spinner>
</div>
`;
}
if (!this._externalActions?.actions.length) {
return html`
<ha-alert alert-type="info">
${this.hass.localize(
"ui.dialogs.more_info_control.add_to.no_actions"
)}
</ha-alert>
`;
}
return html`
<div class="actions-list">
${this._externalActions.actions.map(
(action) => html`
<ha-list-item
graphic="icon"
.disabled=${!action.enabled}
.action=${action}
.twoline=${!!action.details}
@click=${this._actionSelected}
>
<span>${action.name}</span>
${action.details
? html`<span slot="secondary">${action.details}</span>`
: nothing}
<ha-icon slot="graphic" .icon=${action.mdi_icon}></ha-icon>
</ha-list-item>
`
)}
</div>
`;
}
static styles = css`
:host {
display: block;
padding: var(--ha-space-2) var(--ha-space-6) var(--ha-space-6)
var(--ha-space-6);
}
.loading {
display: flex;
justify-content: center;
align-items: center;
padding: var(--ha-space-8);
}
.actions-list {
display: flex;
flex-direction: column;
}
ha-list-item {
cursor: pointer;
}
ha-list-item[disabled] {
cursor: not-allowed;
opacity: 0.5;
}
ha-icon {
display: flex;
align-items: center;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-more-info-add-to": HaMoreInfoAddTo;
}
}

View File

@@ -8,7 +8,6 @@ import {
mdiPencil,
mdiPencilOff,
mdiPencilOutline,
mdiPlusBoxMultipleOutline,
mdiTransitConnectionVariant,
} from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
@@ -61,7 +60,6 @@ import {
computeShowLogBookComponent,
} from "./const";
import "./controls/more-info-default";
import "./ha-more-info-add-to";
import "./ha-more-info-history-and-logbook";
import "./ha-more-info-info";
import "./ha-more-info-settings";
@@ -75,7 +73,7 @@ export interface MoreInfoDialogParams {
data?: Record<string, any>;
}
type View = "info" | "history" | "settings" | "related" | "add_to";
type View = "info" | "history" | "settings" | "related";
interface ChildView {
viewTag: string;
@@ -196,10 +194,6 @@ export class MoreInfoDialog extends LitElement {
);
}
private _shouldShowAddEntityTo(): boolean {
return !!this.hass.auth.external?.config.hasEntityAddTo;
}
private _getDeviceId(): string | null {
const entity = this.hass.entities[this._entityId!] as
| EntityRegistryEntry
@@ -301,11 +295,6 @@ export class MoreInfoDialog extends LitElement {
this._setView("related");
}
private _goToAddEntityTo(ev) {
if (!shouldHandleRequestSelectedEvent(ev)) return;
this._setView("add_to");
}
private _breadcrumbClick(ev: Event) {
ev.stopPropagation();
this._setView("related");
@@ -532,22 +521,6 @@ export class MoreInfoDialog extends LitElement {
.path=${mdiInformationOutline}
></ha-svg-icon>
</ha-list-item>
${this._shouldShowAddEntityTo()
? html`
<ha-list-item
graphic="icon"
@request-selected=${this._goToAddEntityTo}
>
${this.hass.localize(
"ui.dialogs.more_info_control.add_entity_to"
)}
<ha-svg-icon
slot="graphic"
.path=${mdiPlusBoxMultipleOutline}
></ha-svg-icon>
</ha-list-item>
`
: nothing}
</ha-button-menu>
`
: nothing}
@@ -640,14 +613,7 @@ export class MoreInfoDialog extends LitElement {
: "entity"}
></ha-related-items>
`
: this._currView === "add_to"
? html`
<ha-more-info-add-to
.hass=${this.hass}
.entityId=${entityId}
></ha-more-info-add-to>
`
: nothing
: nothing
)}
</div>
`

View File

@@ -36,13 +36,6 @@ interface EMOutgoingMessageConfigGet extends EMMessage {
type: "config/get";
}
interface EMOutgoingMessageEntityAddToGetActions extends EMMessage {
type: "entity/add_to/get_actions";
payload: {
entity_id: string;
};
}
interface EMOutgoingMessageBarCodeScan extends EMMessage {
type: "bar_code/scan";
payload: {
@@ -82,10 +75,6 @@ interface EMOutgoingMessageWithAnswer {
request: EMOutgoingMessageConfigGet;
response: ExternalConfig;
};
"entity/add_to/get_actions": {
request: EMOutgoingMessageEntityAddToGetActions;
response: ExternalEntityAddToActions;
};
}
interface EMOutgoingMessageExoplayerPlayHLS extends EMMessage {
@@ -168,14 +157,6 @@ interface EMOutgoingMessageThreadStoreInPlatformKeychain extends EMMessage {
};
}
interface EMOutgoingMessageAddEntityTo extends EMMessage {
type: "entity/add_to";
payload: {
entity_id: string;
app_payload: string; // Opaque string received from get_actions
};
}
type EMOutgoingMessageWithoutAnswer =
| EMMessageResultError
| EMMessageResultSuccess
@@ -196,8 +177,7 @@ type EMOutgoingMessageWithoutAnswer =
| EMOutgoingMessageThemeUpdate
| EMOutgoingMessageThreadStoreInPlatformKeychain
| EMOutgoingMessageImprovScan
| EMOutgoingMessageImprovConfigureDevice
| EMOutgoingMessageAddEntityTo;
| EMOutgoingMessageImprovConfigureDevice;
export interface EMIncomingMessageRestart {
id: number;
@@ -313,31 +293,18 @@ type EMIncomingMessage =
type EMIncomingMessageHandler = (msg: EMIncomingMessageCommands) => boolean;
export interface ExternalConfig {
hasSettingsScreen?: boolean;
hasSidebar?: boolean;
canWriteTag?: boolean;
hasExoPlayer?: boolean;
canCommissionMatter?: boolean;
canImportThreadCredentials?: boolean;
canTransferThreadCredentialsToKeychain?: boolean;
hasAssist?: boolean;
hasBarCodeScanner?: number;
canSetupImprov?: boolean;
downloadFileSupported?: boolean;
appVersion?: string;
hasEntityAddTo?: boolean; // Supports "Add to" from more-info dialog, with action coming from external app
}
export interface ExternalEntityAddToAction {
enabled: boolean;
name: string; // Translated name of the action to be displayed in the UI
details?: string; // Optional translated details of the action to be displayed in the UI
mdi_icon: string; // MDI icon name to be displayed in the UI (e.g., "mdi:car")
app_payload: string; // Opaque string to be sent back when the action is selected
}
export interface ExternalEntityAddToActions {
actions: ExternalEntityAddToAction[];
hasSettingsScreen: boolean;
hasSidebar: boolean;
canWriteTag: boolean;
hasExoPlayer: boolean;
canCommissionMatter: boolean;
canImportThreadCredentials: boolean;
canTransferThreadCredentialsToKeychain: boolean;
hasAssist: boolean;
hasBarCodeScanner: number;
canSetupImprov: boolean;
downloadFileSupported: boolean;
appVersion: string;
}
export class ExternalMessaging {

View File

@@ -20,21 +20,6 @@
<meta name="color-scheme" content="dark light" />
<%= renderTemplate("_style_base.html.template") %>
<style>
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
::view-transition-group(launch-screen) {
animation-duration: var(--ha-animation-base-duration, 350ms);
animation-timing-function: ease-out;
}
::view-transition-old(launch-screen) {
animation: fade-out var(--ha-animation-base-duration, 350ms) ease-out;
}
html {
background-color: var(--primary-background-color, #fafafa);
color: var(--primary-text-color, #212121);
@@ -47,28 +32,11 @@
}
}
#ha-launch-screen {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
view-transition-name: launch-screen;
background-color: var(--primary-background-color, #fafafa);
z-index: 100;
}
@media (prefers-color-scheme: dark) {
#ha-launch-screen {
background-color: var(--primary-background-color, #111111);
}
}
#ha-launch-screen.removing {
opacity: 0;
}
#ha-launch-screen svg {
width: 112px;

View File

@@ -33,7 +33,7 @@ const COMPONENTS = {
"media-browser": () =>
import("../panels/media-browser/ha-panel-media-browser"),
light: () => import("../panels/light/ha-panel-light"),
security: () => import("../panels/security/ha-panel-security"),
safety: () => import("../panels/safety/ha-panel-safety"),
climate: () => import("../panels/climate/ha-panel-climate"),
};

View File

@@ -1,23 +1,24 @@
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { goBack } from "../../common/navigate";
import { debounce } from "../../common/util/debounce";
import { deepEqual } from "../../common/util/deep-equal";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-menu-button";
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view";
import type { LovelaceConfig } from "../../data/lovelace/config/types";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy";
import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container";
const CLIMATE_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = {
strategy: {
type: "climate",
},
const CLIMATE_LOVELACE_CONFIG: LovelaceConfig = {
views: [
{
strategy: {
type: "climate",
},
},
],
};
@customElement("ha-panel-climate")
@@ -32,119 +33,65 @@ class PanelClimate extends LitElement {
@state() private _searchParms = new URLSearchParams(window.location.search);
public firstUpdated(_changedProperties: PropertyValues): void {
super.firstUpdated(_changedProperties);
}
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
// Initial setup
if (!this.hasUpdated) {
this.hass.loadFragmentTranslation("lovelace");
this._setLovelace();
return;
}
if (!changedProps.has("hass")) {
return;
}
const oldHass = changedProps.get("hass") as this["hass"];
if (oldHass && oldHass.localize !== this.hass.localize) {
if (oldHass?.locale !== this.hass.locale) {
this._setLovelace();
return;
}
if (oldHass && this.hass) {
// If the entity registry changed, ask the user if they want to refresh the config
if (
oldHass.entities !== this.hass.entities ||
oldHass.devices !== this.hass.devices ||
oldHass.areas !== this.hass.areas ||
oldHass.floors !== this.hass.floors
) {
if (this.hass.config.state === "RUNNING") {
this._debounceRegistriesChanged();
return;
}
}
// If ha started, refresh the config
if (
this.hass.config.state === "RUNNING" &&
oldHass.config.state !== "RUNNING"
) {
this._setLovelace();
}
}
}
private _debounceRegistriesChanged = debounce(
() => this._registriesChanged(),
200
);
private _registriesChanged = async () => {
this._setLovelace();
};
private _back(ev) {
ev.stopPropagation();
goBack();
}
protected render() {
protected render(): TemplateResult {
return html`
<div class="header">
<div class="toolbar">
${
this._searchParms.has("historyBack")
? html`
<ha-icon-button-arrow-prev
@click=${this._back}
slot="navigationIcon"
></ha-icon-button-arrow-prev>
`
: html`
<ha-menu-button
slot="navigationIcon"
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`
}
${this._searchParms.has("historyBack")
? html`
<ha-icon-button-arrow-prev
@click=${this._back}
slot="navigationIcon"
></ha-icon-button-arrow-prev>
`
: html`
<ha-menu-button
slot="navigationIcon"
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`}
<div class="main-title">${this.hass.localize("panel.climate")}</div>
</div>
</div>
${
this._lovelace
? html`
<hui-view-container .hass=${this.hass}>
<hui-view
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.index=${this._viewIndex}
></hui-view
></hui-view-container>
`
: nothing
}
<hui-view-container .hass=${this.hass}>
<hui-view
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.index=${this._viewIndex}
></hui-view>
</hui-view-container>
`;
}
private async _setLovelace() {
const viewConfig = await generateLovelaceViewStrategy(
CLIMATE_LOVELACE_VIEW_CONFIG,
this.hass
);
const config = { views: [viewConfig] };
const rawConfig = { views: [CLIMATE_LOVELACE_VIEW_CONFIG] };
if (deepEqual(config, this._lovelace?.config)) {
return;
}
private _setLovelace() {
this._lovelace = {
config: config,
rawConfig: rawConfig,
config: CLIMATE_LOVELACE_CONFIG,
rawConfig: CLIMATE_LOVELACE_CONFIG,
editMode: false,
urlPath: "climate",
mode: "generated",

View File

@@ -115,24 +115,6 @@ const processAreasForClimate = (
return cards;
};
const processUnassignedEntities = (
hass: HomeAssistant,
entities: string[]
): LovelaceCardConfig[] => {
const unassignedFilter = generateEntityFilter(hass, {
area: null,
});
const unassignedEntities = entities.filter(unassignedFilter);
const areaCards: LovelaceCardConfig[] = [];
const computeTileCard = computeAreaTileCardConfig(hass, "", true);
for (const entityId of unassignedEntities) {
areaCards.push(computeTileCard(entityId));
}
return areaCards;
};
@customElement("climate-view-strategy")
export class ClimateViewStrategy extends ReactiveElement {
static async generate(
@@ -208,33 +190,10 @@ export class ClimateViewStrategy extends ReactiveElement {
}
}
// Process unassigned entities
const unassignedCards = processUnassignedEntities(hass, entities);
if (unassignedCards.length > 0) {
const section: LovelaceSectionRawConfig = {
type: "grid",
column_span: 2,
cards: [
{
type: "heading",
heading:
sections.length > 0
? hass.localize(
"ui.panel.lovelace.strategy.climate.other_devices"
)
: hass.localize("ui.panel.lovelace.strategy.climate.devices"),
},
...unassignedCards,
],
};
sections.push(section);
}
return {
type: "sections",
max_columns: 2,
sections: sections,
sections: sections || [],
};
}
}

View File

@@ -257,7 +257,7 @@ class DialogAddAutomationElement
const results = fuse.multiTermsSearch(filter);
if (results) {
return results.map((result) => result.item).filter((item) => item.name);
return results.map((result) => result.item);
}
return items;
}
@@ -294,7 +294,7 @@ class DialogAddAutomationElement
const results = fuse.multiTermsSearch(filter);
if (results) {
return results.map((result) => result.item).filter((item) => item.name);
return results.map((result) => result.item);
}
return items;
}
@@ -383,16 +383,9 @@ class DialogAddAutomationElement
generatedCollections.push({
titleKey: collection.titleKey,
groups: groups.sort((a, b) => {
// make sure device is always on top
if (a.key === "device" || a.key === "device_id") {
return -1;
}
if (b.key === "device" || b.key === "device_id") {
return 1;
}
return stringCompare(a.name, b.name, this.hass.locale.language);
}),
groups: groups.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language)
),
});
});
return generatedCollections;
@@ -685,7 +678,7 @@ class DialogAddAutomationElement
);
const typeTitle = this.hass.localize(
`ui.panel.config.automation.editor.${automationElementType}s.add`
`ui.panel.config.automation.editor.${automationElementType}s.header`
);
const tabButtons = [
@@ -902,9 +895,7 @@ class DialogAddAutomationElement
return html`
<div class="items-title ${this._itemsScrolled ? "scrolled" : ""}">
${this._tab === "blocks" && !this._filter
? this.hass.localize("ui.panel.config.automation.editor.blocks")
: title}
${title}
</div>
<ha-md-list
dialogInitialFocus=${ifDefined(this._fullScreen ? "" : undefined)}

View File

@@ -9,7 +9,6 @@ import "../../../../components/ha-dialog";
import "../../../../components/ha-button";
import "../../../../components/ha-formfield";
import "../../../../components/ha-radio";
import "../../../../components/ha-markdown";
import type { HaRadio } from "../../../../components/ha-radio";
import type {
FlowFromGridSourceEnergyPreference,
@@ -20,7 +19,11 @@ import {
emptyFlowToGridSourceEnergyPreference,
energyStatisticHelpUrl,
} from "../../../../data/energy";
import { isExternalStatistic } from "../../../../data/recorder";
import {
getDisplayUnit,
getStatisticMetadata,
isExternalStatistic,
} from "../../../../data/recorder";
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../resources/styles";
@@ -44,6 +47,8 @@ export class DialogEnergyGridFlowSettings
@state() private _costs?: "no-costs" | "number" | "entity" | "statistic";
@state() private _pickedDisplayUnit?: string | null;
@state() private _energy_units?: string[];
@state() private _error?: string;
@@ -76,6 +81,11 @@ export class DialogEnergyGridFlowSettings
: "stat_energy_to"
];
this._pickedDisplayUnit = getDisplayUnit(
this.hass,
initialSourceId,
params.metadata
);
this._energy_units = (
await getSensorDeviceClassConvertibleUnits(this.hass, "energy")
).units;
@@ -93,6 +103,7 @@ export class DialogEnergyGridFlowSettings
public closeDialog() {
this._params = undefined;
this._source = undefined;
this._pickedDisplayUnit = undefined;
this._error = undefined;
this._excludeList = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
@@ -106,6 +117,10 @@ export class DialogEnergyGridFlowSettings
const pickableUnit = this._energy_units?.join(", ") || "";
const unitPriceSensor = this._pickedDisplayUnit
? `${this.hass.config.currency}/${this._pickedDisplayUnit}`
: undefined;
const unitPriceFixed = `${this.hass.config.currency}/kWh`;
const externalSource =
@@ -231,15 +246,9 @@ export class DialogEnergyGridFlowSettings
.hass=${this.hass}
include-domains='["sensor", "input_number"]'
.value=${this._source.entity_energy_price}
.label=${this.hass.localize(
.label=${`${this.hass.localize(
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.cost_entity_input`
)}
.helper=${html`<ha-markdown
.content=${this.hass.localize(
"ui.panel.config.energy.grid.flow_dialog.cost_entity_helper",
{ currency: this.hass.config.currency }
)}
></ha-markdown>`}
)} ${unitPriceSensor ? ` (${unitPriceSensor})` : ""}`}
@value-changed=${this._priceEntityChanged}
></ha-entity-picker>`
: ""}
@@ -332,6 +341,16 @@ export class DialogEnergyGridFlowSettings
}
private async _statisticChanged(ev: CustomEvent<{ value: string }>) {
if (ev.detail.value) {
const metadata = await getStatisticMetadata(this.hass, [ev.detail.value]);
this._pickedDisplayUnit = getDisplayUnit(
this.hass,
ev.detail.value,
metadata[0]
);
} else {
this._pickedDisplayUnit = undefined;
}
this._source = {
...this._source!,
[this._params!.direction === "from"

View File

@@ -4,7 +4,6 @@ import { customElement, property, query, state } from "lit/decorators";
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
import { dynamicElement } from "../../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-button";
import type { ExtEntityRegistryEntry } from "../../../../../data/entity_registry";
import { removeEntityRegistryEntry } from "../../../../../data/entity_registry";
import { HELPERS_CRUD } from "../../../../../data/helpers_crud";
@@ -23,6 +22,7 @@ import "../../../helpers/forms/ha-schedule-form";
import "../../../helpers/forms/ha-timer-form";
import "../../../voice-assistants/entity-voice-settings";
import "../../entity-registry-settings-editor";
import "../../../../../components/ha-button";
import type { EntityRegistrySettingsEditor } from "../../entity-registry-settings-editor";
@customElement("entity-settings-helper-tab")
@@ -72,28 +72,22 @@ export class EntitySettingsHelperTab extends LitElement {
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
${this._item === null
? html`<ha-alert alert-type="info"
>${this.hass.localize(
"ui.dialogs.helper_settings.yaml_not_editable"
)}</ha-alert
>`
: nothing}
${!this._componentLoaded
? this.hass.localize(
"ui.dialogs.helper_settings.platform_not_loaded",
{ platform: this.entry.platform }
)
: html`
<span @value-changed=${this._valueChanged}>
${dynamicElement(`ha-${this.entry.platform}-form`, {
hass: this.hass,
item: this._item,
entry: this.entry,
disabled: this._item === null,
})}
</span>
`}
: this._item === null
? this.hass.localize("ui.dialogs.helper_settings.yaml_not_editable")
: html`
<span @value-changed=${this._valueChanged}>
${dynamicElement(`ha-${this.entry.platform}-form`, {
hass: this.hass,
item: this._item,
entry: this.entry,
})}
</span>
`}
<entity-registry-settings-editor
.hass=${this.hass}
.entry=${this.entry}
@@ -128,9 +122,6 @@ export class EntitySettingsHelperTab extends LitElement {
}
private _valueChanged(ev: CustomEvent): void {
if (this._item === null) {
return;
}
this._error = undefined;
this._item = ev.detail.value;
}
@@ -204,10 +195,6 @@ export class EntitySettingsHelperTab extends LitElement {
display: block;
padding: 0 !important;
}
ha-alert {
display: block;
margin-bottom: var(--ha-space-4);
}
.form {
padding: 20px 24px;
}

View File

@@ -784,7 +784,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
<ha-labels-picker
.hass=${this.hass}
.value=${this._labels}
.disabled=${!!this.disabled}
.disabled=${this.disabled}
@value-changed=${this._labelsChanged}
></ha-labels-picker>
${this._cameraPrefs

View File

@@ -17,8 +17,6 @@ class HaCounterForm extends LitElement {
@property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
private _item?: Partial<Counter>;
@state() private _name!: string;
@@ -84,7 +82,6 @@ class HaCounterForm extends LitElement {
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
@@ -94,7 +91,6 @@ class HaCounterForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.disabled=${this.disabled}
></ha-icon-picker>
<ha-textfield
.value=${this._minimum}
@@ -104,7 +100,6 @@ class HaCounterForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.counter.minimum"
)}
.disabled=${this.disabled}
></ha-textfield>
<ha-textfield
.value=${this._maximum}
@@ -114,7 +109,6 @@ class HaCounterForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.counter.maximum"
)}
.disabled=${this.disabled}
></ha-textfield>
<ha-textfield
.value=${this._initial}
@@ -124,7 +118,6 @@ class HaCounterForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.counter.initial"
)}
.disabled=${this.disabled}
></ha-textfield>
<ha-expansion-panel
header=${this.hass.localize(
@@ -140,14 +133,12 @@ class HaCounterForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.counter.step"
)}
.disabled=${this.disabled}
></ha-textfield>
<div class="row">
<ha-switch
.checked=${this._restore}
.configValue=${"restore"}
@change=${this._valueChanged}
.disabled=${this.disabled}
>
</ha-switch>
<div>

View File

@@ -14,8 +14,6 @@ class HaInputBooleanForm extends LitElement {
@property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
private _item?: InputBoolean;
@state() private _name!: string;
@@ -61,7 +59,6 @@ class HaInputBooleanForm extends LitElement {
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
@@ -71,7 +68,6 @@ class HaInputBooleanForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.disabled=${this.disabled}
></ha-icon-picker>
</div>
`;

View File

@@ -14,8 +14,6 @@ class HaInputButtonForm extends LitElement {
@property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
@state() private _name!: string;
@state() private _icon!: string;
@@ -61,7 +59,6 @@ class HaInputButtonForm extends LitElement {
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
@@ -71,7 +68,6 @@ class HaInputButtonForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.disabled=${this.disabled}
></ha-icon-picker>
</div>
`;

View File

@@ -17,8 +17,6 @@ class HaInputDateTimeForm extends LitElement {
@property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
private _item?: InputDateTime;
@state() private _name!: string;
@@ -75,7 +73,6 @@ class HaInputDateTimeForm extends LitElement {
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
@@ -85,7 +82,6 @@ class HaInputDateTimeForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.disabled=${this.disabled}
></ha-icon-picker>
<br />
${this.hass.localize("ui.dialogs.helper_settings.input_datetime.mode")}:
@@ -101,7 +97,6 @@ class HaInputDateTimeForm extends LitElement {
value="date"
.checked=${this._mode === "date"}
@change=${this._modeChanged}
.disabled=${this.disabled}
></ha-radio>
</ha-formfield>
<ha-formfield
@@ -114,7 +109,6 @@ class HaInputDateTimeForm extends LitElement {
value="time"
.checked=${this._mode === "time"}
@change=${this._modeChanged}
.disabled=${this.disabled}
></ha-radio>
</ha-formfield>
<ha-formfield
@@ -127,7 +121,6 @@ class HaInputDateTimeForm extends LitElement {
value="datetime"
.checked=${this._mode === "datetime"}
@change=${this._modeChanged}
.disabled=${this.disabled}
></ha-radio>
</ha-formfield>
</div>

View File

@@ -18,8 +18,6 @@ class HaInputNumberForm extends LitElement {
@property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
private _item?: Partial<InputNumber>;
@state() private _name!: string;
@@ -91,7 +89,6 @@ class HaInputNumberForm extends LitElement {
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
@@ -101,7 +98,6 @@ class HaInputNumberForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.disabled=${this.disabled}
></ha-icon-picker>
<ha-textfield
.value=${this._min}
@@ -112,7 +108,6 @@ class HaInputNumberForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_number.min"
)}
.disabled=${this.disabled}
></ha-textfield>
<ha-textfield
.value=${this._max}
@@ -123,7 +118,6 @@ class HaInputNumberForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_number.max"
)}
.disabled=${this.disabled}
></ha-textfield>
<ha-expansion-panel
header=${this.hass.localize(
@@ -145,7 +139,6 @@ class HaInputNumberForm extends LitElement {
value="slider"
.checked=${this._mode === "slider"}
@change=${this._modeChanged}
.disabled=${this.disabled}
></ha-radio>
</ha-formfield>
<ha-formfield
@@ -158,7 +151,6 @@ class HaInputNumberForm extends LitElement {
value="box"
.checked=${this._mode === "box"}
@change=${this._modeChanged}
.disabled=${this.disabled}
></ha-radio>
</ha-formfield>
</div>
@@ -171,7 +163,6 @@ class HaInputNumberForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_number.step"
)}
.disabled=${this.disabled}
></ha-textfield>
<ha-textfield
@@ -181,7 +172,6 @@ class HaInputNumberForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_number.unit_of_measurement"
)}
.disabled=${this.disabled}
></ha-textfield>
</ha-expansion-panel>
</div>

View File

@@ -23,8 +23,6 @@ class HaInputSelectForm extends LitElement {
@property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
private _item?: InputSelect;
@state() private _name!: string;
@@ -88,7 +86,6 @@ class HaInputSelectForm extends LitElement {
)}
.configValue=${"name"}
@input=${this._valueChanged}
.disabled=${this.disabled}
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
@@ -98,18 +95,13 @@ class HaInputSelectForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.disabled=${this.disabled}
></ha-icon-picker>
<div class="header">
${this.hass!.localize(
"ui.dialogs.helper_settings.input_select.options"
)}:
</div>
<ha-sortable
@item-moved=${this._optionMoved}
handle-selector=".handle"
.disabled=${this.disabled}
>
<ha-sortable @item-moved=${this._optionMoved} handle-selector=".handle">
<ha-list class="options">
${this._options.length
? repeat(
@@ -132,7 +124,6 @@ class HaInputSelectForm extends LitElement {
"ui.dialogs.helper_settings.input_select.remove_option"
)}
@click=${this._removeOption}
.disabled=${this.disabled}
.path=${mdiDelete}
></ha-icon-button>
</ha-list-item>
@@ -155,13 +146,8 @@ class HaInputSelectForm extends LitElement {
"ui.dialogs.helper_settings.input_select.add_option"
)}
@keydown=${this._handleKeyAdd}
.disabled=${this.disabled}
></ha-textfield>
<ha-button
size="small"
appearance="plain"
@click=${this._addOption}
.disabled=${this.disabled}
<ha-button size="small" appearance="plain" @click=${this._addOption}
>${this.hass!.localize(
"ui.dialogs.helper_settings.input_select.add"
)}</ha-button

View File

@@ -19,8 +19,6 @@ class HaInputTextForm extends LitElement {
@property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
private _item?: InputText;
@state() private _name!: string;
@@ -81,7 +79,6 @@ class HaInputTextForm extends LitElement {
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
@@ -91,7 +88,6 @@ class HaInputTextForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.disabled=${this.disabled}
></ha-icon-picker>
<ha-expansion-panel
header=${this.hass.localize(
@@ -109,7 +105,6 @@ class HaInputTextForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_text.min"
)}
.disabled=${this.disabled}
></ha-textfield>
<ha-textfield
.value=${this._max}
@@ -134,7 +129,6 @@ class HaInputTextForm extends LitElement {
value="text"
.checked=${this._mode === "text"}
@change=${this._modeChanged}
.disabled=${this.disabled}
></ha-radio>
</ha-formfield>
<ha-formfield
@@ -147,7 +141,6 @@ class HaInputTextForm extends LitElement {
value="password"
.checked=${this._mode === "password"}
@change=${this._modeChanged}
.disabled=${this.disabled}
></ha-radio>
</ha-formfield>
</div>
@@ -161,7 +154,6 @@ class HaInputTextForm extends LitElement {
.helper=${this.hass!.localize(
"ui.dialogs.helper_settings.input_text.pattern_helper"
)}
.disabled=${this.disabled}
></ha-textfield>
</ha-expansion-panel>
</div>

View File

@@ -17,9 +17,9 @@ import "../../../../components/ha-textfield";
import type { Schedule, ScheduleDay } from "../../../../data/schedule";
import { weekdays } from "../../../../data/schedule";
import { TimeZone } from "../../../../data/translation";
import { showScheduleBlockInfoDialog } from "./show-dialog-schedule-block-info";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { showScheduleBlockInfoDialog } from "./show-dialog-schedule-block-info";
const defaultFullCalendarConfig: CalendarOptions = {
plugins: [timeGridPlugin, interactionPlugin],
@@ -43,8 +43,6 @@ class HaScheduleForm extends LitElement {
@property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
@state() private _name!: string;
@state() private _icon!: string;
@@ -134,7 +132,6 @@ class HaScheduleForm extends LitElement {
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
@@ -144,9 +141,8 @@ class HaScheduleForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.disabled=${this.disabled}
></ha-icon-picker>
${!this.disabled ? html`<div id="calendar"></div>` : nothing}
<div id="calendar"></div>
</div>
`;
}
@@ -179,9 +175,7 @@ class HaScheduleForm extends LitElement {
}
protected firstUpdated(): void {
if (!this.disabled) {
this._setupCalendar();
}
this._setupCalendar();
}
private _setupCalendar(): void {

View File

@@ -1,18 +1,18 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { createDurationData } from "../../../../common/datetime/create_duration_data";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-checkbox";
import "../../../../components/ha-duration-input";
import type { HaDurationData } from "../../../../components/ha-duration-input";
import "../../../../components/ha-formfield";
import "../../../../components/ha-icon-picker";
import "../../../../components/ha-duration-input";
import "../../../../components/ha-textfield";
import type { ForDict } from "../../../../data/automation";
import type { DurationDict, Timer } from "../../../../data/timer";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { createDurationData } from "../../../../common/datetime/create_duration_data";
import type { HaDurationData } from "../../../../components/ha-duration-input";
import type { ForDict } from "../../../../data/automation";
@customElement("ha-timer-form")
class HaTimerForm extends LitElement {
@@ -20,8 +20,6 @@ class HaTimerForm extends LitElement {
@property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
private _item?: Timer;
@state() private _name!: string;
@@ -79,7 +77,6 @@ class HaTimerForm extends LitElement {
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
@@ -89,13 +86,11 @@ class HaTimerForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.disabled=${this.disabled}
></ha-icon-picker>
<ha-duration-input
.configValue=${"duration"}
.data=${this._duration_data}
@value-changed=${this._valueChanged}
.disabled=${this.disabled}
></ha-duration-input>
<ha-formfield
.label=${this.hass.localize(
@@ -106,7 +101,6 @@ class HaTimerForm extends LitElement {
.configValue=${"restore"}
.checked=${this._restore}
@click=${this._toggleRestore}
.disabled=${this.disabled}
>
</ha-checkbox>
</ha-formfield>
@@ -136,9 +130,6 @@ class HaTimerForm extends LitElement {
}
private _toggleRestore() {
if (this.disabled) {
return;
}
this._restore = !this._restore;
fireEvent(this, "value-changed", {
value: { ...this._item, restore: this._restore },

View File

@@ -332,13 +332,13 @@ export class HaConfigLovelaceDashboards extends LitElement {
});
}
if (this.hass.panels.security) {
if (this.hass.panels.safety) {
result.push({
icon: "mdi:security",
title: this.hass.localize("panel.security"),
title: this.hass.localize("panel.safety"),
show_in_sidebar: false,
mode: "storage",
url_path: "security",
url_path: "safety",
filename: "",
default: false,
require_admin: false,
@@ -470,13 +470,13 @@ export class HaConfigLovelaceDashboards extends LitElement {
}
private _canDelete(urlPath: string) {
return !["lovelace", "energy", "light", "security", "climate"].includes(
return !["lovelace", "energy", "light", "safety", "climate"].includes(
urlPath
);
}
private _canEdit(urlPath: string) {
return !["light", "security", "climate"].includes(urlPath);
return !["light", "safety", "climate"].includes(urlPath);
}
private _handleDelete = async (item: DataTableItem) => {

View File

@@ -109,26 +109,15 @@ export class AssistPipelineDetailConversation extends LitElement {
}
private _supportedLanguagesChanged(ev) {
this._supportedLanguages = ev.detail.value;
if (
this._supportedLanguages === "*" ||
!this._supportedLanguages?.includes(
this.data?.conversation_language || ""
) ||
!this.data?.conversation_language
) {
if (ev.detail.value === "*") {
// wait for update of conversation_engine
setTimeout(() => {
const value = { ...this.data };
if (this._supportedLanguages === "*") {
value.conversation_language = "*";
} else {
value.conversation_language = this._supportedLanguages?.[0] ?? null;
}
value.conversation_language = "*";
fireEvent(this, "value-changed", { value });
}, 0);
}
this._supportedLanguages = ev.detail.value;
}
static styles = css`

View File

@@ -214,7 +214,7 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement {
<ha-button
slot="primaryAction"
@click=${this._updatePipeline}
.loading=${this._submitting}
.disabled=${this._submitting}
dialogInitialFocus
>
${this._params.pipeline?.id

View File

@@ -1,23 +1,24 @@
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { goBack } from "../../common/navigate";
import { debounce } from "../../common/util/debounce";
import { deepEqual } from "../../common/util/deep-equal";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-menu-button";
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view";
import type { LovelaceConfig } from "../../data/lovelace/config/types";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy";
import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container";
const LIGHT_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = {
strategy: {
type: "light",
},
const LIGHT_LOVELACE_CONFIG: LovelaceConfig = {
views: [
{
strategy: {
type: "light",
},
},
],
};
@customElement("ha-panel-light")
@@ -33,118 +34,60 @@ class PanelLight extends LitElement {
@state() private _searchParms = new URLSearchParams(window.location.search);
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
// Initial setup
if (!this.hasUpdated) {
this.hass.loadFragmentTranslation("lovelace");
this._setLovelace();
return;
}
if (!changedProps.has("hass")) {
return;
}
const oldHass = changedProps.get("hass") as this["hass"];
if (oldHass && oldHass.localize !== this.hass.localize) {
if (oldHass?.locale !== this.hass.locale) {
this._setLovelace();
return;
}
if (oldHass && this.hass) {
// If the entity registry changed, ask the user if they want to refresh the config
if (
oldHass.entities !== this.hass.entities ||
oldHass.devices !== this.hass.devices ||
oldHass.areas !== this.hass.areas ||
oldHass.floors !== this.hass.floors
) {
if (this.hass.config.state === "RUNNING") {
this._debounceRegistriesChanged();
return;
}
}
// If ha started, refresh the config
if (
this.hass.config.state === "RUNNING" &&
oldHass.config.state !== "RUNNING"
) {
this._setLovelace();
}
}
}
private _debounceRegistriesChanged = debounce(
() => this._registriesChanged(),
200
);
private _registriesChanged = async () => {
this._setLovelace();
};
private _back(ev) {
ev.stopPropagation();
goBack();
}
protected render() {
protected render(): TemplateResult {
return html`
<div class="header">
<div class="toolbar">
${
this._searchParms.has("historyBack")
? html`
<ha-icon-button-arrow-prev
@click=${this._back}
slot="navigationIcon"
></ha-icon-button-arrow-prev>
`
: html`
<ha-menu-button
slot="navigationIcon"
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`
}
${this._searchParms.has("historyBack")
? html`
<ha-icon-button-arrow-prev
@click=${this._back}
slot="navigationIcon"
></ha-icon-button-arrow-prev>
`
: html`
<ha-menu-button
slot="navigationIcon"
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`}
<div class="main-title">${this.hass.localize("panel.light")}</div>
</div>
</div>
${
this._lovelace
? html`
<hui-view-container .hass=${this.hass}>
<hui-view
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.index=${this._viewIndex}
></hui-view
></hui-view-container>
`
: nothing
}
<hui-view-container .hass=${this.hass}>
<hui-view
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.index=${this._viewIndex}
></hui-view>
</hui-view-container>
`;
}
private async _setLovelace() {
const viewConfig = await generateLovelaceViewStrategy(
LIGHT_LOVELACE_VIEW_CONFIG,
this.hass
);
const config = { views: [viewConfig] };
const rawConfig = { views: [LIGHT_LOVELACE_VIEW_CONFIG] };
if (deepEqual(config, this._lovelace?.config)) {
return;
}
private _setLovelace() {
this._lovelace = {
config: config,
rawConfig: rawConfig,
config: LIGHT_LOVELACE_CONFIG,
rawConfig: LIGHT_LOVELACE_CONFIG,
editMode: false,
urlPath: "light",
mode: "generated",

View File

@@ -61,24 +61,6 @@ const processAreasForLight = (
return cards;
};
const processUnassignedLights = (
hass: HomeAssistant,
entities: string[]
): LovelaceCardConfig[] => {
const unassignedFilter = generateEntityFilter(hass, {
area: null,
});
const unassignedLights = entities.filter(unassignedFilter);
const areaCards: LovelaceCardConfig[] = [];
const computeTileCard = computeAreaTileCardConfig(hass, "", false);
for (const entityId of unassignedLights) {
areaCards.push(computeTileCard(entityId));
}
return areaCards;
};
@customElement("light-view-strategy")
export class LightViewStrategy extends ReactiveElement {
static async generate(
@@ -154,30 +136,10 @@ export class LightViewStrategy extends ReactiveElement {
}
}
// Process unassigned lights
const unassignedCards = processUnassignedLights(hass, entities);
if (unassignedCards.length > 0) {
const section: LovelaceSectionRawConfig = {
type: "grid",
column_span: 2,
cards: [
{
type: "heading",
heading:
sections.length > 0
? hass.localize("ui.panel.lovelace.strategy.light.other_lights")
: hass.localize("ui.panel.lovelace.strategy.light.lights"),
},
...unassignedCards,
],
};
sections.push(section);
}
return {
type: "sections",
max_columns: 2,
sections: sections,
sections: sections || [],
};
}
}

View File

@@ -419,15 +419,13 @@ class HuiEnergySankeyCard
};
deviceNodes.forEach((deviceNode) => {
const entity = this.hass.states[deviceNode.id];
const { area, floor } = entity
? getEntityContext(
entity,
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
)
: { area: null, floor: null };
const { area, floor } = getEntityContext(
entity,
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
if (area) {
if (area.area_id in areas) {
areas[area.area_id].value += deviceNode.value;

View File

@@ -33,7 +33,7 @@ import type { HomeSummaryCard } from "./types";
const COLORS: Record<HomeSummary, string> = {
light: "amber",
climate: "deep-orange",
security: "blue-grey",
safety: "blue-grey",
media_players: "blue",
};
@@ -87,6 +87,11 @@ export class HuiHomeSummaryCard extends LitElement implements LovelaceCard {
const allEntities = Object.keys(this.hass!.states);
const areas = Object.values(this.hass.areas);
const areasFilter = generateEntityFilter(this.hass, {
area: areas.map((area) => area.area_id),
});
const entitiesInsideArea = allEntities.filter(areasFilter);
switch (this._config.summary) {
case "light": {
@@ -95,7 +100,7 @@ export class HuiHomeSummaryCard extends LitElement implements LovelaceCard {
generateEntityFilter(this.hass!, filter)
);
const lightEntities = findEntities(allEntities, lightsFilters);
const lightEntities = findEntities(entitiesInsideArea, lightsFilters);
const onLights = lightEntities.filter((entityId) => {
const s = this.hass!.states[entityId]?.state;
@@ -142,20 +147,20 @@ export class HuiHomeSummaryCard extends LitElement implements LovelaceCard {
? `${formattedMinTemp}°`
: `${formattedMinTemp} - ${formattedMaxTemp}°`;
}
case "security": {
case "safety": {
// Alarm and lock status
const securityFilters = HOME_SUMMARIES_FILTERS.security.map((filter) =>
const safetyFilters = HOME_SUMMARIES_FILTERS.safety.map((filter) =>
generateEntityFilter(this.hass!, filter)
);
const securityEntities = findEntities(allEntities, securityFilters);
const safetyEntities = findEntities(entitiesInsideArea, safetyFilters);
const locks = securityEntities.filter((entityId) => {
const locks = safetyEntities.filter((entityId) => {
const domain = computeDomain(entityId);
return domain === "lock";
});
const alarms = securityEntities.filter((entityId) => {
const alarms = safetyEntities.filter((entityId) => {
const domain = computeDomain(entityId);
return domain === "alarm_control_panel";
});
@@ -199,7 +204,7 @@ export class HuiHomeSummaryCard extends LitElement implements LovelaceCard {
);
const mediaPlayerEntities = findEntities(
allEntities,
entitiesInsideArea,
mediaPlayerFilters
);

View File

@@ -1,6 +1,5 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
@@ -98,10 +97,7 @@ export class HuiIframeCard extends LitElement implements LovelaceCard {
: `${sandbox_user_params} ${IFRAME_SANDBOX}`;
return html`
<ha-card
class=${classMap({ "hide-background": this._config.hide_background })}
.header=${this._config.title}
>
<ha-card .header=${this._config.title}>
<div
id="root"
style=${styleMap({
@@ -137,12 +133,6 @@ export class HuiIframeCard extends LitElement implements LovelaceCard {
flex-direction: column;
}
ha-card.hide-background {
background: transparent;
box-shadow: none;
border: none;
}
#root {
width: 100%;
height: 100%;

View File

@@ -323,7 +323,6 @@ export interface IframeCardConfig extends LovelaceCardConfig {
title?: string;
allow?: string;
url: string;
hide_background?: boolean;
}
export interface LightCardConfig extends LovelaceCardConfig {

View File

@@ -16,7 +16,6 @@ const cardConfigStruct = assign(
url: optional(string()),
aspect_ratio: optional(string()),
allow_open_top_navigation: optional(boolean()),
hide_background: optional(boolean()),
})
);
@@ -30,7 +29,6 @@ const SCHEMA = [
{ name: "aspect_ratio", selector: { text: {} } },
],
},
{ name: "hide_background", selector: { boolean: {} } },
] as const;
@customElement("hui-iframe-card-editor")
@@ -58,7 +56,6 @@ export class HuiIframeCardEditor
.data=${this._config}
.schema=${SCHEMA}
.computeLabel=${this._computeLabelCallback}
.computeHelper=${this._computeHelperCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
@@ -68,29 +65,8 @@ export class HuiIframeCardEditor
fireEvent(this, "config-changed", { config: ev.detail.value });
}
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
switch (schema.name) {
case "hide_background":
return this.hass!.localize(
"ui.panel.lovelace.editor.card.iframe.hide_background"
);
default:
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
);
}
};
private _computeHelperCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
switch (schema.name) {
case "hide_background":
return this.hass!.localize(
"ui.panel.lovelace.editor.card.iframe.hide_background_helper"
);
default:
return "";
}
};
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) =>
this.hass!.localize(`ui.panel.lovelace.editor.card.generic.${schema.name}`);
}
declare global {

View File

@@ -1,7 +1,5 @@
import type { HassServiceTarget } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import {
array,
assert,
@@ -11,21 +9,23 @@ import {
optional,
string,
} from "superstruct";
import type { HassServiceTarget } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/entity/ha-entities-picker";
import "../../../../components/ha-target-picker";
import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import "../../../../components/ha-target-picker";
import type { HaEntityPickerEntityFilterFunc } from "../../../../data/entity";
import { filterLogbookCompatibleEntities } from "../../../../data/logbook";
import { targetStruct } from "../../../../data/script";
import { resolveEntityIDs } from "../../../../data/selector";
import { getSensorNumericDeviceClasses } from "../../../../data/sensor";
import type { HomeAssistant } from "../../../../types";
import { DEFAULT_HOURS_TO_SHOW } from "../../cards/hui-logbook-card";
import type { LogbookCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { DEFAULT_HOURS_TO_SHOW } from "../../cards/hui-logbook-card";
import { targetStruct } from "../../../../data/script";
import { getSensorNumericDeviceClasses } from "../../../../data/sensor";
import type { HaEntityPickerEntityFilterFunc } from "../../../../data/entity";
import { resolveEntityIDs } from "../../../../data/selector";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
@@ -132,6 +132,7 @@ export class HuiLogbookCardEditor
.hass=${this.hass}
.entityFilter=${this._filterFunc}
.value=${this._targetPicker}
add-on-top
@value-changed=${this._entitiesChanged}
></ha-target-picker>
`;
@@ -188,13 +189,6 @@ export class HuiLogbookCardEditor
);
}
};
static styles = css`
ha-target-picker {
display: block;
margin-top: var(--ha-space-4);
}
`;
}
declare global {

View File

@@ -48,7 +48,7 @@ const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
import("./home/home-media-players-view-strategy"),
"home-area": () => import("./home/home-area-view-strategy"),
light: () => import("../../light/strategies/light-view-strategy"),
security: () => import("../../security/strategies/security-view-strategy"),
safety: () => import("../../safety/strategies/safety-view-strategy"),
climate: () => import("../../climate/strategies/climate-view-strategy"),
},
section: {

View File

@@ -2,12 +2,12 @@ import type { EntityFilter } from "../../../../../common/entity/entity_filter";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import { climateEntityFilters } from "../../../../climate/strategies/climate-view-strategy";
import { lightEntityFilters } from "../../../../light/strategies/light-view-strategy";
import { securityEntityFilters } from "../../../../security/strategies/security-view-strategy";
import { safetyEntityFilters } from "../../../../safety/strategies/safety-view-strategy";
export const HOME_SUMMARIES = [
"light",
"climate",
"security",
"safety",
"media_players",
] as const;
@@ -16,14 +16,14 @@ export type HomeSummary = (typeof HOME_SUMMARIES)[number];
export const HOME_SUMMARIES_ICONS: Record<HomeSummary, string> = {
light: "mdi:lamps",
climate: "mdi:home-thermometer",
security: "mdi:security",
safety: "mdi:security",
media_players: "mdi:multimedia",
};
export const HOME_SUMMARIES_FILTERS: Record<HomeSummary, EntityFilter[]> = {
light: lightEntityFilters,
climate: climateEntityFilters,
security: securityEntityFilters,
safety: safetyEntityFilters,
media_players: [{ domain: "media_player", entity_category: "none" }],
};
@@ -31,7 +31,7 @@ export const getSummaryLabel = (
localize: LocalizeFunc,
summary: HomeSummary
) => {
if (summary === "light" || summary === "climate" || summary === "security") {
if (summary === "light" || summary === "climate" || summary === "safety") {
return localize(`panel.${summary}`);
}
return localize(`ui.panel.lovelace.strategy.home.summary_list.${summary}`);

View File

@@ -104,7 +104,7 @@ export class HomeAreaViewStrategy extends ReactiveElement {
const {
light,
climate,
security,
safety,
media_players: mediaPlayers,
} = entitiesBySummary;
@@ -136,16 +136,16 @@ export class HomeAreaViewStrategy extends ReactiveElement {
});
}
if (security.length > 0) {
if (safety.length > 0) {
sections.push({
type: "grid",
cards: [
computeHeadingCard(
getSummaryLabel(hass.localize, "security"),
HOME_SUMMARIES_ICONS.security,
"/security?historyBack=1"
getSummaryLabel(hass.localize, "safety"),
HOME_SUMMARIES_ICONS.safety,
"/safety?historyBack=1"
),
...security.map(computeTileCard),
...safety.map(computeTileCard),
],
});
}

View File

@@ -1,11 +1,7 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import {
findEntities,
generateEntityFilter,
} from "../../../../common/entity/entity_filter";
import { floorDefaultIcon } from "../../../../components/ha-floor-icon";
import { generateEntityFilter } from "../../../../common/entity/entity_filter";
import type { AreaRegistryEntry } from "../../../../data/area_registry";
import { getEnergyPreferences } from "../../../../data/energy";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
@@ -25,7 +21,7 @@ import type {
import { getAreas, getFloors } from "../areas/helpers/areas-strategy-helper";
import type { CommonControlSectionStrategyConfig } from "../usage_prediction/common-controls-section-strategy";
import { getHomeStructure } from "./helpers/home-structure";
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
import { floorDefaultIcon } from "../../../../components/ha-floor-icon";
export interface HomeMainViewStrategyConfig {
type: "home-main";
@@ -148,33 +144,15 @@ export class HomeMainViewStrategy extends ReactiveElement {
column_span: maxColumns,
} as LovelaceStrategySectionConfig;
const allEntities = Object.keys(hass.states);
const mediaPlayerFilter = HOME_SUMMARIES_FILTERS.media_players.map(
(filter) => generateEntityFilter(hass, filter)
);
const lightsFilters = HOME_SUMMARIES_FILTERS.light.map((filter) =>
generateEntityFilter(hass, filter)
);
const climateFilters = HOME_SUMMARIES_FILTERS.climate.map((filter) =>
generateEntityFilter(hass, filter)
);
const securityFilters = HOME_SUMMARIES_FILTERS.security.map((filter) =>
generateEntityFilter(hass, filter)
);
const hasLights = findEntities(allEntities, lightsFilters).length > 0;
const hasMediaPlayers =
findEntities(allEntities, mediaPlayerFilter).length > 0;
const hasClimate = findEntities(allEntities, climateFilters).length > 0;
const hasSecurity = findEntities(allEntities, securityFilters).length > 0;
const summaryCards: LovelaceCardConfig[] = [
hasLights &&
({
const summarySection: LovelaceSectionConfig = {
type: "grid",
column_span: maxColumns,
cards: [
{
type: "heading",
heading: hass.localize("ui.panel.lovelace.strategy.home.summaries"),
},
{
type: "home-summary",
summary: "light",
vertical: true,
@@ -186,9 +164,8 @@ export class HomeMainViewStrategy extends ReactiveElement {
rows: 2,
columns: 4,
},
} satisfies HomeSummaryCard),
hasClimate &&
({
} satisfies HomeSummaryCard,
{
type: "home-summary",
summary: "climate",
vertical: true,
@@ -200,23 +177,21 @@ export class HomeMainViewStrategy extends ReactiveElement {
rows: 2,
columns: 4,
},
} satisfies HomeSummaryCard),
hasSecurity &&
({
} satisfies HomeSummaryCard,
{
type: "home-summary",
summary: "security",
summary: "safety",
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "/security?historyBack=1",
navigation_path: "/safety?historyBack=1",
},
grid_options: {
rows: 2,
columns: 4,
},
} satisfies HomeSummaryCard),
hasMediaPlayers &&
({
} satisfies HomeSummaryCard,
{
type: "home-summary",
summary: "media_players",
vertical: true,
@@ -228,25 +203,10 @@ export class HomeMainViewStrategy extends ReactiveElement {
rows: 2,
columns: 4,
},
} satisfies HomeSummaryCard),
].filter(Boolean) as LovelaceCardConfig[];
const summarySection: LovelaceSectionConfig = {
type: "grid",
column_span: maxColumns,
cards: [],
} satisfies HomeSummaryCard,
],
};
if (summaryCards.length) {
summarySection.cards!.push(
{
type: "heading",
heading: hass.localize("ui.panel.lovelace.strategy.home.summaries"),
},
...summaryCards
);
}
const weatherFilter = generateEntityFilter(hass, {
domain: "weather",
entity_category: "none",
@@ -302,7 +262,7 @@ export class HomeMainViewStrategy extends ReactiveElement {
[
favoriteSection.cards && favoriteSection,
commonControlsSection,
summarySection.cards && summarySection,
summarySection,
...floorsSections,
widgetSection.cards && widgetSection,
] satisfies (LovelaceSectionRawConfig | undefined)[]

View File

@@ -59,26 +59,6 @@ const processAreasForMediaPlayers = (
return cards;
};
const processUnassignedEntities = (
hass: HomeAssistant,
entities: string[]
): LovelaceCardConfig[] => {
const unassignedFilter = generateEntityFilter(hass, {
area: null,
});
const unassignedEntities = entities.filter(unassignedFilter);
const areaCards: LovelaceCardConfig[] = [];
for (const entityId of unassignedEntities) {
areaCards.push({
type: "media-control",
entity: entityId,
} satisfies MediaControlCardConfig);
}
return areaCards;
};
@customElement("home-media-players-view-strategy")
export class HomeMMediaPlayersViewStrategy extends ReactiveElement {
static async generate(
@@ -154,35 +134,10 @@ export class HomeMMediaPlayersViewStrategy extends ReactiveElement {
}
}
// Process unassigned entities
const unassignedCards = processUnassignedEntities(hass, entities);
if (unassignedCards.length > 0) {
const section: LovelaceSectionRawConfig = {
type: "grid",
column_span: 2,
cards: [
{
type: "heading",
heading:
sections.length > 0
? hass.localize(
"ui.panel.lovelace.strategy.home_media_players.other_media_players"
)
: hass.localize(
"ui.panel.lovelace.strategy.home_media_players.media_players"
),
},
...unassignedCards,
],
};
sections.push(section);
}
return {
type: "sections",
max_columns: 2,
sections: sections,
sections: sections || [],
};
}
}

View File

@@ -29,6 +29,8 @@ import {
} from "../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "../../components/ha-dropdown-item";
import "../../components/ha-dropdown";
// Client ID used by iOS app
const iOSclientId = "https://home-assistant.io/iOS";
@@ -146,19 +148,18 @@ class HaRefreshTokens extends LitElement {
)}
</div>
<div>
<ha-md-button-menu positioning="popover">
<ha-dropdown>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-md-menu-item
graphic="icon"
<ha-dropdown-item
@click=${this._toggleTokenExpiration}
.token=${token}
>
<ha-svg-icon
slot="start"
slot="icon"
.path=${token.expire_at
? mdiClockRemoveOutline
: mdiClockCheckOutline}
@@ -170,24 +171,20 @@ class HaRefreshTokens extends LitElement {
: this.hass.localize(
"ui.panel.profile.refresh_tokens.enable_token_expiration"
)}
</ha-md-menu-item>
<ha-md-menu-item
graphic="icon"
class="warning"
</ha-dropdown-item>
<ha-dropdown-item
variant="danger"
.disabled=${token.is_current}
@click=${this._deleteToken}
.token=${token}
>
<ha-svg-icon
class="warning"
slot="start"
slot="icon"
.path=${mdiDelete}
></ha-svg-icon>
<div slot="headline">
${this.hass.localize("ui.common.delete")}
</div>
</ha-md-menu-item>
</ha-md-button-menu>
${this.hass.localize("ui.common.delete")}
</ha-dropdown-item>
</ha-dropdown>
</div>
</ha-settings-row>
`

View File

@@ -1,27 +1,28 @@
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { goBack } from "../../common/navigate";
import { debounce } from "../../common/util/debounce";
import { deepEqual } from "../../common/util/deep-equal";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-menu-button";
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view";
import type { LovelaceConfig } from "../../data/lovelace/config/types";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy";
import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container";
const SECURITY_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = {
strategy: {
type: "security",
},
const SAFETY_LOVELACE_CONFIG: LovelaceConfig = {
views: [
{
strategy: {
type: "safety",
},
},
],
};
@customElement("ha-panel-security")
class PanelSecurity extends LitElement {
@customElement("ha-panel-safety")
class PanelSafety extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public narrow = false;
@@ -33,120 +34,62 @@ class PanelSecurity extends LitElement {
@state() private _searchParms = new URLSearchParams(window.location.search);
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
// Initial setup
if (!this.hasUpdated) {
this.hass.loadFragmentTranslation("lovelace");
this._setLovelace();
return;
}
if (!changedProps.has("hass")) {
return;
}
const oldHass = changedProps.get("hass") as this["hass"];
if (oldHass && oldHass.localize !== this.hass.localize) {
if (oldHass?.locale !== this.hass.locale) {
this._setLovelace();
return;
}
if (oldHass && this.hass) {
// If the entity registry changed, ask the user if they want to refresh the config
if (
oldHass.entities !== this.hass.entities ||
oldHass.devices !== this.hass.devices ||
oldHass.areas !== this.hass.areas ||
oldHass.floors !== this.hass.floors
) {
if (this.hass.config.state === "RUNNING") {
this._debounceRegistriesChanged();
return;
}
}
// If ha started, refresh the config
if (
this.hass.config.state === "RUNNING" &&
oldHass.config.state !== "RUNNING"
) {
this._setLovelace();
}
}
}
private _debounceRegistriesChanged = debounce(
() => this._registriesChanged(),
200
);
private _registriesChanged = async () => {
this._setLovelace();
};
private _back(ev) {
ev.stopPropagation();
goBack();
}
protected render() {
protected render(): TemplateResult {
return html`
<div class="header">
<div class="toolbar">
${
this._searchParms.has("historyBack")
? html`
<ha-icon-button-arrow-prev
@click=${this._back}
slot="navigationIcon"
></ha-icon-button-arrow-prev>
`
: html`
<ha-menu-button
slot="navigationIcon"
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`
}
<div class="main-title">${this.hass.localize("panel.security")}</div>
</div>
</div>
${
this._lovelace
? html`
<hui-view-container .hass=${this.hass}>
<hui-view
${this._searchParms.has("historyBack")
? html`
<ha-icon-button-arrow-prev
@click=${this._back}
slot="navigationIcon"
></ha-icon-button-arrow-prev>
`
: html`
<ha-menu-button
slot="navigationIcon"
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.index=${this._viewIndex}
></hui-view
></hui-view-container>
`
: nothing
}
></ha-menu-button>
`}
<div class="main-title">${this.hass.localize("panel.safety")}</div>
</div>
</div>
<hui-view-container .hass=${this.hass}>
<hui-view
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.index=${this._viewIndex}
></hui-view>
</hui-view-container>
`;
}
private async _setLovelace() {
const viewConfig = await generateLovelaceViewStrategy(
SECURITY_LOVELACE_VIEW_CONFIG,
this.hass
);
const config = { views: [viewConfig] };
const rawConfig = { views: [SECURITY_LOVELACE_VIEW_CONFIG] };
if (deepEqual(config, this._lovelace?.config)) {
return;
}
private _setLovelace() {
this._lovelace = {
config: config,
rawConfig: rawConfig,
config: SAFETY_LOVELACE_CONFIG,
rawConfig: SAFETY_LOVELACE_CONFIG,
editMode: false,
urlPath: "security",
urlPath: "safety",
mode: "generated",
locale: this.hass.locale,
enableFullEditMode: () => undefined,
@@ -248,6 +191,6 @@ class PanelSecurity extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"ha-panel-security": PanelSecurity;
"ha-panel-safety": PanelSafety;
}
}

View File

@@ -17,11 +17,11 @@ import {
} from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
import { getHomeStructure } from "../../lovelace/strategies/home/helpers/home-structure";
export interface SecurityViewStrategyConfig {
type: "security";
export interface SafetyViewStrategyConfig {
type: "safety";
}
export const securityEntityFilters: EntityFilter[] = [
export const safetyEntityFilters: EntityFilter[] = [
{
domain: "camera",
entity_category: "none",
@@ -67,7 +67,7 @@ export const securityEntityFilters: EntityFilter[] = [
},
];
const processAreasForSecurity = (
const processAreasForSafety = (
areaIds: string[],
hass: HomeAssistant,
entities: string[]
@@ -81,12 +81,12 @@ const processAreasForSecurity = (
const areaFilter = generateEntityFilter(hass, {
area: area.area_id,
});
const areaSecurityEntities = entities.filter(areaFilter);
const areaSafetyEntities = entities.filter(areaFilter);
const areaCards: LovelaceCardConfig[] = [];
const computeTileCard = computeAreaTileCardConfig(hass, "", false);
for (const entityId of areaSecurityEntities) {
for (const entityId of areaSafetyEntities) {
areaCards.push(computeTileCard(entityId));
}
@@ -103,28 +103,10 @@ const processAreasForSecurity = (
return cards;
};
const processUnassignedEntities = (
hass: HomeAssistant,
entities: string[]
): LovelaceCardConfig[] => {
const unassignedFilter = generateEntityFilter(hass, {
area: null,
});
const unassignedLights = entities.filter(unassignedFilter);
const areaCards: LovelaceCardConfig[] = [];
const computeTileCard = computeAreaTileCardConfig(hass, "", false);
for (const entityId of unassignedLights) {
areaCards.push(computeTileCard(entityId));
}
return areaCards;
};
@customElement("security-view-strategy")
export class SecurityViewStrategy extends ReactiveElement {
@customElement("safety-view-strategy")
export class SafetyViewStrategy extends ReactiveElement {
static async generate(
_config: SecurityViewStrategyConfig,
_config: SafetyViewStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const areas = getAreas(hass.areas);
@@ -135,11 +117,11 @@ export class SecurityViewStrategy extends ReactiveElement {
const allEntities = Object.keys(hass.states);
const securityFilters = securityEntityFilters.map((filter) =>
const safetyFilters = safetyEntityFilters.map((filter) =>
generateEntityFilter(hass, filter)
);
const entities = findEntities(allEntities, securityFilters);
const entities = findEntities(allEntities, safetyFilters);
const floorCount = home.floors.length + (home.areas.length ? 1 : 0);
@@ -164,7 +146,7 @@ export class SecurityViewStrategy extends ReactiveElement {
],
};
const areaCards = processAreasForSecurity(areaIds, hass, entities);
const areaCards = processAreasForSafety(areaIds, hass, entities);
if (areaCards.length > 0) {
section.cards!.push(...areaCards);
@@ -188,7 +170,7 @@ export class SecurityViewStrategy extends ReactiveElement {
],
};
const areaCards = processAreasForSecurity(home.areas, hass, entities);
const areaCards = processAreasForSafety(home.areas, hass, entities);
if (areaCards.length > 0) {
section.cards!.push(...areaCards);
@@ -196,39 +178,16 @@ export class SecurityViewStrategy extends ReactiveElement {
}
}
// Process unassigned entities
const unassignedCards = processUnassignedEntities(hass, entities);
if (unassignedCards.length > 0) {
const section: LovelaceSectionRawConfig = {
type: "grid",
column_span: 2,
cards: [
{
type: "heading",
heading:
sections.length > 0
? hass.localize(
"ui.panel.lovelace.strategy.security.other_devices"
)
: hass.localize("ui.panel.lovelace.strategy.security.devices"),
},
...unassignedCards,
],
};
sections.push(section);
}
return {
type: "sections",
max_columns: 2,
sections: sections,
sections: sections || [],
};
}
}
declare global {
interface HTMLElementTagNameMap {
"security-view-strategy": SecurityViewStrategy;
"safety-view-strategy": SafetyViewStrategy;
}
}

View File

@@ -199,23 +199,3 @@ export const baseEntrypointStyles = css`
width: 100vw;
}
`;
export const baseAnimationStyles = css`
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
`;

View File

@@ -155,6 +155,10 @@ export const semanticColorStyles = css`
/* Surfaces */
--ha-color-surface-default: var(--ha-color-neutral-95);
--ha-color-on-surface-default: var(--ha-color-neutral-05);
/* shadow */
--ha-color-shadow: rgba(0, 0, 0, 0.2);
}
`;
@@ -286,5 +290,9 @@ export const darkSemanticColorStyles = css`
/* Surfaces */
--ha-color-surface-default: var(--ha-color-neutral-10);
--ha-color-on-surface-default: var(--ha-color-neutral-95);
/* shadow */
--ha-color-shadow: rgba(255, 255, 255, 0.2);
}
`;

View File

@@ -52,7 +52,9 @@ export const waColorStyles = css`
--wa-color-danger-on-normal: var(--ha-color-on-danger-normal);
--wa-color-danger-on-quiet: var(--ha-color-on-danger-quiet);
--wa-color-surface-default: var(--card-background-color);
--wa-color-text-normal: var(--ha-color-text-primary);
--wa-color-surface-default: var(--card-background-color);
--wa-color-surface-raised: var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff));
--wa-panel-border-radius: var(--ha-border-radius-3xl);
--wa-panel-border-style: solid;
--wa-panel-border-width: 1px;

View File

@@ -42,14 +42,6 @@ export const coreStyles = css`
--ha-space-18: 72px;
--ha-space-19: 76px;
--ha-space-20: 80px;
--ha-animation-base-duration: 350ms;
}
@media (prefers-reduced-motion: reduce) {
html {
--ha-animation-base-duration: 0ms;
}
}
`;

View File

@@ -16,7 +16,17 @@ export const waMainStyles = css`
--wa-font-weight-action: var(--ha-font-weight-medium);
--wa-transition-fast: 75ms;
--wa-transition-easing: ease;
--wa-border-width-l: var(--ha-border-radius-lg);
--wa-border-style: solid;
--wa-border-width-s: var(--ha-border-width-sm);
--wa-border-width-m: var(--ha-border-width-md);
--wa-border-width-l: var(--ha-border-width-lg);
--wa-border-radius-s: var(--ha-border-radius-sm);
--wa-border-radius-m: var(--ha-border-radius-md);
--wa-border-radius-l: var(--ha-border-radius-lg);
--wa-line-height-condensed: 1.25;
--wa-space-xl: 32px;
}

View File

@@ -12,7 +12,7 @@
"media_browser": "Media",
"profile": "Profile",
"light": "Lights",
"security": "Security",
"safety": "Safety",
"climate": "Climate"
},
"state": {
@@ -695,11 +695,6 @@
"remove_device_id": "Remove device",
"remove_entity_id": "Remove entity",
"remove_label_id": "Remove label",
"floor_not_found": "Floor not found",
"area_not_found": "Area not found",
"device_not_found": "Device not found",
"entity_not_found": "Entity not found",
"label_not_found": "Label not found",
"devices_count": "{count} {count, plural,\n one {device}\n other {devices}\n}",
"entities_count": "{count} {count, plural,\n one {entity}\n other {entities}\n}",
"target_details": "Target details",
@@ -1434,7 +1429,6 @@
"back_to_info": "Back to info",
"info": "Information",
"related": "Related",
"add_entity_to": "Add to",
"history": "History",
"aggregate": "5-minute aggregated",
"logbook": "Activity",
@@ -1451,10 +1445,6 @@
"last_action": "Last action",
"last_triggered": "Last triggered"
},
"add_to": {
"no_actions": "No actions available",
"action_failed": "Failed to perform the action {error}"
},
"sun": {
"azimuth": "Azimuth",
"elevation": "Elevation",
@@ -3076,7 +3066,6 @@
"remove_co2_signal": "Remove Electricity Maps integration",
"add_co2_signal": "Add Electricity Maps integration",
"flow_dialog": {
"cost_entity_helper": "Any sensor with a unit of `{currency}/(valid energy unit)` (e.g. `{currency}/Wh` or `{currency}/kWh`) may be used and will be automatically converted.",
"from": {
"header": "Configure grid consumption",
"paragraph": "Grid consumption is the energy that flows from the energy grid to your home.",
@@ -6986,22 +6975,6 @@
"common_controls": {
"not_loaded": "Usage Prediction integration is not loaded.",
"no_data": "This place will soon fill up with the entities you use most often, based on your activity."
},
"light": {
"lights": "Lights",
"other_lights": "Other lights"
},
"security": {
"devices": "Devices",
"other_devices": "Other devices"
},
"climate": {
"devices": "Devices",
"other_devices": "Other devices"
},
"home_media_players": {
"media_players": "Media players",
"other_media_players": "Other media players"
}
},
"cards": {
@@ -7789,9 +7762,7 @@
},
"iframe": {
"name": "Webpage",
"description": "The Webpage card allows you to embed your favorite webpage right into Home Assistant.",
"hide_background": "Hide background",
"hide_background_helper": "Useful for pages which allow a transparent background."
"description": "The Webpage card allows you to embed your favorite webpage right into Home Assistant."
},
"light": {
"name": "Light",

View File

@@ -1,26 +1,10 @@
import type { TemplateResult } from "lit";
import { render } from "lit";
const removeElement = (launchScreenElement: HTMLElement) => {
launchScreenElement.classList.add("removing");
setTimeout(() => {
launchScreenElement.parentElement?.removeChild(launchScreenElement);
}, 500);
};
export const removeLaunchScreen = () => {
const launchScreenElement = document.getElementById("ha-launch-screen");
if (!launchScreenElement?.parentElement) {
return;
}
if (document.startViewTransition) {
document.startViewTransition(() => {
removeElement(launchScreenElement);
});
} else {
// Fallback: Direct removal without transition
removeElement(launchScreenElement);
if (launchScreenElement) {
launchScreenElement.parentElement!.removeChild(launchScreenElement);
}
};

View File

@@ -388,70 +388,4 @@ describe("generateEntityFilter", () => {
expect(filter("light.no_area")).toBe(false);
});
});
describe("null filtering", () => {
it("should filter entities with no area when null is used", () => {
const filter = generateEntityFilter(mockHass, { area: null });
expect(filter("light.no_area")).toBe(true);
expect(filter("light.living_room")).toBe(false);
});
it("should filter entities with specific area OR no area when null is in array", () => {
const filter = generateEntityFilter(mockHass, {
area: ["living_room", null],
});
expect(filter("light.living_room")).toBe(true);
expect(filter("sensor.temperature")).toBe(true);
expect(filter("light.no_area")).toBe(true);
expect(filter("switch.kitchen")).toBe(false);
});
it("should filter entities with no floor when null is used", () => {
const filter = generateEntityFilter(mockHass, { floor: null });
expect(filter("light.no_area")).toBe(true);
expect(filter("light.living_room")).toBe(false);
});
it("should filter entities with specific floor OR no floor", () => {
const filter = generateEntityFilter(mockHass, {
floor: ["main_floor", null],
});
expect(filter("light.living_room")).toBe(true);
expect(filter("switch.kitchen")).toBe(true);
expect(filter("light.no_area")).toBe(true);
expect(filter("light.bedroom")).toBe(false);
});
it("should filter entities with no device when null is used", () => {
const filter = generateEntityFilter(mockHass, { device: null });
expect(filter("light.living_room")).toBe(false);
expect(filter("light.no_area")).toBe(false);
});
it("should filter entities with specific device OR no device", () => {
const filter = generateEntityFilter(mockHass, {
device: ["device1", null],
});
expect(filter("light.living_room")).toBe(true);
expect(filter("switch.kitchen")).toBe(false);
});
it("should combine null filtering with other criteria", () => {
const filter = generateEntityFilter(mockHass, {
domain: "light",
area: ["living_room", null],
});
expect(filter("light.living_room")).toBe(true);
expect(filter("light.no_area")).toBe(true);
expect(filter("light.bedroom")).toBe(false);
expect(filter("sensor.temperature")).toBe(false);
});
});
});

118
yarn.lock
View File

@@ -3446,18 +3446,18 @@ __metadata:
languageName: node
linkType: hard
"@octokit/core@npm:^7.0.6":
version: 7.0.6
resolution: "@octokit/core@npm:7.0.6"
"@octokit/core@npm:^7.0.2":
version: 7.0.4
resolution: "@octokit/core@npm:7.0.4"
dependencies:
"@octokit/auth-token": "npm:^6.0.0"
"@octokit/graphql": "npm:^9.0.3"
"@octokit/request": "npm:^10.0.6"
"@octokit/request-error": "npm:^7.0.2"
"@octokit/types": "npm:^16.0.0"
"@octokit/graphql": "npm:^9.0.1"
"@octokit/request": "npm:^10.0.2"
"@octokit/request-error": "npm:^7.0.0"
"@octokit/types": "npm:^15.0.0"
before-after-hook: "npm:^4.0.0"
universal-user-agent: "npm:^7.0.0"
checksum: 10/852d41fc3150d2a891156427dd0575c77889f1c7a109894ee541594e3fd47c0d4e0a93fee22966c507dfd6158b522e42846c2ac46b9d896078194c95fa81f4ae
checksum: 10/d691df211ba9a2941ec97dc32e1c34e26e7c8161425fb573425cfc2727f548bf2a2445eec9d36748b4b469478feb21de4bec1c00a34aa02eedf5b7625ca52189
languageName: node
linkType: hard
@@ -3471,14 +3471,14 @@ __metadata:
languageName: node
linkType: hard
"@octokit/graphql@npm:^9.0.3":
version: 9.0.3
resolution: "@octokit/graphql@npm:9.0.3"
"@octokit/graphql@npm:^9.0.1":
version: 9.0.1
resolution: "@octokit/graphql@npm:9.0.1"
dependencies:
"@octokit/request": "npm:^10.0.6"
"@octokit/types": "npm:^16.0.0"
"@octokit/request": "npm:^10.0.2"
"@octokit/types": "npm:^14.0.0"
universal-user-agent: "npm:^7.0.0"
checksum: 10/7b16f281f8571dce55280b3986fbb8d15465a7236164a5f6497ded7597ff9ee95d5796924555b979903fe8c6706fe6be1b3e140d807297f85ac8edeadc28f9fe
checksum: 10/02d7ea4e2c17a4d4b7311150d0326318c756aff6cf955d9ba443a4bf26b32784832060379fc74f4537657415b262c10adb7f4a1655e15b143d19c2f099b87f16
languageName: node
linkType: hard
@@ -3501,6 +3501,20 @@ __metadata:
languageName: node
linkType: hard
"@octokit/openapi-types@npm:^25.1.0":
version: 25.1.0
resolution: "@octokit/openapi-types@npm:25.1.0"
checksum: 10/91989a4cec12250e6b3226e9aa931c05c27d46a946725d01e6a831af3890f157210a7032f07641a156c608cc6bf6cf55a28f07179910b644966358d6d559dec6
languageName: node
linkType: hard
"@octokit/openapi-types@npm:^26.0.0":
version: 26.0.0
resolution: "@octokit/openapi-types@npm:26.0.0"
checksum: 10/b9e1b1230b0a3d280b48902a927ce4e7df0d51096c928e2ee929035b0bce779fe7748a1ae58696f1c3080bf8338b6388d5caba5b0dbf254e9713303ed3abf7c2
languageName: node
linkType: hard
"@octokit/openapi-types@npm:^27.0.0":
version: 27.0.0
resolution: "@octokit/openapi-types@npm:27.0.0"
@@ -3508,14 +3522,14 @@ __metadata:
languageName: node
linkType: hard
"@octokit/plugin-paginate-rest@npm:^14.0.0":
version: 14.0.0
resolution: "@octokit/plugin-paginate-rest@npm:14.0.0"
"@octokit/plugin-paginate-rest@npm:^13.0.1":
version: 13.1.1
resolution: "@octokit/plugin-paginate-rest@npm:13.1.1"
dependencies:
"@octokit/types": "npm:^16.0.0"
"@octokit/types": "npm:^14.1.0"
peerDependencies:
"@octokit/core": ">=6"
checksum: 10/57ddd857528dad9c02431bc6254c2374c06057872cf9656a4a88b162ebe1c2bc9f34fbec360f2ccff72c940f29b120758ce14e8135bd027223d381eb1b8b6579
checksum: 10/26b9b7a233b77fff31d31469879a281e651417df86799387d6563446f037c9969061b833ab0698857c9797ea856f2ef7ed6970d8fb471239879c9298bbabe200
languageName: node
linkType: hard
@@ -3528,14 +3542,14 @@ __metadata:
languageName: node
linkType: hard
"@octokit/plugin-rest-endpoint-methods@npm:^17.0.0":
version: 17.0.0
resolution: "@octokit/plugin-rest-endpoint-methods@npm:17.0.0"
"@octokit/plugin-rest-endpoint-methods@npm:^16.0.0":
version: 16.1.0
resolution: "@octokit/plugin-rest-endpoint-methods@npm:16.1.0"
dependencies:
"@octokit/types": "npm:^16.0.0"
"@octokit/types": "npm:^15.0.0"
peerDependencies:
"@octokit/core": ">=6"
checksum: 10/e9d9ad4d9755cc7fb82fdcbfa870ddea8a432180f0f76c8469095557fd1e26f8caea8cae58401209be17c4f3d8cc48c0e16a3643e37e48f4d23c39e058bf2c55
checksum: 10/9b62d1ddf3435d77cbf11f36abf951b755cf2033dfd5f1875d35f97ad4ab9391e7d0e008ae5b813b7fe0b6c537ab38240b4911c61b53cf8fb8d9957a3d898e09
languageName: node
linkType: hard
@@ -3552,7 +3566,7 @@ __metadata:
languageName: node
linkType: hard
"@octokit/request-error@npm:^7.0.2":
"@octokit/request-error@npm:^7.0.0, @octokit/request-error@npm:^7.0.2":
version: 7.0.2
resolution: "@octokit/request-error@npm:7.0.2"
dependencies:
@@ -3561,7 +3575,7 @@ __metadata:
languageName: node
linkType: hard
"@octokit/request@npm:^10.0.6":
"@octokit/request@npm:^10.0.2, @octokit/request@npm:^10.0.6":
version: 10.0.6
resolution: "@octokit/request@npm:10.0.6"
dependencies:
@@ -3574,15 +3588,33 @@ __metadata:
languageName: node
linkType: hard
"@octokit/rest@npm:22.0.1":
version: 22.0.1
resolution: "@octokit/rest@npm:22.0.1"
"@octokit/rest@npm:22.0.0":
version: 22.0.0
resolution: "@octokit/rest@npm:22.0.0"
dependencies:
"@octokit/core": "npm:^7.0.6"
"@octokit/plugin-paginate-rest": "npm:^14.0.0"
"@octokit/core": "npm:^7.0.2"
"@octokit/plugin-paginate-rest": "npm:^13.0.1"
"@octokit/plugin-request-log": "npm:^6.0.0"
"@octokit/plugin-rest-endpoint-methods": "npm:^17.0.0"
checksum: 10/ec2e94cfa8766716faeb3ca18527d9af746482d35aaa6e4265a30cb669ae3f31f4ebb6235edebe5ae62bc2cec2b8e88902584f698df2e7cabac3a15fd27da665
"@octokit/plugin-rest-endpoint-methods": "npm:^16.0.0"
checksum: 10/d2b80fefd6aed307cb728980cb1d94cb484d48fabf0055198664287a7fb50544d312b005e4fb8dec2a6e97a153ec0ad7654d62f59898e1077a4cfba64e6d5c3e
languageName: node
linkType: hard
"@octokit/types@npm:^14.0.0, @octokit/types@npm:^14.1.0":
version: 14.1.0
resolution: "@octokit/types@npm:14.1.0"
dependencies:
"@octokit/openapi-types": "npm:^25.1.0"
checksum: 10/ea5549ca6176bd1184427141a77bca88c68f07d252d3ea1db7f9b58ec16b66391218a75a99927efb1e36a2cb00e8ed37a79b71fdc95a1117a9982516156fd997
languageName: node
linkType: hard
"@octokit/types@npm:^15.0.0":
version: 15.0.0
resolution: "@octokit/types@npm:15.0.0"
dependencies:
"@octokit/openapi-types": "npm:^26.0.0"
checksum: 10/c9207551ea0a56f7b740d7fed7f0eb3801abfc39b67ece6f914ffc41484879342cf909300c601ccb1ea5735737491f539c9a1464ddbc35acad323f6d78b6f17f
languageName: node
linkType: hard
@@ -8939,10 +8971,10 @@ __metadata:
languageName: node
linkType: hard
"globals@npm:16.5.0":
version: 16.5.0
resolution: "globals@npm:16.5.0"
checksum: 10/f9e8a2a13f50222c127030a619e283e7bbfe32966316bdde0715af1d15a7e40cb9c24ff52cad59671f97762ed8b515353c2f8674f560c63d9385f19ee26735a6
"globals@npm:16.4.0":
version: 16.4.0
resolution: "globals@npm:16.4.0"
checksum: 10/1627a9f42fb4c82d7af6a0c8b6cd616e00110908304d5f1ddcdf325998f3aed45a4b29d8a1e47870f328817805263e31e4f1673f00022b9c2b210552767921cf
languageName: node
linkType: hard
@@ -9184,10 +9216,10 @@ __metadata:
languageName: node
linkType: hard
"hls.js@npm:1.6.14":
version: 1.6.14
resolution: "hls.js@npm:1.6.14"
checksum: 10/56eedf163912abf72bd5ca0eac44dbaa442cff59bb8d1ff3303614bf18d21dfa3ec7b85db5b71449986c52c63b7efb6bd5392b048bf22c61549f71a001fa94cd
"hls.js@npm:1.6.13":
version: 1.6.13
resolution: "hls.js@npm:1.6.13"
checksum: 10/4de045fddbeb6edc3859021ff60b268a642aa87e83347a3a764e53299b616d57f36eecef0b91eab240ffde84f75d1ab5f7cc0916a929de265707d0f0b27d1c71
languageName: node
linkType: hard
@@ -9261,7 +9293,7 @@ __metadata:
"@mdi/svg": "npm:7.4.47"
"@octokit/auth-oauth-device": "npm:8.0.3"
"@octokit/plugin-retry": "npm:8.0.3"
"@octokit/rest": "npm:22.0.1"
"@octokit/rest": "npm:22.0.0"
"@replit/codemirror-indentation-markers": "npm:6.5.3"
"@rsdoctor/rspack-plugin": "npm:1.3.7"
"@rspack/core": "npm:1.6.0"
@@ -9331,7 +9363,7 @@ __metadata:
gulp-json-transform: "npm:0.5.0"
gulp-rename: "npm:2.1.0"
gulp-zopfli-green: "npm:6.0.2"
hls.js: "npm:1.6.14"
hls.js: "npm:1.6.13"
home-assistant-js-websocket: "npm:9.5.0"
html-minifier-terser: "npm:7.2.0"
husky: "npm:9.1.7"