Compare commits

..

31 Commits

Author SHA1 Message Date
Petar Petrov
c472010ac5 Add support for downstream water meters in energy dashboard 2025-11-06 13:53:37 +02:00
Wendelin
cb0f59b26d Fix floor details area picker (#27827) 2025-11-06 12:42:30 +02:00
Paul Bottein
c89fc35578 Fix OHF logo theme (#27830) 2025-11-06 12:39:52 +02:00
Timothy
f03cd9c239 Add Add entity to feature for external_app (#26346)
* Add Add entity to feature for external_app

* Update icon from plus to plusboxmultiple

* Apply suggestion on the name

* Add missing shouldHandleRequestSelectedEvent that caused duplicate

* WIP

* Rework the logic to match the agreed design

* Rename property

* Apply PR comments

* Apply prettier

* Merge MessageWithAnswer

* Apply PR comments
2025-11-06 08:25:05 +00:00
karwosts
19a4e37933 Fix incorrect unit displayed in energy grid flow settings (#27822) 2025-11-06 08:46:19 +02:00
Bram Kragten
76514babd5 Fix landing page build (#27817) 2025-11-05 16:13:48 +01:00
Timothy
b6abbdafb8 All external config properties could be undefined (#27803)
All external config attributes could be undefined
2025-11-05 16:57:51 +02:00
Wendelin
d35e6c0092 Add fallback icon for domain template (#27814) 2025-11-05 15:14:41 +01:00
Wendelin
5c2ee54dec Fix target picker with empty sections (#27813) 2025-11-05 13:56:58 +00:00
Wendelin
1dfca76c81 Fix assist conversation language picker (#27764) 2025-11-05 14:48:18 +01:00
Wendelin
fd7f028fbf Change add trigger/condition/action dialog title (#27811)
Change add dialog title
2025-11-05 15:31:25 +02:00
Wendelin
3f7283b1af Add trigger/condition/action dialog - Show device group always on top (#27812)
add automation element dialog Device always on top
2025-11-05 15:27:53 +02:00
Paul Bottein
d35323ac52 Fix target picker in logbook card editor (#27804)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2025-11-05 11:57:34 +00:00
Wendelin
06475382e8 Fix auth language picker styles (#27805) 2025-11-05 10:54:36 +01:00
Wendelin
b60dd7f15d Add condition/action dialog: blocks title (#27801) 2025-11-05 10:47:46 +01:00
Wendelin
b77e65fabd Add trigger/condition/action dialog: fix empty elements in search results (#27802) 2025-11-05 10:47:20 +01:00
Wendelin
cea691a04e Fix target picker in card editor (#27800) 2025-11-05 10:03:56 +01:00
Jan-Philipp Benecke
50df2a34cd Fix z-index for target picker item row icon (#27798) 2025-11-05 08:33:11 +01:00
Aidan Timson
e6c0a84994 Add hide background option to iframe card (#27792)
* Add hide background option to iframe card

* Fix

* Add helper
2025-11-05 08:07:43 +02:00
Bram Kragten
b03fa4bdc5 Handle unknown items in target picker (#27795)
* Handle unknown items in target picker

* Update ha-target-picker-item-row.ts

* update colors

* fallback to domain icons
2025-11-04 18:02:56 +01:00
Paul Bottein
058cecc124 Auto refresh summary dashboard when registries changed (#27794) 2025-11-04 16:41:27 +00:00
Paul Bottein
a5f058a7eb Rename safety panel to security panel (#27796) 2025-11-04 17:23:06 +01:00
Paul Bottein
655c2ff3c2 Don't show summary card if summary dashboards are empty (#27788)
Don't show summary card if summary dashboard are empty
2025-11-04 13:40:53 +02:00
Tobias Bieniek
e1b0a3e737 Hide media players summary when no entities exist (#27642) 2025-11-04 11:44:12 +01:00
Paul Bottein
d59d436080 Display entities without area in summary dashboard (#27777)
* Add support for no area, no floor and no device in entity filter

* Display entities without area in summary dashboard
2025-11-04 12:10:18 +02:00
Wendelin
0fe0bf12f2 Fix-labels-yaml-helper (#27776)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-11-04 09:01:36 +00:00
Paul Bottein
f5a3877f47 Fix tooltip hide delay (#27786) 2025-11-04 09:45:26 +01:00
renovate[bot]
31d04f5338 Update dependency globals to v16.5.0 (#27785)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 08:49:20 +02:00
karwosts
4f7d223aa7 Fix sankey with external statistics devices (#27784) 2025-11-04 08:22:36 +02:00
renovate[bot]
484c60073d Update dependency @octokit/rest to v22.0.1 (#27779)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 06:33:20 +01:00
renovate[bot]
0e1ab1a60c Update dependency hls.js to v1.6.14 (#27780)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-11-04 06:33:05 +01:00
71 changed files with 2106 additions and 944 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 }) =>
module.exports.emptyPackages = ({ isHassioBuild, isLandingPageBuild }) =>
[
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 &&
(isHassioBuild || isLandingPageBuild) &&
require.resolve(
path.resolve(paths.root_dir, "src/components/ha-icon.ts")
),
isHassioBuild &&
(isHassioBuild || isLandingPageBuild) &&
require.resolve(
path.resolve(paths.root_dir, "src/components/ha-icon-picker.ts")
),
@@ -337,6 +337,7 @@ module.exports.config = {
publicPath: publicPath(latestBuild),
isProdBuild,
latestBuild,
isLandingPageBuild: true,
};
},
};

View File

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

View File

@@ -84,6 +84,7 @@ export const mockEnergy = (hass: MockHomeAssistant) => {
stat_consumption: "sensor.energy_boiler",
},
],
device_consumption_water: [],
})
);
hass.mockWS(

View File

@@ -1,55 +0,0 @@
---
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

@@ -1,133 +0,0 @@
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,22 +1,25 @@
import "@material/mwc-linear-progress";
import { type PropertyValues, css, html, nothing } from "lit";
import { mdiOpenInNew } from "@mdi/js";
import { css, html, nothing, type PropertyValues } 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 { haStyle } from "../../src/resources/styles";
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 "../../src/components/ha-svg-icon";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import { LandingPageBaseElement } from "./landing-page-base-element";
import "../../src/onboarding/onboarding-welcome-links";
import { onBoardingStyles } from "../../src/onboarding/styles";
import { haStyle } from "../../src/resources/styles";
import "./components/landing-page-logs";
import "./components/landing-page-network";
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;
@@ -94,16 +97,21 @@ class HaLandingPage extends LandingPageBaseElement {
<ha-language-picker
.value=${this.language}
.label=${""}
button-style
native-name
@value-changed=${this._languageChanged}
inline-arrow
></ha-language-picker>
<a
<ha-button
appearance="plain"
variant="neutral"
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>
`;
}
@@ -218,26 +226,8 @@ class HaLandingPage extends LandingPageBaseElement {
ha-alert p {
text-align: unset;
}
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;
.footer ha-svg-icon {
--mdc-icon-size: var(--ha-space-5);
}
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.13",
"hls.js": "1.6.14",
"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.0",
"@octokit/rest": "22.0.1",
"@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.4.0",
"globals": "16.5.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,4 +1,5 @@
/* 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";
@@ -6,6 +7,8 @@ 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";
@@ -133,25 +136,8 @@ export class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
justify-content: space-between;
align-items: center;
}
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;
.footer ha-svg-icon {
--mdc-icon-size: var(--ha-space-5);
}
h1 {
font-size: var(--ha-font-size-3xl);
@@ -205,16 +191,21 @@ 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>
<a
<ha-button
appearance="plain"
variant="neutral"
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 | string[];
area?: string | string[];
floor?: string | string[];
device?: string | null | (string | null)[];
area?: string | null | (string | null)[];
floor?: string | null | (string | null)[];
label?: string | string[];
entity_category?: EntityCategory | EntityCategory[];
hidden_platform?: string | string[];
@@ -19,6 +19,18 @@ 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
@@ -29,11 +41,9 @@ export const generateEntityFilter = (
const deviceClasses = filter.device_class
? new Set(ensureArray(filter.device_class))
: undefined;
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 floors = normalizeFilterArray(filter.floor);
const areas = normalizeFilterArray(filter.area);
const devices = normalizeFilterArray(filter.device);
const entityCategories = filter.entity_category
? new Set(ensureArray(filter.entity_category))
: undefined;
@@ -73,23 +83,20 @@ export const generateEntityFilter = (
}
if (floors) {
if (!floor || !floors.has(floor.floor_id)) {
const floorId = floor?.floor_id ?? null;
if (!floors.has(floorId)) {
return false;
}
}
if (areas) {
if (!area) {
return false;
}
if (!areas.has(area.area_id)) {
const areaId = area?.area_id ?? null;
if (!areas.has(areaId)) {
return false;
}
}
if (devices) {
if (!device) {
return false;
}
if (!devices.has(device.id)) {
const deviceId = device?.id ?? null;
if (!devices.has(deviceId)) {
return false;
}
}

View File

@@ -87,6 +87,8 @@ export class HaAreaPicker extends LitElement {
@property({ type: Boolean }) public required = false;
@property({ attribute: "add-button-label" }) public addButtonLabel?: string;
@query("ha-generic-picker") private _picker?: HaGenericPicker;
public async open() {
@@ -375,6 +377,7 @@ export class HaAreaPicker extends LitElement {
.getItems=${this._getItems}
.getAdditionalItems=${this._getAdditionalItems}
.valueRenderer=${valueRenderer}
.addButtonLabel=${this.addButtonLabel}
@value-changed=${this._valueChanged}
>
</ha-generic-picker>

View File

@@ -1,33 +0,0 @@
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

@@ -1,45 +0,0 @@
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,7 +10,6 @@ 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,6 +1,7 @@
import { mdiMenuDown } from "@mdi/js";
import type { PropertyValues } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { formatLanguageCode } from "../common/language/format_language";
@@ -8,10 +9,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 "./ha-list-item";
import type { HaGenericPicker } from "./ha-generic-picker";
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
import "./ha-select";
export const getLanguageOptions = (
languages: string[],
@@ -75,6 +76,9 @@ 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 })
@@ -82,6 +86,8 @@ 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();
@@ -101,12 +107,13 @@ export class HaLanguagePicker extends LitElement {
this.hass?.locale
);
private _valueRenderer = (value) => {
const language = this._getItems().find(
(lang) => lang.id === value
)?.primary;
return html`<span slot="headline">${language ?? value}</span> `;
};
private _getLanguageName = (lang?: string) =>
this._getItems().find((language) => language.id === lang)?.primary;
private _valueRenderer = (value) =>
html`<span slot="headline"
>${this._getLanguageName(value) ?? value}</span
> `;
protected render() {
const value =
@@ -130,10 +137,28 @@ export class HaLanguagePicker extends LitElement {
.getItems=${this._getItems}
@value-changed=${this._changed}
hide-clear-icon
></ha-generic-picker>
>
${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>
`;
}
private _openPicker(ev: Event) {
ev.stopPropagation();
this.genericPicker.open();
}
static styles = css`
ha-generic-picker {
width: 100%;

View File

@@ -87,166 +87,208 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
protected render() {
if (this.addOnTop) {
return html` ${this._renderChips()} ${this._renderItems()} `;
return html` ${this._renderPicker()} ${this._renderItems()} `;
}
return html` ${this._renderItems()} ${this._renderChips()} `;
return html` ${this._renderItems()} ${this._renderPicker()} `;
}
private _renderValueChips() {
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>`;
}
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)
: [];
private _renderValueGroups() {
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 (
!this.value?.floor_id &&
!this.value?.area_id &&
!this.value?.device_id &&
!this.value?.entity_id &&
!this.value?.label_id
!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>
`;
}
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)
: [];
if (
!entityIds.length &&
!deviceIds.length &&
!areaIds.length &&
!floorIds.length &&
!labelIds.length
) {
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 _renderChips() {
private _renderPicker() {
return html`
<div class="add-target-wrapper">
<ha-button
@@ -347,7 +389,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
this._pickerFilter = filter;
};
private _hidePicker() {
private _hidePicker(ev) {
ev.stopPropagation();
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 = 400;
@property({ attribute: "hide-delay", type: Number }) hideDelay = 150;
static get styles(): CSSResultGroup {
return [

View File

@@ -6,6 +6,7 @@ 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";
@@ -19,9 +20,12 @@ 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 {
@@ -111,10 +115,10 @@ export class HaTargetPickerItemRow extends LitElement {
}
protected render() {
const { name, context, iconPath, fallbackIconPath, stateObject } =
const { name, context, iconPath, fallbackIconPath, stateObject, notFound } =
this._itemData(this.type, this.itemId);
const showEntities = this.type !== "entity";
const showEntities = this.type !== "entity" && !notFound;
const entries = this.parentEntries || this._entries;
@@ -128,7 +132,7 @@ export class HaTargetPickerItemRow extends LitElement {
}
return html`
<ha-md-list-item type="text">
<ha-md-list-item type="text" class=${notFound ? "error" : ""}>
<div class="icon" slot="start">
${this.subEntry
? html`
@@ -148,11 +152,15 @@ export class HaTargetPickerItemRow extends LitElement {
/>`
: fallbackIconPath
? html`<ha-svg-icon .path=${fallbackIconPath}></ha-svg-icon>`
: stateObject
: this.type === "entity"
? html`
<ha-state-icon
.hass=${this.hass}
.stateObj=${stateObject}
.stateObj=${stateObject ||
({
entity_id: this.itemId,
attributes: {},
} as HassEntity)}
>
</ha-state-icon>
`
@@ -160,8 +168,14 @@ export class HaTargetPickerItemRow extends LitElement {
</div>
<div slot="headline">${name}</div>
${context && !this.hideContext
? html`<span slot="supporting-text">${context}</span>`
${notFound || (context && !this.hideContext)
? html`<span slot="supporting-text"
>${notFound
? this.hass.localize(
`ui.components.target-picker.${this.type}_not_found`
)
: context}</span
>`
: nothing}
${this._domainName && this.subEntry
? html`<span slot="supporting-text" class="domain"
@@ -474,26 +488,28 @@ export class HaTargetPickerItemRow extends LitElement {
private _itemData = memoizeOne((type: TargetType, item: string) => {
if (type === "floor") {
const floor = this.hass.floors?.[item];
const floor: FloorRegistryEntry | undefined = this.hass.floors?.[item];
return {
name: floor?.name || item,
iconPath: floor?.icon,
fallbackIconPath: floor ? floorDefaultIconPath(floor) : mdiHome,
notFound: !floor,
};
}
if (type === "area") {
const area = this.hass.areas?.[item];
const area: AreaRegistryEntry | undefined = 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 = this.hass.devices?.[item];
const device: DeviceRegistryEntry | undefined = this.hass.devices?.[item];
if (device.primary_config_entry) {
if (device?.primary_config_entry) {
this._getDeviceDomain(device.primary_config_entry);
}
@@ -501,24 +517,25 @@ 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 = 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 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 deviceName = device ? computeDeviceName(device) : undefined;
const areaName = area ? computeAreaName(area) : undefined;
const context = [areaName, entityName ? deviceName : undefined]
@@ -528,15 +545,19 @@ export class HaTargetPickerItemRow extends LitElement {
name: entityName || deviceName || item,
context,
stateObject,
notFound: !stateObject,
};
}
// type label
const label = this._labelRegistry.find((lab) => lab.label_id === item);
const label: LabelRegistryEntry | undefined = this._labelRegistry.find(
(lab) => lab.label_id === item
);
return {
name: label?.name || item,
iconPath: label?.icon,
fallbackIconPath: mdiLabel,
notFound: !label,
};
});
@@ -597,17 +618,27 @@ 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) {
if (!filterType && entities.length) {
// 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) {
if (!filterType && devices.length) {
// 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) {
if (!filterType && areasAndFloors.length) {
// 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) {
if (!filterType && labels.length) {
// show group title
items.push(
this.hass.localize("ui.components.target-picker.type.labels")

View File

@@ -191,6 +191,7 @@ export type EnergySource =
export interface EnergyPreferences {
energy_sources: EnergySource[];
device_consumption: DeviceConsumptionEnergyPreference[];
device_consumption_water: DeviceConsumptionEnergyPreference[];
}
export interface EnergyInfo {
@@ -347,6 +348,11 @@ export const getReferencedStatisticIds = (
if (!(includeTypes && !includeTypes.includes("device"))) {
statIDs.push(...prefs.device_consumption.map((d) => d.stat_consumption));
}
if (!(includeTypes && !includeTypes.includes("water"))) {
statIDs.push(
...prefs.device_consumption_water.map((d) => d.stat_consumption)
);
}
return statIDs;
};

View File

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

View File

@@ -0,0 +1,152 @@
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,6 +8,7 @@ import {
mdiPencil,
mdiPencilOff,
mdiPencilOutline,
mdiPlusBoxMultipleOutline,
mdiTransitConnectionVariant,
} from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
@@ -60,6 +61,7 @@ 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";
@@ -73,7 +75,7 @@ export interface MoreInfoDialogParams {
data?: Record<string, any>;
}
type View = "info" | "history" | "settings" | "related";
type View = "info" | "history" | "settings" | "related" | "add_to";
interface ChildView {
viewTag: string;
@@ -194,6 +196,10 @@ 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
@@ -295,6 +301,11 @@ 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");
@@ -521,6 +532,22 @@ 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}
@@ -613,7 +640,14 @@ export class MoreInfoDialog extends LitElement {
: "entity"}
></ha-related-items>
`
: nothing
: this._currView === "add_to"
? html`
<ha-more-info-add-to
.hass=${this.hass}
.entityId=${entityId}
></ha-more-info-add-to>
`
: nothing
)}
</div>
`

View File

@@ -36,6 +36,13 @@ 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: {
@@ -75,6 +82,10 @@ interface EMOutgoingMessageWithAnswer {
request: EMOutgoingMessageConfigGet;
response: ExternalConfig;
};
"entity/add_to/get_actions": {
request: EMOutgoingMessageEntityAddToGetActions;
response: ExternalEntityAddToActions;
};
}
interface EMOutgoingMessageExoplayerPlayHLS extends EMMessage {
@@ -157,6 +168,14 @@ 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
@@ -177,7 +196,8 @@ type EMOutgoingMessageWithoutAnswer =
| EMOutgoingMessageThemeUpdate
| EMOutgoingMessageThreadStoreInPlatformKeychain
| EMOutgoingMessageImprovScan
| EMOutgoingMessageImprovConfigureDevice;
| EMOutgoingMessageImprovConfigureDevice
| EMOutgoingMessageAddEntityTo;
export interface EMIncomingMessageRestart {
id: number;
@@ -293,18 +313,31 @@ 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;
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[];
}
export class ExternalMessaging {

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"),
safety: () => import("../panels/safety/ha-panel-safety"),
security: () => import("../panels/security/ha-panel-security"),
climate: () => import("../panels/climate/ha-panel-climate"),
};

View File

@@ -1,24 +1,23 @@
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } 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 { LovelaceConfig } from "../../data/lovelace/config/types";
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view";
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_CONFIG: LovelaceConfig = {
views: [
{
strategy: {
type: "climate",
},
},
],
const CLIMATE_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = {
strategy: {
type: "climate",
},
};
@customElement("ha-panel-climate")
@@ -33,65 +32,119 @@ 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?.locale !== this.hass.locale) {
if (oldHass && oldHass.localize !== this.hass.localize) {
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(): TemplateResult {
protected render() {
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>
<hui-view-container .hass=${this.hass}>
<hui-view
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.index=${this._viewIndex}
></hui-view>
${
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>
`;
}
private _setLovelace() {
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;
}
this._lovelace = {
config: CLIMATE_LOVELACE_CONFIG,
rawConfig: CLIMATE_LOVELACE_CONFIG,
config: config,
rawConfig: rawConfig,
editMode: false,
urlPath: "climate",
mode: "generated",

View File

@@ -115,6 +115,24 @@ 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(
@@ -190,10 +208,33 @@ 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

@@ -8,24 +8,24 @@ import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/chips/ha-chip-set";
import "../../../components/chips/ha-input-chip";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-aliases-editor";
import "../../../components/ha-area-picker";
import "../../../components/ha-button";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-icon-picker";
import "../../../components/ha-picture-upload";
import "../../../components/ha-settings-row";
import "../../../components/ha-svg-icon";
import "../../../components/ha-textfield";
import "../../../components/ha-area-picker";
import { updateAreaRegistryEntry } from "../../../data/area_registry";
import type {
FloorRegistryEntry,
FloorRegistryEntryMutableParams,
} from "../../../data/floor_registry";
import { haStyle, haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import type { FloorRegistryDetailDialogParams } from "./show-dialog-floor-registry-detail";
import { showAreaRegistryDetailDialog } from "./show-dialog-area-registry-detail";
import { updateAreaRegistryEntry } from "../../../data/area_registry";
import type { FloorRegistryDetailDialogParams } from "./show-dialog-floor-registry-detail";
class DialogFloorDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -168,11 +168,6 @@ class DialogFloorDetail extends LitElement {
)}
</h3>
<p class="description">
${this.hass.localize(
"ui.panel.config.floors.editor.areas_description"
)}
</p>
${areas.length
? html`<ha-chip-set>
${repeat(
@@ -197,13 +192,17 @@ class DialogFloorDetail extends LitElement {
</ha-input-chip>`
)}
</ha-chip-set>`
: nothing}
: html`<p class="description">
${this.hass.localize(
"ui.panel.config.floors.editor.areas_description"
)}
</p>`}
<ha-area-picker
no-add
.hass=${this.hass}
@value-changed=${this._addArea}
.excludeAreas=${areas.map((a) => a.area_id)}
.label=${this.hass.localize(
.addButtonLabel=${this.hass.localize(
"ui.panel.config.floors.editor.add_area"
)}
></ha-area-picker>

View File

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

View File

@@ -0,0 +1,249 @@
import {
mdiDelete,
mdiWater,
mdiDragHorizontalVariant,
mdiPencil,
mdiPlus,
} from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { repeat } from "lit/directives/repeat";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-card";
import "../../../../components/ha-button";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-sortable";
import "../../../../components/ha-svg-icon";
import type {
DeviceConsumptionEnergyPreference,
EnergyPreferences,
EnergyPreferencesValidation,
} from "../../../../data/energy";
import { saveEnergyPreferences } from "../../../../data/energy";
import type { StatisticsMetaData } from "../../../../data/recorder";
import { getStatisticLabel } from "../../../../data/recorder";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../../dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { documentationUrl } from "../../../../util/documentation-url";
import { showEnergySettingsDeviceWaterDialog } from "../dialogs/show-dialogs-energy";
import "./ha-energy-validation-result";
import { energyCardStyles } from "./styles";
@customElement("ha-energy-device-settings-water")
export class EnergyDeviceSettingsWater extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false })
public preferences!: EnergyPreferences;
@property({ attribute: false })
public statsMetadata?: Record<string, StatisticsMetaData>;
@property({ attribute: false })
public validationResult?: EnergyPreferencesValidation;
protected render(): TemplateResult {
return html`
<ha-card outlined>
<h1 class="card-header">
<ha-svg-icon .path=${mdiWater}></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.energy.device_consumption_water.title"
)}
</h1>
<div class="card-content">
<p>
${this.hass.localize(
"ui.panel.config.energy.device_consumption_water.sub"
)}
<a
target="_blank"
rel="noopener noreferrer"
href=${documentationUrl(
this.hass,
"/docs/energy/water/#individual-devices"
)}
>${this.hass.localize(
"ui.panel.config.energy.device_consumption_water.learn_more"
)}</a
>
</p>
<h3>
${this.hass.localize(
"ui.panel.config.energy.device_consumption_water.devices"
)}
</h3>
<ha-sortable handle-selector=".handle" @item-moved=${this._itemMoved}>
<div class="devices">
${repeat(
this.preferences.device_consumption_water,
(device) => device.stat_consumption,
(device) => html`
<div class="row" .device=${device}>
<div class="handle">
<ha-svg-icon
.path=${mdiDragHorizontalVariant}
></ha-svg-icon>
</div>
<span class="content"
>${device.name ||
getStatisticLabel(
this.hass,
device.stat_consumption,
this.statsMetadata?.[device.stat_consumption]
)}</span
>
<ha-icon-button
.label=${this.hass.localize("ui.common.edit")}
@click=${this._editDevice}
.path=${mdiPencil}
></ha-icon-button>
<ha-icon-button
.label=${this.hass.localize("ui.common.delete")}
@click=${this._deleteDevice}
.device=${device}
.path=${mdiDelete}
></ha-icon-button>
</div>
`
)}
</div>
</ha-sortable>
<div class="row">
<ha-svg-icon .path=${mdiWater}></ha-svg-icon>
<ha-button
@click=${this._addDevice}
appearance="filled"
size="small"
>
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon
>${this.hass.localize(
"ui.panel.config.energy.device_consumption_water.add_device"
)}</ha-button
>
</div>
</div>
</ha-card>
`;
}
private _itemMoved(ev: CustomEvent): void {
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;
const devices = this.preferences.device_consumption_water.concat();
const device = devices.splice(oldIndex, 1)[0];
devices.splice(newIndex, 0, device);
const newPrefs = {
...this.preferences,
device_consumption_water: devices,
};
fireEvent(this, "value-changed", { value: newPrefs });
this._savePreferences(newPrefs);
}
private _editDevice(ev) {
const origDevice: DeviceConsumptionEnergyPreference =
ev.currentTarget.closest(".row").device;
showEnergySettingsDeviceWaterDialog(this, {
statsMetadata: this.statsMetadata,
device: { ...origDevice },
device_consumptions: this.preferences
.device_consumption_water as DeviceConsumptionEnergyPreference[],
saveCallback: async (newDevice) => {
const newPrefs = {
...this.preferences,
device_consumption_water:
this.preferences.device_consumption_water.map((d) =>
d === origDevice ? newDevice : d
),
};
this._sanitizeParents(newPrefs);
await this._savePreferences(newPrefs);
},
});
}
private _addDevice() {
showEnergySettingsDeviceWaterDialog(this, {
statsMetadata: this.statsMetadata,
device_consumptions: this.preferences
.device_consumption_water as DeviceConsumptionEnergyPreference[],
saveCallback: async (device) => {
await this._savePreferences({
...this.preferences,
device_consumption_water:
this.preferences.device_consumption_water.concat(device),
});
},
});
}
private _sanitizeParents(prefs: EnergyPreferences) {
const statIds = prefs.device_consumption_water.map(
(d) => d.stat_consumption
);
prefs.device_consumption_water.forEach((d) => {
if (d.included_in_stat && !statIds.includes(d.included_in_stat)) {
delete d.included_in_stat;
}
});
}
private async _deleteDevice(ev) {
const deviceToDelete: DeviceConsumptionEnergyPreference =
ev.currentTarget.device;
if (
!(await showConfirmationDialog(this, {
title: this.hass.localize("ui.panel.config.energy.delete_source"),
}))
) {
return;
}
try {
const newPrefs = {
...this.preferences,
device_consumption_water:
this.preferences.device_consumption_water.filter(
(device) => device !== deviceToDelete
),
};
this._sanitizeParents(newPrefs);
await this._savePreferences(newPrefs);
} catch (err: any) {
showAlertDialog(this, { title: `Failed to save config: ${err.message}` });
}
}
private async _savePreferences(preferences: EnergyPreferences) {
const result = await saveEnergyPreferences(this.hass, preferences);
fireEvent(this, "value-changed", { value: result });
}
static get styles(): CSSResultGroup {
return [
haStyle,
energyCardStyles,
css`
.handle {
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-energy-device-settings-water": EnergyDeviceSettingsWater;
}
}

View File

@@ -0,0 +1,268 @@
import { mdiWater } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/entity/ha-entity-picker";
import "../../../../components/entity/ha-statistic-picker";
import "../../../../components/ha-dialog";
import "../../../../components/ha-radio";
import "../../../../components/ha-button";
import "../../../../components/ha-select";
import "../../../../components/ha-list-item";
import type { DeviceConsumptionEnergyPreference } from "../../../../data/energy";
import { energyStatisticHelpUrl } from "../../../../data/energy";
import { getStatisticLabel } from "../../../../data/recorder";
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import type { EnergySettingsDeviceWaterDialogParams } from "./show-dialogs-energy";
const volumeUnitClasses = ["volume"];
@customElement("dialog-energy-device-settings-water")
export class DialogEnergyDeviceSettingsWater
extends LitElement
implements HassDialog<EnergySettingsDeviceWaterDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: EnergySettingsDeviceWaterDialogParams;
@state() private _device?: DeviceConsumptionEnergyPreference;
@state() private _volume_units?: string[];
@state() private _error?: string;
private _excludeList?: string[];
private _possibleParents: DeviceConsumptionEnergyPreference[] = [];
public async showDialog(
params: EnergySettingsDeviceWaterDialogParams
): Promise<void> {
this._params = params;
this._device = this._params.device;
this._computePossibleParents();
this._volume_units = (
await getSensorDeviceClassConvertibleUnits(this.hass, "water")
).units;
this._excludeList = this._params.device_consumptions
.map((entry) => entry.stat_consumption)
.filter((id) => id !== this._device?.stat_consumption);
}
private _computePossibleParents() {
if (!this._device || !this._params) {
this._possibleParents = [];
return;
}
const children: string[] = [];
const devices = this._params.device_consumptions;
function getChildren(stat) {
devices.forEach((d) => {
if (d.included_in_stat === stat) {
children.push(d.stat_consumption);
getChildren(d.stat_consumption);
}
});
}
getChildren(this._device.stat_consumption);
this._possibleParents = this._params.device_consumptions.filter(
(d) =>
d.stat_consumption !== this._device!.stat_consumption &&
d.stat_consumption !== this._params?.device?.stat_consumption &&
!children.includes(d.stat_consumption)
);
}
public closeDialog() {
this._params = undefined;
this._device = undefined;
this._error = undefined;
this._excludeList = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
return true;
}
protected render() {
if (!this._params) {
return nothing;
}
const pickableUnit = this._volume_units?.join(", ") || "";
return html`
<ha-dialog
open
.heading=${html`<ha-svg-icon
.path=${mdiWater}
style="--mdc-icon-size: 32px;"
></ha-svg-icon>
${this.hass.localize(
"ui.panel.config.energy.device_consumption_water.dialog.header"
)}`}
@closed=${this.closeDialog}
>
${this._error ? html`<p class="error">${this._error}</p>` : ""}
<div>
${this.hass.localize(
"ui.panel.config.energy.device_consumption_water.dialog.selected_stat_intro",
{ unit: pickableUnit }
)}
</div>
<ha-statistic-picker
.hass=${this.hass}
.helpMissingEntityUrl=${energyStatisticHelpUrl}
.includeUnitClass=${volumeUnitClasses}
.value=${this._device?.stat_consumption}
.label=${this.hass.localize(
"ui.panel.config.energy.device_consumption_water.dialog.device_consumption_water"
)}
.excludeStatistics=${this._excludeList}
@value-changed=${this._statisticChanged}
dialogInitialFocus
></ha-statistic-picker>
<ha-textfield
.label=${this.hass.localize(
"ui.panel.config.energy.device_consumption_water.dialog.display_name"
)}
type="text"
.disabled=${!this._device}
.value=${this._device?.name || ""}
.placeholder=${this._device
? getStatisticLabel(
this.hass,
this._device.stat_consumption,
this._params?.statsMetadata?.[this._device.stat_consumption]
)
: ""}
@input=${this._nameChanged}
>
</ha-textfield>
<ha-select
.label=${this.hass.localize(
"ui.panel.config.energy.device_consumption_water.dialog.included_in_device"
)}
.value=${this._device?.included_in_stat || ""}
.helper=${this.hass.localize(
"ui.panel.config.energy.device_consumption_water.dialog.included_in_device_helper"
)}
.disabled=${!this._device}
@selected=${this._parentSelected}
@closed=${stopPropagation}
fixedMenuPosition
naturalMenuWidth
clearable
>
${!this._possibleParents.length
? html`
<ha-list-item disabled value="-"
>${this.hass.localize(
"ui.panel.config.energy.device_consumption_water.dialog.no_upstream_devices"
)}</ha-list-item
>
`
: this._possibleParents.map(
(stat) => html`
<ha-list-item .value=${stat.stat_consumption}
>${stat.name ||
getStatisticLabel(
this.hass,
stat.stat_consumption,
this._params?.statsMetadata?.[stat.stat_consumption]
)}</ha-list-item
>
`
)}
</ha-select>
<ha-button
appearance="plain"
@click=${this.closeDialog}
slot="primaryAction"
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
@click=${this._save}
.disabled=${!this._device}
slot="primaryAction"
>
${this.hass.localize("ui.common.save")}
</ha-button>
</ha-dialog>
`;
}
private _statisticChanged(ev: CustomEvent<{ value: string }>) {
if (!ev.detail.value) {
this._device = undefined;
return;
}
this._device = { stat_consumption: ev.detail.value };
this._computePossibleParents();
}
private _nameChanged(ev) {
const newDevice = {
...this._device!,
name: ev.target!.value,
} as DeviceConsumptionEnergyPreference;
if (!newDevice.name) {
delete newDevice.name;
}
this._device = newDevice;
}
private _parentSelected(ev) {
const newDevice = {
...this._device!,
included_in_stat: ev.target!.value,
} as DeviceConsumptionEnergyPreference;
if (!newDevice.included_in_stat) {
delete newDevice.included_in_stat;
}
this._device = newDevice;
}
private async _save() {
try {
await this._params!.saveCallback(this._device!);
this.closeDialog();
} catch (err: any) {
this._error = err.message;
}
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-statistic-picker {
width: 100%;
}
ha-select {
margin-top: 16px;
width: 100%;
}
ha-textfield {
margin-top: 16px;
width: 100%;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-energy-device-settings-water": DialogEnergyDeviceSettingsWater;
}
}

View File

@@ -9,6 +9,7 @@ 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,
@@ -19,11 +20,7 @@ import {
emptyFlowToGridSourceEnergyPreference,
energyStatisticHelpUrl,
} from "../../../../data/energy";
import {
getDisplayUnit,
getStatisticMetadata,
isExternalStatistic,
} from "../../../../data/recorder";
import { isExternalStatistic } from "../../../../data/recorder";
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../resources/styles";
@@ -47,8 +44,6 @@ 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;
@@ -81,11 +76,6 @@ export class DialogEnergyGridFlowSettings
: "stat_energy_to"
];
this._pickedDisplayUnit = getDisplayUnit(
this.hass,
initialSourceId,
params.metadata
);
this._energy_units = (
await getSensorDeviceClassConvertibleUnits(this.hass, "energy")
).units;
@@ -103,7 +93,6 @@ 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 });
@@ -117,10 +106,6 @@ 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 =
@@ -246,9 +231,15 @@ 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`
)} ${unitPriceSensor ? ` (${unitPriceSensor})` : ""}`}
)}
.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>`}
@value-changed=${this._priceEntityChanged}
></ha-entity-picker>`
: ""}
@@ -341,16 +332,6 @@ 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

@@ -76,6 +76,13 @@ export interface EnergySettingsDeviceDialogParams {
saveCallback: (device: DeviceConsumptionEnergyPreference) => Promise<void>;
}
export interface EnergySettingsDeviceWaterDialogParams {
device?: DeviceConsumptionEnergyPreference;
device_consumptions: DeviceConsumptionEnergyPreference[];
statsMetadata?: Record<string, StatisticsMetaData>;
saveCallback: (device: DeviceConsumptionEnergyPreference) => Promise<void>;
}
export const showEnergySettingsDeviceDialog = (
element: HTMLElement,
dialogParams: EnergySettingsDeviceDialogParams
@@ -152,3 +159,14 @@ export const showEnergySettingsGridFlowToDialog = (
dialogParams: { ...dialogParams, direction: "to" },
});
};
export const showEnergySettingsDeviceWaterDialog = (
element: HTMLElement,
dialogParams: EnergySettingsDeviceWaterDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-energy-device-settings-water",
dialogImport: () => import("./dialog-energy-device-settings-water"),
dialogParams: dialogParams,
});
};

View File

@@ -22,6 +22,7 @@ import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route } from "../../../types";
import "../../../components/ha-alert";
import "./components/ha-energy-device-settings";
import "./components/ha-energy-device-settings-water";
import "./components/ha-energy-grid-settings";
import "./components/ha-energy-solar-settings";
import "./components/ha-energy-battery-settings";
@@ -32,6 +33,7 @@ import { fileDownload } from "../../../util/file_download";
const INITIAL_CONFIG: EnergyPreferences = {
energy_sources: [],
device_consumption: [],
device_consumption_water: [],
};
@customElement("ha-config-energy")
@@ -142,6 +144,13 @@ class HaConfigEnergy extends LitElement {
.validationResult=${this._validationResult}
@value-changed=${this._prefsChanged}
></ha-energy-device-settings>
<ha-energy-device-settings-water
.hass=${this.hass}
.preferences=${this._preferences!}
.statsMetadata=${this._statsMetadata}
.validationResult=${this._validationResult}
@value-changed=${this._prefsChanged}
></ha-energy-device-settings-water>
</div>
</hass-subpage>
`;

View File

@@ -4,6 +4,7 @@ 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";
@@ -22,7 +23,6 @@ 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,22 +72,28 @@ 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 }
)
: 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>
`}
: 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>
`}
<entity-registry-settings-editor
.hass=${this.hass}
.entry=${this.entry}
@@ -122,6 +128,9 @@ export class EntitySettingsHelperTab extends LitElement {
}
private _valueChanged(ev: CustomEvent): void {
if (this._item === null) {
return;
}
this._error = undefined;
this._item = ev.detail.value;
}
@@ -195,6 +204,10 @@ 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,6 +17,8 @@ class HaCounterForm extends LitElement {
@property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
private _item?: Partial<Counter>;
@state() private _name!: string;
@@ -82,6 +84,7 @@ class HaCounterForm extends LitElement {
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
@@ -91,6 +94,7 @@ 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}
@@ -100,6 +104,7 @@ class HaCounterForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.counter.minimum"
)}
.disabled=${this.disabled}
></ha-textfield>
<ha-textfield
.value=${this._maximum}
@@ -109,6 +114,7 @@ class HaCounterForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.counter.maximum"
)}
.disabled=${this.disabled}
></ha-textfield>
<ha-textfield
.value=${this._initial}
@@ -118,6 +124,7 @@ 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(
@@ -133,12 +140,14 @@ 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,6 +14,8 @@ class HaInputBooleanForm extends LitElement {
@property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
private _item?: InputBoolean;
@state() private _name!: string;
@@ -59,6 +61,7 @@ class HaInputBooleanForm extends LitElement {
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
@@ -68,6 +71,7 @@ class HaInputBooleanForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.disabled=${this.disabled}
></ha-icon-picker>
</div>
`;

View File

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

View File

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

View File

@@ -18,6 +18,8 @@ class HaInputNumberForm extends LitElement {
@property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
private _item?: Partial<InputNumber>;
@state() private _name!: string;
@@ -89,6 +91,7 @@ class HaInputNumberForm extends LitElement {
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
@@ -98,6 +101,7 @@ 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}
@@ -108,6 +112,7 @@ 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}
@@ -118,6 +123,7 @@ 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(
@@ -139,6 +145,7 @@ class HaInputNumberForm extends LitElement {
value="slider"
.checked=${this._mode === "slider"}
@change=${this._modeChanged}
.disabled=${this.disabled}
></ha-radio>
</ha-formfield>
<ha-formfield
@@ -151,6 +158,7 @@ class HaInputNumberForm extends LitElement {
value="box"
.checked=${this._mode === "box"}
@change=${this._modeChanged}
.disabled=${this.disabled}
></ha-radio>
</ha-formfield>
</div>
@@ -163,6 +171,7 @@ class HaInputNumberForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.input_number.step"
)}
.disabled=${this.disabled}
></ha-textfield>
<ha-textfield
@@ -172,6 +181,7 @@ 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,6 +23,8 @@ class HaInputSelectForm extends LitElement {
@property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
private _item?: InputSelect;
@state() private _name!: string;
@@ -86,6 +88,7 @@ class HaInputSelectForm extends LitElement {
)}
.configValue=${"name"}
@input=${this._valueChanged}
.disabled=${this.disabled}
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
@@ -95,13 +98,18 @@ 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">
<ha-sortable
@item-moved=${this._optionMoved}
handle-selector=".handle"
.disabled=${this.disabled}
>
<ha-list class="options">
${this._options.length
? repeat(
@@ -124,6 +132,7 @@ 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>
@@ -146,8 +155,13 @@ 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}
<ha-button
size="small"
appearance="plain"
@click=${this._addOption}
.disabled=${this.disabled}
>${this.hass!.localize(
"ui.dialogs.helper_settings.input_select.add"
)}</ha-button

View File

@@ -19,6 +19,8 @@ class HaInputTextForm extends LitElement {
@property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
private _item?: InputText;
@state() private _name!: string;
@@ -79,6 +81,7 @@ class HaInputTextForm extends LitElement {
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
@@ -88,6 +91,7 @@ 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(
@@ -105,6 +109,7 @@ 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}
@@ -129,6 +134,7 @@ class HaInputTextForm extends LitElement {
value="text"
.checked=${this._mode === "text"}
@change=${this._modeChanged}
.disabled=${this.disabled}
></ha-radio>
</ha-formfield>
<ha-formfield
@@ -141,6 +147,7 @@ class HaInputTextForm extends LitElement {
value="password"
.checked=${this._mode === "password"}
@change=${this._modeChanged}
.disabled=${this.disabled}
></ha-radio>
</ha-formfield>
</div>
@@ -154,6 +161,7 @@ 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,6 +43,8 @@ class HaScheduleForm extends LitElement {
@property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
@state() private _name!: string;
@state() private _icon!: string;
@@ -132,6 +134,7 @@ class HaScheduleForm extends LitElement {
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
@@ -141,8 +144,9 @@ class HaScheduleForm extends LitElement {
.label=${this.hass!.localize(
"ui.dialogs.helper_settings.generic.icon"
)}
.disabled=${this.disabled}
></ha-icon-picker>
<div id="calendar"></div>
${!this.disabled ? html`<div id="calendar"></div>` : nothing}
</div>
`;
}
@@ -175,7 +179,9 @@ class HaScheduleForm extends LitElement {
}
protected firstUpdated(): void {
this._setupCalendar();
if (!this.disabled) {
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,6 +20,8 @@ class HaTimerForm extends LitElement {
@property({ type: Boolean }) public new = false;
@property({ type: Boolean }) public disabled = false;
private _item?: Timer;
@state() private _name!: string;
@@ -77,6 +79,7 @@ class HaTimerForm extends LitElement {
"ui.dialogs.helper_settings.required_error_msg"
)}
dialogInitialFocus
.disabled=${this.disabled}
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
@@ -86,11 +89,13 @@ 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(
@@ -101,6 +106,7 @@ class HaTimerForm extends LitElement {
.configValue=${"restore"}
.checked=${this._restore}
@click=${this._toggleRestore}
.disabled=${this.disabled}
>
</ha-checkbox>
</ha-formfield>
@@ -130,6 +136,9 @@ 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

@@ -107,6 +107,8 @@ class HaConfigInfo extends LitElement {
const customUiList: { name: string; url: string; version: string }[] =
(window as any).CUSTOM_UI_LIST || [];
const isDark = this.hass.themes?.darkMode || false;
return html`
<hass-subpage
.hass=${this.hass}
@@ -186,7 +188,7 @@ class HaConfigInfo extends LitElement {
: nothing}
</ul>
</ha-card>
<ha-card outlined class="ohf">
<ha-card outlined class="ohf ${isDark ? "dark" : ""}">
<div>
${this.hass.localize("ui.panel.config.info.proud_part_of")}
</div>
@@ -346,6 +348,10 @@ class HaConfigInfo extends LitElement {
max-width: 250px;
}
.ohf.dark img {
color-scheme: dark;
}
.versions {
display: flex;
flex-direction: column;

View File

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

View File

@@ -109,15 +109,26 @@ export class AssistPipelineDetailConversation extends LitElement {
}
private _supportedLanguagesChanged(ev) {
if (ev.detail.value === "*") {
this._supportedLanguages = ev.detail.value;
if (
this._supportedLanguages === "*" ||
!this._supportedLanguages?.includes(
this.data?.conversation_language || ""
) ||
!this.data?.conversation_language
) {
// wait for update of conversation_engine
setTimeout(() => {
const value = { ...this.data };
value.conversation_language = "*";
if (this._supportedLanguages === "*") {
value.conversation_language = "*";
} else {
value.conversation_language = this._supportedLanguages?.[0] ?? null;
}
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}
.disabled=${this._submitting}
.loading=${this._submitting}
dialogInitialFocus
>
${this._params.pipeline?.id

View File

@@ -30,6 +30,7 @@ export class EnergySetupWizard extends LitElement implements LovelaceCard {
@state() private _preferences: EnergyPreferences = {
energy_sources: [],
device_consumption: [],
device_consumption_water: [],
};
public getCardSize() {

View File

@@ -1,24 +1,23 @@
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } 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 { LovelaceConfig } from "../../data/lovelace/config/types";
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view";
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_CONFIG: LovelaceConfig = {
views: [
{
strategy: {
type: "light",
},
},
],
const LIGHT_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = {
strategy: {
type: "light",
},
};
@customElement("ha-panel-light")
@@ -34,60 +33,118 @@ 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?.locale !== this.hass.locale) {
if (oldHass && oldHass.localize !== this.hass.localize) {
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(): TemplateResult {
protected render() {
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>
<hui-view-container .hass=${this.hass}>
<hui-view
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.index=${this._viewIndex}
></hui-view>
${
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>
`;
}
private _setLovelace() {
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;
}
this._lovelace = {
config: LIGHT_LOVELACE_CONFIG,
rawConfig: LIGHT_LOVELACE_CONFIG,
config: config,
rawConfig: rawConfig,
editMode: false,
urlPath: "light",
mode: "generated",

View File

@@ -61,6 +61,24 @@ 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(
@@ -136,10 +154,30 @@ 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,13 +419,15 @@ class HuiEnergySankeyCard
};
deviceNodes.forEach((deviceNode) => {
const entity = this.hass.states[deviceNode.id];
const { area, floor } = getEntityContext(
entity,
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
const { area, floor } = entity
? getEntityContext(
entity,
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
)
: { area: null, floor: null };
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",
safety: "blue-grey",
security: "blue-grey",
media_players: "blue",
};
@@ -87,11 +87,6 @@ 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": {
@@ -100,7 +95,7 @@ export class HuiHomeSummaryCard extends LitElement implements LovelaceCard {
generateEntityFilter(this.hass!, filter)
);
const lightEntities = findEntities(entitiesInsideArea, lightsFilters);
const lightEntities = findEntities(allEntities, lightsFilters);
const onLights = lightEntities.filter((entityId) => {
const s = this.hass!.states[entityId]?.state;
@@ -147,20 +142,20 @@ export class HuiHomeSummaryCard extends LitElement implements LovelaceCard {
? `${formattedMinTemp}°`
: `${formattedMinTemp} - ${formattedMaxTemp}°`;
}
case "safety": {
case "security": {
// Alarm and lock status
const safetyFilters = HOME_SUMMARIES_FILTERS.safety.map((filter) =>
const securityFilters = HOME_SUMMARIES_FILTERS.security.map((filter) =>
generateEntityFilter(this.hass!, filter)
);
const safetyEntities = findEntities(entitiesInsideArea, safetyFilters);
const securityEntities = findEntities(allEntities, securityFilters);
const locks = safetyEntities.filter((entityId) => {
const locks = securityEntities.filter((entityId) => {
const domain = computeDomain(entityId);
return domain === "lock";
});
const alarms = safetyEntities.filter((entityId) => {
const alarms = securityEntities.filter((entityId) => {
const domain = computeDomain(entityId);
return domain === "alarm_control_panel";
});
@@ -204,7 +199,7 @@ export class HuiHomeSummaryCard extends LitElement implements LovelaceCard {
);
const mediaPlayerEntities = findEntities(
entitiesInsideArea,
allEntities,
mediaPlayerFilters
);

View File

@@ -1,5 +1,6 @@
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";
@@ -97,7 +98,10 @@ export class HuiIframeCard extends LitElement implements LovelaceCard {
: `${sandbox_user_params} ${IFRAME_SANDBOX}`;
return html`
<ha-card .header=${this._config.title}>
<ha-card
class=${classMap({ "hide-background": this._config.hide_background })}
.header=${this._config.title}
>
<div
id="root"
style=${styleMap({
@@ -133,6 +137,12 @@ 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,6 +323,7 @@ export interface IframeCardConfig extends LovelaceCardConfig {
title?: string;
allow?: string;
url: string;
hide_background?: boolean;
}
export interface LightCardConfig extends LovelaceCardConfig {

View File

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

View File

@@ -1,5 +1,7 @@
import { html, LitElement, nothing } from "lit";
import type { HassServiceTarget } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import {
array,
assert,
@@ -9,23 +11,21 @@ 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,7 +132,6 @@ export class HuiLogbookCardEditor
.hass=${this.hass}
.entityFilter=${this._filterFunc}
.value=${this._targetPicker}
add-on-top
@value-changed=${this._entitiesChanged}
></ha-target-picker>
`;
@@ -189,6 +188,13 @@ 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"),
safety: () => import("../../safety/strategies/safety-view-strategy"),
security: () => import("../../security/strategies/security-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 { safetyEntityFilters } from "../../../../safety/strategies/safety-view-strategy";
import { securityEntityFilters } from "../../../../security/strategies/security-view-strategy";
export const HOME_SUMMARIES = [
"light",
"climate",
"safety",
"security",
"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",
safety: "mdi:security",
security: "mdi:security",
media_players: "mdi:multimedia",
};
export const HOME_SUMMARIES_FILTERS: Record<HomeSummary, EntityFilter[]> = {
light: lightEntityFilters,
climate: climateEntityFilters,
safety: safetyEntityFilters,
security: securityEntityFilters,
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 === "safety") {
if (summary === "light" || summary === "climate" || summary === "security") {
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,
safety,
security,
media_players: mediaPlayers,
} = entitiesBySummary;
@@ -136,16 +136,16 @@ export class HomeAreaViewStrategy extends ReactiveElement {
});
}
if (safety.length > 0) {
if (security.length > 0) {
sections.push({
type: "grid",
cards: [
computeHeadingCard(
getSummaryLabel(hass.localize, "safety"),
HOME_SUMMARIES_ICONS.safety,
"/safety?historyBack=1"
getSummaryLabel(hass.localize, "security"),
HOME_SUMMARIES_ICONS.security,
"/security?historyBack=1"
),
...safety.map(computeTileCard),
...security.map(computeTileCard),
],
});
}

View File

@@ -1,7 +1,11 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import { generateEntityFilter } from "../../../../common/entity/entity_filter";
import {
findEntities,
generateEntityFilter,
} from "../../../../common/entity/entity_filter";
import { floorDefaultIcon } from "../../../../components/ha-floor-icon";
import type { AreaRegistryEntry } from "../../../../data/area_registry";
import { getEnergyPreferences } from "../../../../data/energy";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
@@ -21,7 +25,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 { floorDefaultIcon } from "../../../../components/ha-floor-icon";
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
export interface HomeMainViewStrategyConfig {
type: "home-main";
@@ -144,15 +148,33 @@ export class HomeMainViewStrategy extends ReactiveElement {
column_span: maxColumns,
} as LovelaceStrategySectionConfig;
const summarySection: LovelaceSectionConfig = {
type: "grid",
column_span: maxColumns,
cards: [
{
type: "heading",
heading: hass.localize("ui.panel.lovelace.strategy.home.summaries"),
},
{
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 &&
({
type: "home-summary",
summary: "light",
vertical: true,
@@ -164,8 +186,9 @@ export class HomeMainViewStrategy extends ReactiveElement {
rows: 2,
columns: 4,
},
} satisfies HomeSummaryCard,
{
} satisfies HomeSummaryCard),
hasClimate &&
({
type: "home-summary",
summary: "climate",
vertical: true,
@@ -177,21 +200,23 @@ export class HomeMainViewStrategy extends ReactiveElement {
rows: 2,
columns: 4,
},
} satisfies HomeSummaryCard,
{
} satisfies HomeSummaryCard),
hasSecurity &&
({
type: "home-summary",
summary: "safety",
summary: "security",
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "/safety?historyBack=1",
navigation_path: "/security?historyBack=1",
},
grid_options: {
rows: 2,
columns: 4,
},
} satisfies HomeSummaryCard,
{
} satisfies HomeSummaryCard),
hasMediaPlayers &&
({
type: "home-summary",
summary: "media_players",
vertical: true,
@@ -203,10 +228,25 @@ export class HomeMainViewStrategy extends ReactiveElement {
rows: 2,
columns: 4,
},
} satisfies HomeSummaryCard,
],
} satisfies HomeSummaryCard),
].filter(Boolean) as LovelaceCardConfig[];
const summarySection: LovelaceSectionConfig = {
type: "grid",
column_span: maxColumns,
cards: [],
};
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",
@@ -262,7 +302,7 @@ export class HomeMainViewStrategy extends ReactiveElement {
[
favoriteSection.cards && favoriteSection,
commonControlsSection,
summarySection,
summarySection.cards && summarySection,
...floorsSections,
widgetSection.cards && widgetSection,
] satisfies (LovelaceSectionRawConfig | undefined)[]

View File

@@ -59,6 +59,26 @@ 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(
@@ -134,10 +154,35 @@ 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,8 +29,6 @@ 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";
@@ -148,18 +146,19 @@ class HaRefreshTokens extends LitElement {
)}
</div>
<div>
<ha-dropdown>
<ha-md-button-menu positioning="popover">
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-dropdown-item
<ha-md-menu-item
graphic="icon"
@click=${this._toggleTokenExpiration}
.token=${token}
>
<ha-svg-icon
slot="icon"
slot="start"
.path=${token.expire_at
? mdiClockRemoveOutline
: mdiClockCheckOutline}
@@ -171,20 +170,24 @@ class HaRefreshTokens extends LitElement {
: this.hass.localize(
"ui.panel.profile.refresh_tokens.enable_token_expiration"
)}
</ha-dropdown-item>
<ha-dropdown-item
variant="danger"
</ha-md-menu-item>
<ha-md-menu-item
graphic="icon"
class="warning"
.disabled=${token.is_current}
@click=${this._deleteToken}
.token=${token}
>
<ha-svg-icon
slot="icon"
class="warning"
slot="start"
.path=${mdiDelete}
></ha-svg-icon>
${this.hass.localize("ui.common.delete")}
</ha-dropdown-item>
</ha-dropdown>
<div slot="headline">
${this.hass.localize("ui.common.delete")}
</div>
</ha-md-menu-item>
</ha-md-button-menu>
</div>
</ha-settings-row>
`

View File

@@ -1,28 +1,27 @@
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html } from "lit";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } 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 { LovelaceConfig } from "../../data/lovelace/config/types";
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view";
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 SAFETY_LOVELACE_CONFIG: LovelaceConfig = {
views: [
{
strategy: {
type: "safety",
},
},
],
const SECURITY_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = {
strategy: {
type: "security",
},
};
@customElement("ha-panel-safety")
class PanelSafety extends LitElement {
@customElement("ha-panel-security")
class PanelSecurity extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public narrow = false;
@@ -34,62 +33,120 @@ class PanelSafety 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?.locale !== this.hass.locale) {
if (oldHass && oldHass.localize !== this.hass.localize) {
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(): TemplateResult {
protected render() {
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.safety")}</div>
${
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>
<hui-view-container .hass=${this.hass}>
<hui-view
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.index=${this._viewIndex}
></hui-view>
${
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>
`;
}
private _setLovelace() {
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;
}
this._lovelace = {
config: SAFETY_LOVELACE_CONFIG,
rawConfig: SAFETY_LOVELACE_CONFIG,
config: config,
rawConfig: rawConfig,
editMode: false,
urlPath: "safety",
urlPath: "security",
mode: "generated",
locale: this.hass.locale,
enableFullEditMode: () => undefined,
@@ -191,6 +248,6 @@ class PanelSafety extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"ha-panel-safety": PanelSafety;
"ha-panel-security": PanelSecurity;
}
}

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 SafetyViewStrategyConfig {
type: "safety";
export interface SecurityViewStrategyConfig {
type: "security";
}
export const safetyEntityFilters: EntityFilter[] = [
export const securityEntityFilters: EntityFilter[] = [
{
domain: "camera",
entity_category: "none",
@@ -67,7 +67,7 @@ export const safetyEntityFilters: EntityFilter[] = [
},
];
const processAreasForSafety = (
const processAreasForSecurity = (
areaIds: string[],
hass: HomeAssistant,
entities: string[]
@@ -81,12 +81,12 @@ const processAreasForSafety = (
const areaFilter = generateEntityFilter(hass, {
area: area.area_id,
});
const areaSafetyEntities = entities.filter(areaFilter);
const areaSecurityEntities = entities.filter(areaFilter);
const areaCards: LovelaceCardConfig[] = [];
const computeTileCard = computeAreaTileCardConfig(hass, "", false);
for (const entityId of areaSafetyEntities) {
for (const entityId of areaSecurityEntities) {
areaCards.push(computeTileCard(entityId));
}
@@ -103,10 +103,28 @@ const processAreasForSafety = (
return cards;
};
@customElement("safety-view-strategy")
export class SafetyViewStrategy extends ReactiveElement {
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 {
static async generate(
_config: SafetyViewStrategyConfig,
_config: SecurityViewStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const areas = getAreas(hass.areas);
@@ -117,11 +135,11 @@ export class SafetyViewStrategy extends ReactiveElement {
const allEntities = Object.keys(hass.states);
const safetyFilters = safetyEntityFilters.map((filter) =>
const securityFilters = securityEntityFilters.map((filter) =>
generateEntityFilter(hass, filter)
);
const entities = findEntities(allEntities, safetyFilters);
const entities = findEntities(allEntities, securityFilters);
const floorCount = home.floors.length + (home.areas.length ? 1 : 0);
@@ -146,7 +164,7 @@ export class SafetyViewStrategy extends ReactiveElement {
],
};
const areaCards = processAreasForSafety(areaIds, hass, entities);
const areaCards = processAreasForSecurity(areaIds, hass, entities);
if (areaCards.length > 0) {
section.cards!.push(...areaCards);
@@ -170,7 +188,7 @@ export class SafetyViewStrategy extends ReactiveElement {
],
};
const areaCards = processAreasForSafety(home.areas, hass, entities);
const areaCards = processAreasForSecurity(home.areas, hass, entities);
if (areaCards.length > 0) {
section.cards!.push(...areaCards);
@@ -178,16 +196,39 @@ export class SafetyViewStrategy 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 {
"safety-view-strategy": SafetyViewStrategy;
"security-view-strategy": SecurityViewStrategy;
}
}

View File

@@ -155,10 +155,6 @@ 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);
}
`;
@@ -290,9 +286,5 @@ 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,9 +52,7 @@ 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-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-color-surface-default: var(--card-background-color);
--wa-panel-border-radius: var(--ha-border-radius-3xl);
--wa-panel-border-style: solid;
--wa-panel-border-width: 1px;

View File

@@ -16,17 +16,7 @@ export const waMainStyles = css`
--wa-font-weight-action: var(--ha-font-weight-medium);
--wa-transition-fast: 75ms;
--wa-transition-easing: ease;
--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-border-width-l: var(--ha-border-radius-lg);
--wa-space-xl: 32px;
}

View File

@@ -12,7 +12,7 @@
"media_browser": "Media",
"profile": "Profile",
"light": "Lights",
"safety": "Safety",
"security": "Security",
"climate": "Climate"
},
"state": {
@@ -695,6 +695,11 @@
"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",
@@ -1429,6 +1434,7 @@
"back_to_info": "Back to info",
"info": "Information",
"related": "Related",
"add_entity_to": "Add to",
"history": "History",
"aggregate": "5-minute aggregated",
"logbook": "Activity",
@@ -1445,6 +1451,10 @@
"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",
@@ -3066,6 +3076,7 @@
"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.",
@@ -3197,6 +3208,22 @@
"included_in_device_helper": "If this device is already counted by another device (such as a smart switch measured by a smart breaker), selecting the upstream device prevents duplicate energy tracking.",
"no_upstream_devices": "No eligible upstream devices"
}
},
"device_consumption_water": {
"title": "Individual water devices",
"sub": "Tracking the water usage of individual devices allows Home Assistant to break down your water usage by device.",
"learn_more": "More information on how to get started.",
"devices": "Devices",
"add_device": "Add device",
"dialog": {
"header": "Add a water device",
"display_name": "Display name",
"device_consumption_water": "Device water consumption",
"selected_stat_intro": "Select the water sensor that measures the device's water usage in either of {unit}.",
"included_in_device": "Upstream device",
"included_in_device_helper": "If this device is already counted by another device (such as a water meter measured by the main water supply), selecting the upstream device prevents duplicate water tracking.",
"no_upstream_devices": "No eligible upstream devices"
}
}
},
"helpers": {
@@ -6975,6 +7002,22 @@
"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": {
@@ -7762,7 +7805,9 @@
},
"iframe": {
"name": "Webpage",
"description": "The Webpage card allows you to embed your favorite webpage right into Home Assistant."
"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."
},
"light": {
"name": "Light",

View File

@@ -388,4 +388,70 @@ 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.2":
version: 7.0.4
resolution: "@octokit/core@npm:7.0.4"
"@octokit/core@npm:^7.0.6":
version: 7.0.6
resolution: "@octokit/core@npm:7.0.6"
dependencies:
"@octokit/auth-token": "npm:^6.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"
"@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"
before-after-hook: "npm:^4.0.0"
universal-user-agent: "npm:^7.0.0"
checksum: 10/d691df211ba9a2941ec97dc32e1c34e26e7c8161425fb573425cfc2727f548bf2a2445eec9d36748b4b469478feb21de4bec1c00a34aa02eedf5b7625ca52189
checksum: 10/852d41fc3150d2a891156427dd0575c77889f1c7a109894ee541594e3fd47c0d4e0a93fee22966c507dfd6158b522e42846c2ac46b9d896078194c95fa81f4ae
languageName: node
linkType: hard
@@ -3471,14 +3471,14 @@ __metadata:
languageName: node
linkType: hard
"@octokit/graphql@npm:^9.0.1":
version: 9.0.1
resolution: "@octokit/graphql@npm:9.0.1"
"@octokit/graphql@npm:^9.0.3":
version: 9.0.3
resolution: "@octokit/graphql@npm:9.0.3"
dependencies:
"@octokit/request": "npm:^10.0.2"
"@octokit/types": "npm:^14.0.0"
"@octokit/request": "npm:^10.0.6"
"@octokit/types": "npm:^16.0.0"
universal-user-agent: "npm:^7.0.0"
checksum: 10/02d7ea4e2c17a4d4b7311150d0326318c756aff6cf955d9ba443a4bf26b32784832060379fc74f4537657415b262c10adb7f4a1655e15b143d19c2f099b87f16
checksum: 10/7b16f281f8571dce55280b3986fbb8d15465a7236164a5f6497ded7597ff9ee95d5796924555b979903fe8c6706fe6be1b3e140d807297f85ac8edeadc28f9fe
languageName: node
linkType: hard
@@ -3501,20 +3501,6 @@ __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"
@@ -3522,14 +3508,14 @@ __metadata:
languageName: node
linkType: hard
"@octokit/plugin-paginate-rest@npm:^13.0.1":
version: 13.1.1
resolution: "@octokit/plugin-paginate-rest@npm:13.1.1"
"@octokit/plugin-paginate-rest@npm:^14.0.0":
version: 14.0.0
resolution: "@octokit/plugin-paginate-rest@npm:14.0.0"
dependencies:
"@octokit/types": "npm:^14.1.0"
"@octokit/types": "npm:^16.0.0"
peerDependencies:
"@octokit/core": ">=6"
checksum: 10/26b9b7a233b77fff31d31469879a281e651417df86799387d6563446f037c9969061b833ab0698857c9797ea856f2ef7ed6970d8fb471239879c9298bbabe200
checksum: 10/57ddd857528dad9c02431bc6254c2374c06057872cf9656a4a88b162ebe1c2bc9f34fbec360f2ccff72c940f29b120758ce14e8135bd027223d381eb1b8b6579
languageName: node
linkType: hard
@@ -3542,14 +3528,14 @@ __metadata:
languageName: node
linkType: hard
"@octokit/plugin-rest-endpoint-methods@npm:^16.0.0":
version: 16.1.0
resolution: "@octokit/plugin-rest-endpoint-methods@npm:16.1.0"
"@octokit/plugin-rest-endpoint-methods@npm:^17.0.0":
version: 17.0.0
resolution: "@octokit/plugin-rest-endpoint-methods@npm:17.0.0"
dependencies:
"@octokit/types": "npm:^15.0.0"
"@octokit/types": "npm:^16.0.0"
peerDependencies:
"@octokit/core": ">=6"
checksum: 10/9b62d1ddf3435d77cbf11f36abf951b755cf2033dfd5f1875d35f97ad4ab9391e7d0e008ae5b813b7fe0b6c537ab38240b4911c61b53cf8fb8d9957a3d898e09
checksum: 10/e9d9ad4d9755cc7fb82fdcbfa870ddea8a432180f0f76c8469095557fd1e26f8caea8cae58401209be17c4f3d8cc48c0e16a3643e37e48f4d23c39e058bf2c55
languageName: node
linkType: hard
@@ -3566,7 +3552,7 @@ __metadata:
languageName: node
linkType: hard
"@octokit/request-error@npm:^7.0.0, @octokit/request-error@npm:^7.0.2":
"@octokit/request-error@npm:^7.0.2":
version: 7.0.2
resolution: "@octokit/request-error@npm:7.0.2"
dependencies:
@@ -3575,7 +3561,7 @@ __metadata:
languageName: node
linkType: hard
"@octokit/request@npm:^10.0.2, @octokit/request@npm:^10.0.6":
"@octokit/request@npm:^10.0.6":
version: 10.0.6
resolution: "@octokit/request@npm:10.0.6"
dependencies:
@@ -3588,33 +3574,15 @@ __metadata:
languageName: node
linkType: hard
"@octokit/rest@npm:22.0.0":
version: 22.0.0
resolution: "@octokit/rest@npm:22.0.0"
"@octokit/rest@npm:22.0.1":
version: 22.0.1
resolution: "@octokit/rest@npm:22.0.1"
dependencies:
"@octokit/core": "npm:^7.0.2"
"@octokit/plugin-paginate-rest": "npm:^13.0.1"
"@octokit/core": "npm:^7.0.6"
"@octokit/plugin-paginate-rest": "npm:^14.0.0"
"@octokit/plugin-request-log": "npm:^6.0.0"
"@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
"@octokit/plugin-rest-endpoint-methods": "npm:^17.0.0"
checksum: 10/ec2e94cfa8766716faeb3ca18527d9af746482d35aaa6e4265a30cb669ae3f31f4ebb6235edebe5ae62bc2cec2b8e88902584f698df2e7cabac3a15fd27da665
languageName: node
linkType: hard
@@ -8971,10 +8939,10 @@ __metadata:
languageName: node
linkType: hard
"globals@npm:16.4.0":
version: 16.4.0
resolution: "globals@npm:16.4.0"
checksum: 10/1627a9f42fb4c82d7af6a0c8b6cd616e00110908304d5f1ddcdf325998f3aed45a4b29d8a1e47870f328817805263e31e4f1673f00022b9c2b210552767921cf
"globals@npm:16.5.0":
version: 16.5.0
resolution: "globals@npm:16.5.0"
checksum: 10/f9e8a2a13f50222c127030a619e283e7bbfe32966316bdde0715af1d15a7e40cb9c24ff52cad59671f97762ed8b515353c2f8674f560c63d9385f19ee26735a6
languageName: node
linkType: hard
@@ -9216,10 +9184,10 @@ __metadata:
languageName: node
linkType: hard
"hls.js@npm:1.6.13":
version: 1.6.13
resolution: "hls.js@npm:1.6.13"
checksum: 10/4de045fddbeb6edc3859021ff60b268a642aa87e83347a3a764e53299b616d57f36eecef0b91eab240ffde84f75d1ab5f7cc0916a929de265707d0f0b27d1c71
"hls.js@npm:1.6.14":
version: 1.6.14
resolution: "hls.js@npm:1.6.14"
checksum: 10/56eedf163912abf72bd5ca0eac44dbaa442cff59bb8d1ff3303614bf18d21dfa3ec7b85db5b71449986c52c63b7efb6bd5392b048bf22c61549f71a001fa94cd
languageName: node
linkType: hard
@@ -9293,7 +9261,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.0"
"@octokit/rest": "npm:22.0.1"
"@replit/codemirror-indentation-markers": "npm:6.5.3"
"@rsdoctor/rspack-plugin": "npm:1.3.7"
"@rspack/core": "npm:1.6.0"
@@ -9363,7 +9331,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.13"
hls.js: "npm:1.6.14"
home-assistant-js-websocket: "npm:9.5.0"
html-minifier-terser: "npm:7.2.0"
husky: "npm:9.1.7"