Compare commits

..

1 Commits

Author SHA1 Message Date
Bram Kragten
d215064c6e Allow the number selector to pick an entity 2025-11-26 10:40:22 +01:00
94 changed files with 2346 additions and 7421 deletions

File diff suppressed because one or more lines are too long

View File

@@ -6,4 +6,4 @@ enableGlobalCache: false
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.12.0.cjs
yarnPath: .yarn/releases/yarn-4.11.0.cjs

View File

@@ -18,6 +18,7 @@ import { HaEventTrigger } from "../../../../src/panels/config/automation/trigger
import { HaGeolocationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-geo_location";
import { HaHassTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-homeassistant";
import { HaTriggerList } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-list";
import { HaMQTTTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt";
import { HaNumericStateTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-numeric_state";
import { HaPersistentNotificationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-persistent_notification";
import { HaStateTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-state";
@@ -37,6 +38,11 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
triggers: [{ ...HaStateTrigger.defaultConfig }],
},
{
name: "MQTT",
triggers: [{ ...HaMQTTTrigger.defaultConfig }],
},
{
name: "GeoLocation",
triggers: [{ ...HaGeolocationTrigger.defaultConfig }],

View File

@@ -28,13 +28,13 @@
"dependencies": {
"@babel/runtime": "7.28.4",
"@braintree/sanitize-url": "7.1.1",
"@codemirror/autocomplete": "6.20.0",
"@codemirror/autocomplete": "6.19.1",
"@codemirror/commands": "6.10.0",
"@codemirror/language": "6.11.3",
"@codemirror/legacy-modes": "6.5.2",
"@codemirror/search": "6.5.11",
"@codemirror/state": "6.5.2",
"@codemirror/view": "6.38.8",
"@codemirror/view": "6.38.6",
"@date-fns/tz": "1.4.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.18.2",
@@ -52,7 +52,7 @@
"@fullcalendar/list": "6.1.19",
"@fullcalendar/luxon3": "6.1.19",
"@fullcalendar/timegrid": "6.1.19",
"@home-assistant/webawesome": "3.0.0-ha.0",
"@home-assistant/webawesome": "3.0.0",
"@lezer/highlight": "1.2.3",
"@lit-labs/motion": "1.0.9",
"@lit-labs/observers": "2.0.6",
@@ -96,10 +96,10 @@
"@webcomponents/scoped-custom-element-registry": "0.0.10",
"@webcomponents/webcomponentsjs": "2.8.0",
"app-datepicker": "5.1.1",
"barcode-detector": "3.0.8",
"color-name": "2.1.0",
"barcode-detector": "3.0.6",
"color-name": "2.0.2",
"comlink": "4.4.2",
"core-js": "3.47.0",
"core-js": "3.46.0",
"cropperjs": "1.6.2",
"culori": "4.0.2",
"date-fns": "4.1.0",
@@ -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.15",
"hls.js": "1.6.14",
"home-assistant-js-websocket": "9.6.0",
"idb-keyval": "6.2.2",
"intl-messageformat": "10.7.18",
@@ -122,7 +122,7 @@
"lit": "3.3.1",
"lit-html": "3.3.1",
"luxon": "3.7.2",
"marked": "17.0.1",
"marked": "17.0.0",
"memoize-one": "6.0.0",
"node-vibrant": "4.0.3",
"object-hash": "3.0.0",
@@ -139,12 +139,12 @@
"vue": "2.7.16",
"vue2-daterange-picker": "0.6.8",
"weekstart": "2.0.0",
"workbox-cacheable-response": "7.4.0",
"workbox-core": "7.4.0",
"workbox-expiration": "7.4.0",
"workbox-precaching": "7.4.0",
"workbox-routing": "7.4.0",
"workbox-strategies": "7.4.0",
"workbox-cacheable-response": "7.3.0",
"workbox-core": "7.3.0",
"workbox-expiration": "7.3.0",
"workbox-precaching": "7.3.0",
"workbox-routing": "7.3.0",
"workbox-strategies": "7.3.0",
"xss": "1.0.15"
},
"devDependencies": {
@@ -153,7 +153,7 @@
"@babel/plugin-transform-runtime": "7.28.5",
"@babel/preset-env": "7.28.5",
"@bundle-stats/plugin-webpack-filter": "4.21.6",
"@lokalise/node-api": "15.4.0",
"@lokalise/node-api": "15.3.1",
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.0.3",
"@octokit/rest": "22.0.1",
@@ -178,7 +178,7 @@
"@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
"@vitest/coverage-v8": "4.0.13",
"@vitest/coverage-v8": "4.0.8",
"babel-loader": "10.0.0",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
@@ -201,25 +201,25 @@
"gulp-rename": "2.1.0",
"html-minifier-terser": "7.2.0",
"husky": "9.1.7",
"jsdom": "27.2.0",
"jsdom": "27.1.0",
"jszip": "3.10.1",
"lint-staged": "16.2.7",
"lint-staged": "16.2.6",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.5.0",
"map-stream": "0.0.7",
"pinst": "3.0.0",
"prettier": "3.6.2",
"rspack-manifest-plugin": "5.2.0",
"rspack-manifest-plugin": "5.1.0",
"serve": "14.2.5",
"sinon": "21.0.0",
"tar": "7.5.2",
"terser-webpack-plugin": "5.3.14",
"ts-lit-plugin": "2.0.2",
"typescript": "5.9.3",
"typescript-eslint": "8.47.0",
"typescript-eslint": "8.46.3",
"vite-tsconfig-paths": "5.1.4",
"vitest": "4.0.13",
"vitest": "4.0.8",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
@@ -236,7 +236,7 @@
"@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",
"glob@^10.2.2": "^10.5.0"
},
"packageManager": "yarn@4.12.0",
"packageManager": "yarn@4.11.0",
"volta": {
"node": "22.21.1"
}

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20251127.0"
version = "20251029.0"
license = "Apache-2.0"
license-files = ["LICENSE*"]
description = "The Home Assistant frontend"

View File

@@ -1,6 +1,5 @@
import type { AuthData } from "home-assistant-js-websocket";
import { extractSearchParam } from "../url/search-params";
import { hassUrl } from "../../data/auth";
declare global {
interface Window {
@@ -31,11 +30,7 @@ export function askWrite() {
export function saveTokens(tokens: AuthData | null) {
tokenCache.tokens = tokens;
if (
!tokenCache.writeEnabled &&
(extractSearchParam("storeToken") === "true" ||
hassUrl !== `${location.protocol}//${location.host}`)
) {
if (!tokenCache.writeEnabled && extractSearchParam("storeToken") === "true") {
tokenCache.writeEnabled = true;
}

View File

@@ -1,16 +1,64 @@
import memoizeOne from "memoize-one";
import { theme2hex } from "./convert-color";
// Total number of colors defined in CSS variables (--color-1 through --color-54)
export const COLORS_COUNT = 54;
export const COLORS = [
"#4269d0",
"#f4bd4a",
"#ff725c",
"#6cc5b0",
"#a463f2",
"#ff8ab7",
"#9c6b4e",
"#97bbf5",
"#01ab63",
"#094bad",
"#c99000",
"#d84f3e",
"#49a28f",
"#048732",
"#d96895",
"#8043ce",
"#7599d1",
"#7a4c31",
"#6989f4",
"#ffd444",
"#ff957c",
"#8fe9d3",
"#62cc71",
"#ffadda",
"#c884ff",
"#badeff",
"#bf8b6d",
"#927acc",
"#97ee3f",
"#bf3947",
"#9f5b00",
"#f48758",
"#8caed6",
"#f2b94f",
"#eff26e",
"#e43872",
"#d9b100",
"#9d7a00",
"#698cff",
"#00d27e",
"#d06800",
"#009f82",
"#c49200",
"#cbe8ff",
"#fecddf",
"#c27eb6",
"#8cd2ce",
"#c4b8d9",
"#f883b0",
"#a49100",
"#f48800",
"#27d0df",
"#a04a9b",
];
export function getColorByIndex(
index: number,
style: CSSStyleDeclaration
): string {
// Wrap around using modulo to support unlimited indices
const colorIndex = (index % COLORS_COUNT) + 1;
return style.getPropertyValue(`--color-${colorIndex}`);
export function getColorByIndex(index: number) {
return COLORS[index % COLORS.length];
}
export function getGraphColorByIndex(
@@ -20,19 +68,15 @@ export function getGraphColorByIndex(
// The CSS vars for the colors use range 1..n, so we need to adjust the index from the internal 0..n color index range.
const themeColor =
style.getPropertyValue(`--graph-color-${index + 1}`) ||
getColorByIndex(index, style);
getColorByIndex(index);
return theme2hex(themeColor);
}
export const getAllGraphColors = memoizeOne(
(style: CSSStyleDeclaration) =>
Array.from({ length: COLORS_COUNT }, (_, index) =>
getGraphColorByIndex(index, style)
),
COLORS.map((_color, index) => getGraphColorByIndex(index, style)),
(newArgs: [CSSStyleDeclaration], lastArgs: [CSSStyleDeclaration]) =>
// this is not ideal, but we need to memoize the colors
newArgs[0].getPropertyValue("--graph-color-1") ===
lastArgs[0].getPropertyValue("--graph-color-1") &&
newArgs[0].getPropertyValue("--color-1") ===
lastArgs[0].getPropertyValue("--color-1")
lastArgs[0].getPropertyValue("--graph-color-1")
);

View File

@@ -279,7 +279,6 @@ export class HaSankeyChart extends LitElement {
:host {
display: block;
flex: 1;
max-width: 100%;
background: var(--ha-card-background, var(--card-background-color));
}
ha-chart-base {

View File

@@ -202,7 +202,6 @@ export class HaControlSelect extends LitElement {
color: var(--primary-text-color);
user-select: none;
-webkit-tap-highlight-color: transparent;
border-radius: var(--control-select-border-radius);
}
:host([vertical]) {
width: var(--control-select-thickness);
@@ -212,6 +211,7 @@ export class HaControlSelect extends LitElement {
position: relative;
height: 100%;
width: 100%;
border-radius: var(--control-select-border-radius);
transform: translateZ(0);
display: flex;
flex-direction: row;

View File

@@ -6,7 +6,7 @@ import {
mdiHomeFloor3,
mdiHomeFloorNegative1,
} from "@mdi/js";
import { LitElement, html, nothing } from "lit";
import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators";
import type { FloorRegistryEntry } from "../data/floor_registry";
import "./ha-icon";
@@ -48,7 +48,7 @@ export const floorDefaultIcon = (floor: Pick<FloorRegistryEntry, "level">) => {
@customElement("ha-floor-icon")
export class HaFloorIcon extends LitElement {
@property({ attribute: false }) public floor?: Pick<
@property({ attribute: false }) public floor!: Pick<
FloorRegistryEntry,
"icon" | "level"
>;
@@ -56,9 +56,6 @@ export class HaFloorIcon extends LitElement {
@property() public icon?: string;
protected render() {
if (!this.floor) {
return nothing;
}
if (this.floor.icon) {
return html`<ha-icon .icon=${this.floor.icon}></ha-icon>`;
}

View File

@@ -186,7 +186,6 @@ export class HaIcon extends LitElement {
static styles = css`
:host {
display: flex;
fill: currentcolor;
}
`;

View File

@@ -154,10 +154,7 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
}
return this._getLabelsMemoized(
this.hass.states,
this.hass.areas,
this.hass.devices,
this.hass.entities,
this.hass,
this._labels,
this.includeDomains,
this.excludeDomains,

View File

@@ -81,7 +81,6 @@ class HaLabel extends LitElement {
.container {
display: flex;
align-items: center;
position: relative;
height: 100%;
padding: 0 16px;

View File

@@ -192,7 +192,7 @@ export class HaPickerComboBox extends LitElement {
@focus=${this._focusList}
@visibilityChanged=${this._visibilityChanged}
>
</lit-virtualizer>`;
</lit-virtualizer> `;
}
private _renderSectionButtons() {

View File

@@ -1,28 +0,0 @@
import { css, html, LitElement } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-section-title")
class HaSectionTitle extends LitElement {
protected render() {
return html`<slot></slot>`;
}
static styles = css`
:host {
background-color: var(--ha-color-fill-neutral-quiet-resting);
padding: var(--ha-space-1) var(--ha-space-2);
font-weight: var(--ha-font-weight-bold);
color: var(--secondary-text-color);
min-height: var(--ha-space-6);
display: flex;
align-items: center;
box-sizing: border-box;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-section-title": HaSectionTitle;
}
}

View File

@@ -1,10 +1,13 @@
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import type { NumberSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../entity/ha-entity-picker";
import "../ha-button-toggle-group";
import "../ha-input-helper-text";
import "../ha-slider";
import "../ha-textfield";
@@ -15,7 +18,7 @@ export class HaNumberSelector extends LitElement {
@property({ attribute: false }) public selector!: NumberSelector;
@property({ type: Number }) public value?: number;
@property({ type: Number }) public value?: number | string;
@property({ type: Number }) public placeholder?: number;
@@ -30,13 +33,30 @@ export class HaNumberSelector extends LitElement {
@property({ type: Boolean }) public disabled = false;
@state() private _mode: "number" | "entity" = "number";
private _valueStr = "";
protected willUpdate(changedProps: PropertyValues) {
if (!this.hasUpdated) {
if (
this.selector.number?.entity?.domains.length &&
typeof this.value === "string" &&
this.selector.number?.entity?.domains.some((domain) =>
(this.value as string).startsWith(`${domain}.`)
)
) {
this._mode = "entity";
}
}
if (changedProps.has("value")) {
if (this._valueStr === "" || this.value !== Number(this._valueStr)) {
this._valueStr =
this.value == null || isNaN(this.value) ? "" : this.value.toString();
this.value == null ||
typeof this.value === "string" ||
isNaN(this.value)
? ""
: this.value.toString();
}
}
}
@@ -47,6 +67,8 @@ export class HaNumberSelector extends LitElement {
this.selector.number?.min === undefined ||
this.selector.number?.max === undefined;
const multiMode = Boolean(this.selector.number?.entity?.domains.length);
let sliderStep;
if (!isBox) {
@@ -72,51 +94,73 @@ export class HaNumberSelector extends LitElement {
}
return html`
${this.label && !isBox
${this.label && !isBox && !multiMode
? html`${this.label}${this.required ? "*" : ""}`
: nothing}
${multiMode
? html`<div class="multi-header">
<span>${this.label}${this.required ? "*" : ""}</span>
<ha-button-toggle-group
size="small"
.buttons=${this._toggleButtons(this.hass.localize)}
.active=${this._mode}
@value-changed=${this._modeChanged}
></ha-button-toggle-group>
</div>`
: nothing}
<div class="input">
${!isBox
? html`
<ha-slider
labeled
.min=${this.selector.number!.min}
.max=${this.selector.number!.max}
.value=${this.value}
.step=${sliderStep}
${multiMode && this._mode === "entity"
? html`<ha-entity-picker
.hass=${this.hass}
.includeDomains=${this.selector.number!.entity!.domains}
.value=${this.value}
.placeholder=${this.placeholder}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
></ha-entity-picker>`
: html`${!isBox
? html`
<ha-slider
labeled
.min=${this.selector.number!.min}
.max=${this.selector.number!.max}
.value=${this.value}
.step=${sliderStep}
.disabled=${this.disabled}
.required=${this.required}
@change=${this._handleSliderChange}
.withMarkers=${this.selector.number?.slider_ticks ||
false}
>
</ha-slider>
`
: nothing}
<ha-textfield
.inputMode=${this.selector.number?.step === "any" ||
(this.selector.number?.step ?? 1) % 1 !== 0
? "decimal"
: "numeric"}
.label=${!isBox ? undefined : this.label}
.placeholder=${this.placeholder}
class=${classMap({ single: isBox })}
.min=${this.selector.number?.min}
.max=${this.selector.number?.max}
.value=${this._valueStr ?? ""}
.step=${this.selector.number?.step ?? 1}
helperPersistent
.helper=${isBox ? this.helper : undefined}
.disabled=${this.disabled}
.required=${this.required}
@change=${this._handleSliderChange}
.withMarkers=${this.selector.number?.slider_ticks || false}
.suffix=${unit}
type="number"
autoValidate
?no-spinner=${!isBox}
@input=${this._handleInputChange}
>
</ha-slider>
`
: nothing}
<ha-textfield
.inputMode=${this.selector.number?.step === "any" ||
(this.selector.number?.step ?? 1) % 1 !== 0
? "decimal"
: "numeric"}
.label=${!isBox ? undefined : this.label}
.placeholder=${this.placeholder}
class=${classMap({ single: isBox })}
.min=${this.selector.number?.min}
.max=${this.selector.number?.max}
.value=${this._valueStr ?? ""}
.step=${this.selector.number?.step ?? 1}
helperPersistent
.helper=${isBox ? this.helper : undefined}
.disabled=${this.disabled}
.required=${this.required}
.suffix=${unit}
type="number"
autoValidate
?no-spinner=${!isBox}
@input=${this._handleInputChange}
>
</ha-textfield>
</ha-textfield>`}
</div>
${!isBox && this.helper
${!isBox && !(multiMode && this._mode === "entity") && this.helper
? html`<ha-input-helper-text .disabled=${this.disabled}
>${this.helper}</ha-input-helper-text
>`
@@ -124,6 +168,22 @@ export class HaNumberSelector extends LitElement {
`;
}
private _toggleButtons = memoizeOne((localize: HomeAssistant["localize"]) => [
{
label: localize("ui.components.selectors.number.value"),
value: "number",
},
{
label: localize("ui.components.selectors.number.entity_value"),
value: "entity",
},
]);
private _modeChanged(ev) {
ev.stopPropagation();
this._mode = ev.detail?.value || ev.target.value;
}
private _handleInputChange(ev) {
ev.stopPropagation();
this._valueStr = ev.target.value;
@@ -155,17 +215,32 @@ export class HaNumberSelector extends LitElement {
}
ha-slider {
flex: 1;
margin-right: 16px;
margin-inline-end: 16px;
margin-right: var(--ha-space-4);
margin-inline-end: var(--ha-space-4);
margin-inline-start: 0;
}
ha-textfield {
--ha-textfield-input-width: 40px;
}
.multi-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--ha-space-2);
}
.single {
--ha-textfield-input-width: unset;
flex: 1;
}
ha-entity-picker {
display: block;
width: 100%;
}
ha-button-toggle-group {
display: block;
justify-self: end;
}
`;
}

View File

@@ -465,16 +465,10 @@ export class HaServiceControl extends LitElement {
? computeObjectId(this._value.action)
: undefined;
const descriptionPlaceholders =
domain && serviceName
? this.hass.services[domain][serviceName].description_placeholders
: undefined;
const description =
(serviceName &&
this.hass.localize(
`component.${domain}.services.${serviceName}.description`,
descriptionPlaceholders
`component.${domain}.services.${serviceName}.description`
)) ||
serviceData?.description;
@@ -543,8 +537,7 @@ export class HaServiceControl extends LitElement {
.disabled=${this.disabled}
.value=${this._value?.data?.entity_id}
.label=${this.hass.localize(
`component.${domain}.services.${serviceName}.fields.entity_id.description`,
descriptionPlaceholders
`component.${domain}.services.${serviceName}.fields.entity_id.description`
) || entityId.description}
@value-changed=${this._entityPicked}
allow-custom-entity
@@ -582,8 +575,7 @@ export class HaServiceControl extends LitElement {
left-chevron
.expanded=${!dataField.collapsed}
.header=${this.hass.localize(
`component.${domain}.services.${serviceName}.sections.${dataField.key}.name`,
descriptionPlaceholders
`component.${domain}.services.${serviceName}.sections.${dataField.key}.name`
) ||
dataField.name ||
dataField.key}
@@ -619,10 +611,7 @@ export class HaServiceControl extends LitElement {
serviceName: string | undefined
) {
return this.hass!.localize(
`component.${domain}.services.${serviceName}.sections.${dataField.key}.description`,
domain && serviceName
? this.hass.services[domain][serviceName].description_placeholders
: undefined
`component.${domain}.services.${serviceName}.sections.${dataField.key}.description`
);
}
@@ -669,10 +658,6 @@ export class HaServiceControl extends LitElement {
}
const showOptional = showOptionalToggle(dataField);
const descriptionPlaceholders =
domain && serviceName
? this.hass.services[domain][serviceName].description_placeholders
: undefined;
return dataField.selector &&
(!dataField.advanced ||
@@ -694,8 +679,7 @@ export class HaServiceControl extends LitElement {
></ha-checkbox>`}
<span slot="heading"
>${this.hass.localize(
`component.${domain}.services.${serviceName}.fields.${dataField.key}.name`,
descriptionPlaceholders
`component.${domain}.services.${serviceName}.fields.${dataField.key}.name`
) ||
dataField.name ||
dataField.key}</span
@@ -705,8 +689,7 @@ export class HaServiceControl extends LitElement {
breaks
allow-svg
.content=${this.hass.localize(
`component.${domain}.services.${serviceName}.fields.${dataField.key}.description`,
descriptionPlaceholders
`component.${domain}.services.${serviceName}.fields.${dataField.key}.description`
) || dataField?.description}
></ha-markdown>
</span>

View File

@@ -92,14 +92,8 @@ class HaServicePicker extends LitElement {
`;
}
const descriptionPlaceholders =
this.hass.services[domain][service].description_placeholders;
const serviceName =
localize(
`component.${domain}.services.${service}.name`,
descriptionPlaceholders
) ||
localize(`component.${domain}.services.${service}.name`) ||
services[domain][service].name ||
service;
@@ -169,21 +163,16 @@ class HaServicePicker extends LitElement {
const serviceId = `${domain}.${service}`;
const domainName = domainToName(localize, domain);
const descriptionPlaceholders =
this.hass.services[domain][service].description_placeholders;
const name =
this.hass.localize(
`component.${domain}.services.${service}.name`,
descriptionPlaceholders
`component.${domain}.services.${service}.name`
) ||
services[domain][service].name ||
service;
const description =
this.hass.localize(
`component.${domain}.services.${service}.description`,
descriptionPlaceholders
`component.${domain}.services.${service}.description`
) ||
services[domain][service].description ||
"";

View File

@@ -197,8 +197,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
private _mouseLeaveTimeout?: number;
private _touchendTimeout?: number;
private _tooltipHideTimeout?: number;
private _recentKeydownActiveUntil = 0;
@@ -239,18 +237,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
];
}
public disconnectedCallback() {
super.disconnectedCallback();
// clear timeouts
clearTimeout(this._mouseLeaveTimeout);
clearTimeout(this._tooltipHideTimeout);
clearTimeout(this._touchendTimeout);
// set undefined values
this._mouseLeaveTimeout = undefined;
this._tooltipHideTimeout = undefined;
this._touchendTimeout = undefined;
}
protected render() {
if (!this.hass) {
return nothing;
@@ -420,7 +406,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
class="ha-scrollbar"
@focusin=${this._listboxFocusIn}
@focusout=${this._listboxFocusOut}
@touchend=${this._listboxTouchend}
@scroll=${this._listboxScroll}
@keydown=${this._listboxKeydown}
>
@@ -635,14 +620,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
this._hideTooltip();
}
private _listboxTouchend() {
clearTimeout(this._touchendTimeout);
this._touchendTimeout = window.setTimeout(() => {
// Allow 1 second for users to read the tooltip on touch devices
this._hideTooltip();
}, 1000);
}
@eventOptions({
passive: true,
})

View File

@@ -30,7 +30,6 @@ import {
areaMeetsFilter,
deviceMeetsFilter,
entityRegMeetsFilter,
getTargetComboBoxItemType,
type TargetType,
type TargetTypeFloorless,
} from "../data/target";
@@ -48,6 +47,7 @@ import "./ha-tree-indicator";
import "./target-picker/ha-target-picker-item-group";
import "./target-picker/ha-target-picker-value-chip";
const EMPTY_SEARCH = "___EMPTY_SEARCH___";
const SEPARATOR = "________";
const CREATE_ID = "___create-new-entity___";
@@ -634,6 +634,35 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
return undefined;
}
private _getRowType = (
item:
| PickerComboBoxItem
| (FloorComboBoxItem & { last?: boolean | undefined })
| EntityComboBoxItem
| DevicePickerItem
) => {
if (
(item as FloorComboBoxItem).type === "area" ||
(item as FloorComboBoxItem).type === "floor"
) {
return (item as FloorComboBoxItem).type;
}
if ("domain" in item) {
return "device";
}
if ("stateObj" in item) {
return "entity";
}
if (item.id === EMPTY_SEARCH) {
return "empty";
}
return "label";
};
private _sectionTitleFunction = ({
firstIndex,
lastIndex,
@@ -657,7 +686,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
return undefined;
}
const type = getTargetComboBoxItemType(firstItem as PickerComboBoxItem);
const type = this._getRowType(firstItem as PickerComboBoxItem);
const translationType:
| "areas"
| "entities"
@@ -829,10 +858,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
if (!filterType || filterType === "label") {
let labels = this._getLabelsMemoized(
this.hass.states,
this.hass.areas,
this.hass.devices,
this.hass.entities,
this.hass,
this._labelRegistry,
includeDomains,
undefined,
@@ -948,7 +974,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
return nothing;
}
const type = getTargetComboBoxItemType(item);
const type = this._getRowType(item);
let hasFloor = false;
let rtl = false;
let showEntityId = false;

View File

@@ -1,5 +1,3 @@
import "@home-assistant/webawesome/dist/components/dialog/dialog";
import { mdiClose } from "@mdi/js";
import { css, html, LitElement } from "lit";
import {
customElement,
@@ -9,6 +7,8 @@ import {
state,
} from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { mdiClose } from "@mdi/js";
import "@home-assistant/webawesome/dist/components/dialog/dialog";
import { fireEvent } from "../common/dom/fire_event";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
@@ -172,9 +172,7 @@ export class HaWaDialog extends LitElement {
await this.updateComplete;
requestAnimationFrame(() => {
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
});
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
};
private _handleAfterShow = () => {
@@ -237,7 +235,7 @@ export class HaWaDialog extends LitElement {
}
:host([width="large"]) wa-dialog {
--width: min(var(--ha-dialog-width-lg, 1024px), var(--full-width));
--width: min(var(--ha-dialog-width-lg, 720px), var(--full-width));
}
:host([width="full"]) wa-dialog {

View File

@@ -21,52 +21,11 @@ export interface FloorComboBoxItem extends PickerComboBoxItem {
area?: AreaRegistryEntry;
}
export interface FloorNestedComboBoxItem extends PickerComboBoxItem {
floor?: FloorRegistryEntry;
areas: FloorComboBoxItem[];
}
export interface UnassignedAreasFloorComboBoxItem extends PickerComboBoxItem {
areas: FloorComboBoxItem[];
}
export interface AreaFloorValue {
id: string;
type: "floor" | "area";
}
export const getAreasNestedInFloors = (
states: HomeAssistant["states"],
haFloors: HomeAssistant["floors"],
haAreas: HomeAssistant["areas"],
haDevices: HomeAssistant["devices"],
haEntities: HomeAssistant["entities"],
formatId: (value: AreaFloorValue) => string,
includeDomains?: string[],
excludeDomains?: string[],
includeDeviceClasses?: string[],
deviceFilter?: HaDevicePickerDeviceFilterFunc,
entityFilter?: HaEntityPickerEntityFilterFunc,
excludeAreas?: string[],
excludeFloors?: string[]
) =>
getAreasAndFloorsItems(
states,
haFloors,
haAreas,
haDevices,
haEntities,
formatId,
includeDomains,
excludeDomains,
includeDeviceClasses,
deviceFilter,
entityFilter,
excludeAreas,
excludeFloors,
true
) as (FloorNestedComboBoxItem | UnassignedAreasFloorComboBoxItem)[];
export const getAreasAndFloors = (
states: HomeAssistant["states"],
haFloors: HomeAssistant["floors"],
@@ -81,43 +40,7 @@ export const getAreasAndFloors = (
entityFilter?: HaEntityPickerEntityFilterFunc,
excludeAreas?: string[],
excludeFloors?: string[]
) =>
getAreasAndFloorsItems(
states,
haFloors,
haAreas,
haDevices,
haEntities,
formatId,
includeDomains,
excludeDomains,
includeDeviceClasses,
deviceFilter,
entityFilter,
excludeAreas,
excludeFloors
) as FloorComboBoxItem[];
const getAreasAndFloorsItems = (
states: HomeAssistant["states"],
haFloors: HomeAssistant["floors"],
haAreas: HomeAssistant["areas"],
haDevices: HomeAssistant["devices"],
haEntities: HomeAssistant["entities"],
formatId: (value: AreaFloorValue) => string,
includeDomains?: string[],
excludeDomains?: string[],
includeDeviceClasses?: string[],
deviceFilter?: HaDevicePickerDeviceFilterFunc,
entityFilter?: HaEntityPickerEntityFilterFunc,
excludeAreas?: string[],
excludeFloors?: string[],
nested = false
): (
| FloorComboBoxItem
| FloorNestedComboBoxItem
| UnassignedAreasFloorComboBoxItem
)[] => {
): FloorComboBoxItem[] => {
const floors = Object.values(haFloors);
const areas = Object.values(haAreas);
const devices = Object.values(haDevices);
@@ -258,11 +181,7 @@ const getAreasAndFloorsItems = (
const hierarchy = getAreasFloorHierarchy(floors, outputAreas);
const items: (
| FloorComboBoxItem
| FloorNestedComboBoxItem
| UnassignedAreasFloorComboBoxItem
)[] = [];
const items: FloorComboBoxItem[] = [];
hierarchy.floors.forEach((f) => {
const floor = haFloors[f.id];
@@ -277,7 +196,7 @@ const getAreasAndFloorsItems = (
})
.flat();
const floorItem: FloorComboBoxItem | FloorNestedComboBoxItem = {
items.push({
id: formatId({ id: floor.floor_id, type: "floor" }),
type: "floor",
primary: floorName,
@@ -289,53 +208,41 @@ const getAreasAndFloorsItems = (
...floor.aliases,
...areaSearchLabels,
],
};
});
items.push(floorItem);
items.push(
...floorAreas.map((area) => {
const areaName = computeAreaName(area);
return {
id: formatId({ id: area.area_id, type: "area" }),
type: "area" as const,
primary: areaName || area.area_id,
area: area,
icon: area.icon || undefined,
search_labels: [
area.area_id,
...(areaName ? [areaName] : []),
...area.aliases,
],
};
})
);
});
const floorAreasItems = floorAreas.map((area) => {
const areaName = computeAreaName(area);
items.push(
...hierarchy.areas.map((areaId) => {
const area = haAreas[areaId];
const areaName = computeAreaName(area) || area.area_id;
return {
id: formatId({ id: area.area_id, type: "area" }),
type: "area" as const,
primary: areaName || area.area_id,
primary: areaName,
area: area,
icon: area.icon || undefined,
search_labels: [
area.area_id,
...(areaName ? [areaName] : []),
...area.aliases,
],
search_labels: [area.area_id, areaName, ...area.aliases],
};
});
if (nested && floor) {
(floorItem as unknown as FloorNestedComboBoxItem).areas = floorAreasItems;
} else {
items.push(...floorAreasItems);
}
});
const unassignedAreaItems = hierarchy.areas.map((areaId) => {
const area = haAreas[areaId];
const areaName = computeAreaName(area) || area.area_id;
return {
id: formatId({ id: area.area_id, type: "area" }),
type: "area" as const,
primary: areaName,
area: area,
icon: area.icon || undefined,
search_labels: [area.area_id, areaName, ...area.aliases],
};
});
if (nested && unassignedAreaItems.length) {
items.push({
areas: unassignedAreaItems,
} as UnassignedAreasFloorComboBoxItem);
} else {
items.push(...unassignedAreaItems);
}
})
);
return items;
};

View File

@@ -1,10 +1,7 @@
import { stringCompare } from "../common/string/compare";
import type { HomeAssistant } from "../types";
import type { DeviceRegistryEntry } from "./device_registry";
import type {
EntityRegistryDisplayEntry,
EntityRegistryEntry,
} from "./entity_registry";
import type { EntityRegistryEntry } from "./entity_registry";
import type { RegistryEntry } from "./registry";
export { subscribeAreaRegistry } from "./ws-area_registry";
@@ -21,10 +18,7 @@ export interface AreaRegistryEntry extends RegistryEntry {
temperature_entity_id: string | null;
}
export type AreaEntityLookup = Record<
string,
(EntityRegistryEntry | EntityRegistryDisplayEntry)[]
>;
export type AreaEntityLookup = Record<string, EntityRegistryEntry[]>;
export type AreaDeviceLookup = Record<string, DeviceRegistryEntry[]>;
@@ -75,7 +69,7 @@ export const reorderAreaRegistryEntries = (
});
export const getAreaEntityLookup = (
entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[]
entities: EntityRegistryEntry[]
): AreaEntityLookup => {
const areaEntityLookup: AreaEntityLookup = {};
for (const entity of entities) {

View File

@@ -114,6 +114,12 @@ export interface StateTrigger extends BaseTrigger {
for?: string | number | ForDict;
}
export interface MqttTrigger extends BaseTrigger {
trigger: "mqtt";
topic: string;
payload?: string;
}
export interface GeoLocationTrigger extends BaseTrigger {
trigger: "geo_location";
source: string;
@@ -121,12 +127,6 @@ export interface GeoLocationTrigger extends BaseTrigger {
event: "enter" | "leave";
}
export interface MqttTrigger extends BaseTrigger {
trigger: "mqtt";
topic: string;
payload?: string;
}
export interface HassTrigger extends BaseTrigger {
trigger: "homeassistant";
event: "start" | "shutdown";

View File

@@ -137,12 +137,8 @@ const getCalendarDate = (dateObj: any): string | undefined => {
return undefined;
};
export const getCalendars = (
hass: HomeAssistant,
element: Element
): Calendar[] => {
const computedStyles = getComputedStyle(element);
return Object.keys(hass.states)
export const getCalendars = (hass: HomeAssistant): Calendar[] =>
Object.keys(hass.states)
.filter(
(eid) =>
computeDomain(eid) === "calendar" &&
@@ -153,9 +149,8 @@ export const getCalendars = (
.map((eid, idx) => ({
...hass.states[eid],
name: computeStateName(hass.states[eid]),
backgroundColor: getColorByIndex(idx, computedStyles),
backgroundColor: getColorByIndex(idx),
}));
};
export const createCalendarEvent = (
hass: HomeAssistant,

View File

@@ -17,19 +17,11 @@ export const CONDITION_COLLECTIONS: AutomationElementGroupCollection[] = [
},
},
},
{
titleKey:
"ui.panel.config.automation.editor.conditions.groups.helpers.label",
groups: {
helpers: {},
},
},
{
titleKey: "ui.panel.config.automation.editor.conditions.groups.other.label",
groups: {
template: {},
trigger: {},
other: {},
},
},
] as const;

View File

@@ -50,11 +50,7 @@ export type DeviceEntityDisplayLookup = Record<
EntityRegistryDisplayEntry[]
>;
export type DeviceEntityLookup<
T extends EntityRegistryEntry | EntityRegistryDisplayEntry =
| EntityRegistryEntry
| EntityRegistryDisplayEntry,
> = Record<string, T[]>;
export type DeviceEntityLookup = Record<string, EntityRegistryEntry[]>;
export interface DeviceRegistryEntryMutableParams {
area_id?: string | null;
@@ -111,7 +107,7 @@ export const sortDeviceRegistryByName = (
);
export const getDeviceEntityLookup = (
entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[]
entities: EntityRegistryEntry[]
): DeviceEntityLookup => {
const deviceEntityLookup: DeviceEntityLookup = {};
for (const entity of entities) {

View File

@@ -1,14 +0,0 @@
import type { HomeAssistant } from "../types";
export interface ESPHomeEncryptionKey {
encryption_key: string;
}
export const fetchESPHomeEncryptionKey = (
hass: HomeAssistant,
entry_id: string
): Promise<ESPHomeEncryptionKey> =>
hass.callWS({
type: "esphome/get_encryption_key",
entry_id,
});

View File

@@ -1,8 +1,8 @@
import type { Connection } from "home-assistant-js-websocket";
import { createCollection } from "home-assistant-js-websocket";
import type { LocalizeFunc } from "../common/translations/localize";
import { debounce } from "../common/util/debounce";
import type { HomeAssistant } from "../types";
import { debounce } from "../common/util/debounce";
export const integrationsWithPanel = {
bluetooth: "config/bluetooth",
@@ -25,8 +25,6 @@ export type IntegrationType =
| "entity"
| "system";
export type DomainManifestLookup = Record<string, IntegrationManifest>;
export interface IntegrationManifest {
is_built_in: boolean;
overwrites_built_in?: boolean;

View File

@@ -101,10 +101,7 @@ export const deleteLabelRegistryEntry = (
});
export const getLabels = (
hassStates: HomeAssistant["states"],
hassAreas: HomeAssistant["areas"],
hassDevices: HomeAssistant["devices"],
hassEntities: HomeAssistant["entities"],
hass: HomeAssistant,
labels?: LabelRegistryEntry[],
includeDomains?: string[],
excludeDomains?: string[],
@@ -118,8 +115,8 @@ export const getLabels = (
return [];
}
const devices = Object.values(hassDevices);
const entities = Object.values(hassEntities);
const devices = Object.values(hass.devices);
const entities = Object.values(hass.entities);
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
let inputDevices: DeviceRegistryEntry[] | undefined;
@@ -173,7 +170,7 @@ export const getLabels = (
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = hassStates[entity.entity_id];
const stateObj = hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
@@ -184,9 +181,8 @@ export const getLabels = (
});
});
inputEntities = inputEntities!.filter((entity) => {
const stateObj = hassStates[entity.entity_id];
const stateObj = hass.states[entity.entity_id];
return (
stateObj &&
stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class)
);
@@ -204,7 +200,7 @@ export const getLabels = (
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = hassStates[entity.entity_id];
const stateObj = hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
@@ -212,7 +208,7 @@ export const getLabels = (
});
});
inputEntities = inputEntities!.filter((entity) => {
const stateObj = hassStates[entity.entity_id];
const stateObj = hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
@@ -249,8 +245,8 @@ export const getLabels = (
if (areaIds) {
areaIds.forEach((areaId) => {
const area = hassAreas[areaId];
area?.labels.forEach((label) => usedLabels.add(label));
const area = hass.areas[areaId];
area.labels.forEach((label) => usedLabels.add(label));
});
}

View File

@@ -1,10 +1,7 @@
import type { MediaSelectorValue } from "../../selector";
import type { LovelaceBadgeConfig } from "./badge";
import type { LovelaceCardConfig } from "./card";
import type {
LovelaceSectionConfig,
LovelaceSectionRawConfig,
} from "./section";
import type { LovelaceSectionRawConfig } from "./section";
import type { LovelaceStrategyConfig } from "./strategy";
export interface ShowViewConfig {
@@ -36,12 +33,6 @@ export interface LovelaceViewHeaderConfig {
badges_wrap?: "wrap" | "scroll";
}
export interface LovelaceViewSidebarConfig {
sections?: LovelaceSectionConfig[];
content_label?: string;
sidebar_label?: string;
}
export interface LovelaceBaseViewConfig {
index?: number;
title?: string;
@@ -65,8 +56,6 @@ export interface LovelaceViewConfig extends LovelaceBaseViewConfig {
cards?: LovelaceCardConfig[];
sections?: LovelaceSectionRawConfig[];
header?: LovelaceViewHeaderConfig;
// Only used for section view, it should move to a section view config type when the views will have dedicated editor.
sidebar?: LovelaceViewSidebarConfig;
}
export interface LovelaceStrategyViewConfig extends LovelaceBaseViewConfig {

View File

@@ -219,13 +219,9 @@ const tryDescribeAction = <T extends ActionType>(
if (config.action) {
const [domain, serviceName] = config.action.split(".", 2);
const descriptionPlaceholders =
hass.services[domain][serviceName].description_placeholders;
const service =
hass.localize(
`component.${domain}.services.${serviceName}.name`,
descriptionPlaceholders
) || hass.services[domain][serviceName]?.name;
hass.localize(`component.${domain}.services.${serviceName}.name`) ||
hass.services[domain][serviceName]?.name;
if (config.metadata) {
return hass.localize(

View File

@@ -332,6 +332,7 @@ export interface NumberSelector {
max?: number;
step?: number | "any";
mode?: "box" | "slider";
entity?: { domains: readonly string[] };
unit_of_measurement?: string;
slider_ticks?: boolean;
translation_key?: string;

View File

@@ -1,30 +1,15 @@
import type { HassServiceTarget } from "home-assistant-js-websocket";
import { computeDomain } from "../common/entity/compute_domain";
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
import type { HomeAssistant } from "../types";
import type { FloorComboBoxItem } from "./area_floor";
import type { AreaRegistryEntry } from "./area_registry";
import type { DevicePickerItem, DeviceRegistryEntry } from "./device_registry";
import type { DeviceRegistryEntry } from "./device_registry";
import type { HaEntityPickerEntityFilterFunc } from "./entity";
import type {
EntityComboBoxItem,
EntityRegistryDisplayEntry,
} from "./entity_registry";
export const TARGET_SEPARATOR = "________";
import type { EntityRegistryDisplayEntry } from "./entity_registry";
export type TargetType = "entity" | "device" | "area" | "label" | "floor";
export type TargetTypeFloorless = Exclude<TargetType, "floor">;
export interface SingleHassServiceTarget {
entity_id?: string;
device_id?: string;
area_id?: string;
floor_id?: string;
label_id?: string;
}
export interface ExtractFromTargetResult {
missing_areas: string[];
missing_devices: string[];
@@ -50,39 +35,6 @@ export const extractFromTarget = async (
target,
});
export const getTriggersForTarget = async (
callWS: HomeAssistant["callWS"],
target: HassServiceTarget,
expandGroup = true
) =>
callWS<string[]>({
type: "get_triggers_for_target",
target,
expand_group: expandGroup,
});
export const getConditionsForTarget = async (
callWS: HomeAssistant["callWS"],
target: HassServiceTarget,
expandGroup = true
) =>
callWS<string[]>({
type: "get_conditions_for_target",
target,
expand_group: expandGroup,
});
export const getServicesForTarget = async (
callWS: HomeAssistant["callWS"],
target: HassServiceTarget,
expandGroup = true
) =>
callWS<string[]>({
type: "get_services_for_target",
target,
expand_group: expandGroup,
});
export const areaMeetsFilter = (
area: AreaRegistryEntry,
devices: HomeAssistant["devices"],
@@ -210,32 +162,3 @@ export const entityRegMeetsFilter = (
}
return true;
};
export const getTargetComboBoxItemType = (
item:
| PickerComboBoxItem
| (FloorComboBoxItem & { last?: boolean | undefined })
| EntityComboBoxItem
| DevicePickerItem
) => {
if (
(item as FloorComboBoxItem).type === "area" ||
(item as FloorComboBoxItem).type === "floor"
) {
return (item as FloorComboBoxItem).type;
}
if ("domain" in item) {
return "device";
}
if ("stateObj" in item) {
return "entity";
}
if (item.id === "___EMPTY_SEARCH___") {
return "empty";
}
return "label";
};

View File

@@ -28,24 +28,18 @@ export const TRIGGER_COLLECTIONS: AutomationElementGroupCollection[] = [
},
},
},
{
titleKey: "ui.panel.config.automation.editor.triggers.groups.helpers.label",
groups: {
helpers: {},
},
},
{
titleKey: "ui.panel.config.automation.editor.triggers.groups.other.label",
groups: {
event: {},
geo_location: {},
homeassistant: {},
mqtt: {},
conversation: {},
tag: {},
template: {},
webhook: {},
persistent_notification: {},
other: {},
},
},
] as const;

View File

@@ -1,15 +1,15 @@
import { mdiAlertOutline, mdiClose } from "@mdi/js";
import { mdiAlertOutline } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-button";
import "../../components/ha-dialog-footer";
import "../../components/ha-dialog-header";
import "../../components/ha-md-dialog";
import type { HaMdDialog } from "../../components/ha-md-dialog";
import "../../components/ha-svg-icon";
import "../../components/ha-textfield";
import type { HaTextField } from "../../components/ha-textfield";
import "../../components/ha-wa-dialog";
import type { HomeAssistant } from "../../types";
import type { DialogBoxParams } from "./show-dialog-box";
@@ -19,12 +19,12 @@ class DialogBox extends LitElement {
@state() private _params?: DialogBoxParams;
@state() private _open = false;
@state() private _closeState?: "canceled" | "confirmed";
@query("ha-textfield") private _textField?: HaTextField;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
private _closePromise?: Promise<void>;
private _closeResolve?: () => void;
@@ -34,7 +34,6 @@ class DialogBox extends LitElement {
await this._closePromise;
}
this._params = params;
this._open = true;
}
public closeDialog(): boolean {
@@ -61,25 +60,16 @@ class DialogBox extends LitElement {
this.hass.localize("ui.dialogs.generic.default_confirmation_title"));
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
?prevent-scrim-close=${confirmPrompt}
<ha-md-dialog
open
.disableCancelAction=${confirmPrompt}
@closed=${this._dialogClosed}
type="alert"
aria-labelledby="dialog-box-title"
aria-describedby="dialog-box-description"
>
<ha-dialog-header slot="header">
${!confirmPrompt
? html`<slot name="headerNavigationIcon" slot="navigationIcon">
<ha-icon-button
data-dialog="close"
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
.path=${mdiClose}
></ha-icon-button
></slot>`
: nothing}
<span slot="title" id="dialog-box-title">
<div slot="headline">
<span .title=${dialogTitle} id="dialog-box-title">
${this._params.warning
? html`<ha-svg-icon
.path=${mdiAlertOutline}
@@ -88,13 +78,13 @@ class DialogBox extends LitElement {
: nothing}
${dialogTitle}
</span>
</ha-dialog-header>
<div id="dialog-box-description">
</div>
<div slot="content" id="dialog-box-description">
${this._params.text ? html` <p>${this._params.text}</p> ` : ""}
${this._params.prompt
? html`
<ha-textfield
autofocus
dialogInitialFocus
value=${ifDefined(this._params.defaultValue)}
.placeholder=${this._params.placeholder}
.label=${this._params.inputLabel
@@ -109,11 +99,10 @@ class DialogBox extends LitElement {
`
: ""}
</div>
<ha-dialog-footer slot="footer">
<div slot="actions">
${confirmPrompt
? html`
<ha-button
slot="secondaryAction"
@click=${this._dismiss}
?autofocus=${!this._params.prompt && this._params.destructive}
appearance="plain"
@@ -125,7 +114,6 @@ class DialogBox extends LitElement {
`
: nothing}
<ha-button
slot="primaryAction"
@click=${this._confirm}
?autofocus=${!this._params.prompt && !this._params.destructive}
variant=${this._params.destructive ? "danger" : "brand"}
@@ -134,8 +122,8 @@ class DialogBox extends LitElement {
? this._params.confirmText
: this.hass.localize("ui.common.ok")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
</div>
</ha-md-dialog>
`;
}
@@ -160,20 +148,20 @@ class DialogBox extends LitElement {
}
private _closeDialog() {
this._open = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
this._dialog?.close();
this._closePromise = new Promise((resolve) => {
this._closeResolve = resolve;
});
}
private _dialogClosed() {
fireEvent(this, "dialog-closed", { dialog: this.localName });
if (!this._closeState) {
fireEvent(this, "dialog-closed", { dialog: this.localName });
this._cancel();
}
this._closeState = undefined;
this._params = undefined;
this._open = false;
this._closeResolve?.();
this._closeResolve = undefined;
}

View File

@@ -87,7 +87,7 @@ class PanelCalendar extends LitElement {
public willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
this._calendars = getCalendars(this.hass, this);
this._calendars = getCalendars(this.hass);
}
}
@@ -243,7 +243,7 @@ class PanelCalendar extends LitElement {
manifest: await fetchIntegrationManifest(this.hass, "local_calendar"),
dialogClosedCallback: ({ flowFinished }) => {
if (flowFinished) {
this._calendars = getCalendars(this.hass, this);
this._calendars = getCalendars(this.hass);
}
},
});

View File

@@ -84,8 +84,6 @@ export class HaConfigAreasDashboard extends LitElement {
@property({ attribute: false }) public route!: Route;
private _searchParms = new URLSearchParams(window.location.search);
@state() private _hierarchy?: AreasFloorHierarchy;
private _blockHierarchyUpdate = false;
@@ -169,9 +167,7 @@ export class HaConfigAreasDashboard extends LitElement {
.hass=${this.hass}
.narrow=${this.narrow}
.isWide=${this.isWide}
.backPath=${this._searchParms.has("historyBack")
? undefined
: "/config"}
back-path="/config"
.tabs=${configSections.areas}
.route=${this.route}
has-fab

View File

@@ -1,6 +1,5 @@
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple";
import type { HassServiceTarget } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, queryAll, state } from "lit/decorators";
@@ -214,7 +213,7 @@ export default class HaAutomationAction extends LitElement {
});
}
private _addAction = (action: string, target?: HassServiceTarget) => {
private _addAction = (action: string) => {
let actions: Action[];
if (action === PASTE_VALUE) {
actions = this.actions.concat(deepClone(this._clipboard!.action));
@@ -224,7 +223,6 @@ export default class HaAutomationAction extends LitElement {
actions = this.actions.concat({
action: getValueFromDynamic(action),
metadata: {},
target,
});
} else {
const elClass = customElements.get(

File diff suppressed because it is too large Load Diff

View File

@@ -1,388 +0,0 @@
import {
mdiInformationOutline,
mdiLabel,
mdiPlus,
mdiTextureBox,
} from "@mdi/js";
import { LitElement, css, html, nothing, type TemplateResult } from "lit";
import {
customElement,
eventOptions,
property,
query,
state,
} from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/entity/state-badge";
import "../../../../components/ha-domain-icon";
import "../../../../components/ha-floor-icon";
import "../../../../components/ha-icon-next";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-tooltip";
import type { ConfigEntry } from "../../../../data/config_entries";
import type { HomeAssistant } from "../../../../types";
import type { AddAutomationElementListItem } from "../add-automation-element-dialog";
type Target = [string, string | undefined, string | undefined];
@customElement("ha-automation-add-items")
export class HaAutomationAddItems extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public items?: {
title: string;
items: AddAutomationElementListItem[];
}[];
@property() public error?: string;
@property({ attribute: "select-label" }) public selectLabel!: string;
@property({ attribute: "empty-label" }) public emptyLabel!: string;
@property({ attribute: false }) public target?: Target;
@property({ attribute: false }) public getLabel!: (
id: string
) => { name: string; icon?: string } | undefined;
@property({ attribute: false }) public configEntryLookup: Record<
string,
ConfigEntry
> = {};
@property({ type: Boolean, attribute: "tooltip-description" })
public tooltipDescription = false;
@state() private _itemsScrolled = false;
@query(".items")
private _itemsDiv!: HTMLDivElement;
protected render() {
return html`<div
class=${classMap({
items: true,
blank: this.error || !this.items || !this.items.length,
error: this.error,
scrolled: this._itemsScrolled,
})}
@scroll=${this._onItemsScroll}
>
${!this.items && !this.error
? this.selectLabel
: this.error
? html`${this.error}
<div>${this._renderTarget(this.target)}</div>`
: this.items && !this.items.length
? html`${this.emptyLabel}
${this.target
? html`<div>${this._renderTarget(this.target)}</div>`
: nothing}`
: repeat(
this.items,
(_, index) => `item-group-${index}`,
(itemGroup) =>
this._renderItemList(itemGroup.title, itemGroup.items)
)}
</div>`;
}
private _renderItemList(title, items?: AddAutomationElementListItem[]) {
if (!items || !items.length) {
return nothing;
}
return html`
<div class="items-title">${title}</div>
<ha-md-list>
${repeat(
items,
(item) => item.key,
(item) => html`
<ha-md-list-item
interactive
type="button"
.value=${item.key}
@click=${this._selected}
>
<div slot="headline" class=${this.target ? "item-headline" : ""}>
${item.name}${this._renderTarget(this.target)}
</div>
${!this.tooltipDescription && item.description
? html`<div slot="supporting-text">${item.description}</div>`
: nothing}
${item.icon
? html`<span slot="start">${item.icon}</span>`
: item.iconPath
? html`<ha-svg-icon
slot="start"
.path=${item.iconPath}
></ha-svg-icon>`
: nothing}
${this.tooltipDescription && item.description
? html`<ha-svg-icon
tabindex="0"
id=${`description-tooltip-${item.key}`}
slot="end"
.path=${mdiInformationOutline}
@click=${stopPropagation}
></ha-svg-icon>
<ha-tooltip
.for=${`description-tooltip-${item.key}`}
@wa-show=${stopPropagation}
@wa-hide=${stopPropagation}
@wa-after-hide=${stopPropagation}
@wa-after-show=${stopPropagation}
>${item.description}</ha-tooltip
> `
: nothing}
<ha-svg-icon
slot="end"
class="plus"
.path=${mdiPlus}
></ha-svg-icon>
</ha-md-list-item>
`
)}
</ha-md-list>
`;
}
private _renderTarget = memoizeOne((target?: Target) => {
if (!target) {
return nothing;
}
return html`<div class="selected-target">
${this._getSelectedTargetIcon(target[0], target[1])}
<div class="label">${target[2]}</div>
</div>`;
});
private _getSelectedTargetIcon(
targetType: string,
targetId: string | undefined
): TemplateResult | typeof nothing {
if (!targetId) {
return nothing;
}
if (targetType === "floor") {
return html`<ha-floor-icon
.floor=${this.hass.floors[targetId]}
></ha-floor-icon>`;
}
if (targetType === "area" && this.hass.areas[targetId]) {
const area = this.hass.areas[targetId];
if (area.icon) {
return html`<ha-icon .icon=${area.icon}></ha-icon>`;
}
return html`<ha-svg-icon .path=${mdiTextureBox}></ha-svg-icon>`;
}
if (targetType === "device" && this.hass.devices[targetId]) {
const device = this.hass.devices[targetId];
const configEntry = device.primary_config_entry
? this.configEntryLookup[device.primary_config_entry]
: undefined;
const domain = configEntry?.domain;
if (domain) {
return html`<ha-domain-icon
slot="start"
.hass=${this.hass}
.domain=${domain}
brand-fallback
></ha-domain-icon>`;
}
}
if (targetType === "entity" && this.hass.states[targetId]) {
const stateObj = this.hass.states[targetId];
if (stateObj) {
return html`<state-badge
.stateObj=${stateObj}
.hass=${this.hass}
.stateColor=${false}
></state-badge>`;
}
}
if (targetType === "label") {
const label = this.getLabel(targetId);
if (label?.icon) {
return html`<ha-icon .icon=${label.icon}></ha-icon>`;
}
return html`<ha-svg-icon .path=${mdiLabel}></ha-svg-icon>`;
}
return nothing;
}
private _selected(ev) {
const item = ev.currentTarget;
fireEvent(this, "value-changed", {
value: item.value,
});
}
@eventOptions({ passive: true })
private _onItemsScroll(ev) {
const top = ev.target.scrollTop ?? 0;
this._itemsScrolled = top > 0;
}
public override scrollTo(options?: ScrollToOptions): void;
public override scrollTo(x: number, y: number): void;
public override scrollTo(
xOrOptions?: number | ScrollToOptions,
y?: number
): void {
if (typeof xOrOptions === "number") {
this._itemsDiv?.scrollTo(xOrOptions, y!);
} else {
this._itemsDiv?.scrollTo(xOrOptions);
}
}
static styles = css`
:host {
display: flex;
}
.items {
display: flex;
flex-direction: column;
overflow: auto;
flex: 1;
}
.items.blank {
border-radius: var(--ha-border-radius-xl);
background-color: var(--ha-color-surface-default);
align-items: center;
color: var(--ha-color-text-secondary);
padding: var(--ha-space-0);
margin: var(--ha-space-0) var(--ha-space-4)
max(var(--safe-area-inset-bottom), var(--ha-space-3));
line-height: var(--ha-line-height-expanded);
justify-content: center;
}
.items.error {
background-color: var(--ha-color-fill-danger-quiet-resting);
color: var(--ha-color-on-danger-normal);
}
.items ha-md-list {
--md-list-item-two-line-container-height: var(--ha-space-12);
--md-list-item-leading-space: var(--ha-space-3);
--md-list-item-trailing-space: var(--md-list-item-leading-space);
--md-list-item-bottom-space: var(--ha-space-2);
--md-list-item-top-space: var(--md-list-item-bottom-space);
--md-list-item-supporting-text-font: var(--ha-font-family-body);
--ha-md-list-item-gap: var(--ha-space-3);
gap: var(--ha-space-2);
padding: var(--ha-space-0) var(--ha-space-4);
}
.items ha-md-list ha-md-list-item {
border-radius: var(--ha-border-radius-lg);
border: 1px solid var(--ha-color-border-neutral-quiet);
}
.items ha-md-list {
padding-bottom: max(var(--safe-area-inset-bottom), var(--ha-space-3));
}
.items .item-headline {
display: flex;
align-items: center;
gap: var(--ha-space-2);
min-height: var(--ha-space-9);
flex-wrap: wrap;
}
.items-title {
position: sticky;
display: flex;
align-items: center;
font-weight: var(--ha-font-weight-medium);
padding-top: var(--ha-space-2);
padding-bottom: var(--ha-space-2);
padding-inline-start: var(--ha-space-8);
padding-inline-end: var(--ha-space-3);
top: 0;
z-index: 1;
background-color: var(--card-background-color);
}
ha-bottom-sheet .items-title {
padding-top: var(--ha-space-3);
}
.scrolled .items-title:first-of-type {
box-shadow: var(--bar-box-shadow);
border-bottom: 1px solid var(--ha-color-border-neutral-quiet);
}
ha-icon-next {
width: var(--ha-space-6);
}
ha-svg-icon.plus {
color: var(--primary-color);
}
.selected-target {
display: inline-flex;
gap: var(--ha-space-1);
justify-content: center;
align-items: center;
border-radius: var(--ha-border-radius-md);
background: var(--ha-color-fill-neutral-normal-resting);
padding: 0 var(--ha-space-2) 0 var(--ha-space-1);
color: var(--ha-color-on-neutral-normal);
overflow: hidden;
}
.selected-target .label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.selected-target ha-icon,
.selected-target ha-svg-icon,
.selected-target state-badge,
.selected-target ha-domain-icon {
display: flex;
padding: var(--ha-space-1) 0;
}
.selected-target state-badge {
--mdc-icon-size: 24px;
}
.selected-target state-badge,
.selected-target ha-floor-icon {
display: flex;
height: 32px;
width: 24px;
align-items: center;
}
.selected-target ha-domain-icon {
filter: grayscale(100%);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-add-items": HaAutomationAddItems;
}
}

View File

@@ -1,9 +1,5 @@
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple";
import type {
HassServiceTarget,
UnsubscribeFunc,
} from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, queryAll, state } from "lit/decorators";
@@ -28,7 +24,6 @@ import {
CONDITION_BUILDING_BLOCKS,
subscribeConditions,
} from "../../../../data/condition";
import { subscribeLabFeatures } from "../../../../data/labs";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../../types";
import {
@@ -78,52 +73,19 @@ export default class HaAutomationCondition extends SubscribeMixin(LitElement) {
private _conditionKeys = new WeakMap<Condition, string>();
private _unsub?: Promise<UnsubscribeFunc>;
// @ts-ignore
@state() private _newTriggersAndConditions = false;
public disconnectedCallback() {
super.disconnectedCallback();
this._unsubscribe();
}
protected hassSubscribe() {
return [
subscribeLabFeatures(this.hass!.connection, (features) => {
this._newTriggersAndConditions =
features.find(
(feature) =>
feature.domain === "automation" &&
feature.preview_feature === "new_triggers_conditions"
)?.enabled ?? false;
}),
subscribeConditions(this.hass, (conditions) =>
this._addConditions(conditions)
),
];
}
private _subscribeDescriptions() {
this._unsubscribe();
this._conditionDescriptions = {};
this._unsub = subscribeConditions(this.hass, (descriptions) => {
this._conditionDescriptions = {
...this._conditionDescriptions,
...descriptions,
};
});
}
private _unsubscribe() {
if (this._unsub) {
this._unsub.then((unsub) => unsub());
this._unsub = undefined;
}
}
protected willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (changedProperties.has("_newTriggersAndConditions")) {
this._subscribeDescriptions();
}
private _addConditions(conditions: ConditionDescriptions) {
this._conditionDescriptions = {
...this._conditionDescriptions,
...conditions,
};
}
protected firstUpdated(changedProps: PropertyValues) {
@@ -299,7 +261,7 @@ export default class HaAutomationCondition extends SubscribeMixin(LitElement) {
});
}
private _addCondition = (value: string, target?: HassServiceTarget) => {
private _addCondition = (value) => {
let conditions: Condition[];
if (value === PASTE_VALUE) {
conditions = this.conditions.concat(
@@ -308,7 +270,6 @@ export default class HaAutomationCondition extends SubscribeMixin(LitElement) {
} else if (isDynamic(value)) {
conditions = this.conditions.concat({
condition: getValueFromDynamic(value),
target,
});
} else {
const condition = value as Condition["condition"];

View File

@@ -69,45 +69,6 @@ export class HaPlatformCondition extends LitElement {
} else {
this._manifest = undefined;
}
if (
oldValue?.condition !== this.condition?.condition &&
this.condition &&
this.description?.fields
) {
let updatedDefaultValue = false;
const updatedOptions = {};
const loadDefaults = !("options" in this.condition);
// Set mandatory bools without a default value to false
Object.entries(this.description.fields).forEach(([key, field]) => {
if (
field.selector &&
field.required &&
field.default === undefined &&
"boolean" in field.selector &&
updatedOptions[key] === undefined
) {
updatedDefaultValue = true;
updatedOptions[key] = false;
} else if (
loadDefaults &&
field.selector &&
field.default !== undefined &&
updatedOptions[key] === undefined
) {
updatedDefaultValue = true;
updatedOptions[key] = field.default;
}
});
if (updatedDefaultValue) {
fireEvent(this, "value-changed", {
value: {
...this.condition,
options: updatedOptions,
},
});
}
}
}
protected render() {
@@ -393,10 +354,6 @@ export class HaPlatformCondition extends LitElement {
}
static styles = css`
:host {
display: block;
margin: 0px calc(-1 * var(--ha-space-4));
}
ha-settings-row {
padding: 0 var(--ha-space-4);
}

View File

@@ -1,11 +1,10 @@
import type { HassServiceTarget } from "home-assistant-js-websocket";
import { fireEvent } from "../../../common/dom/fire_event";
export const PASTE_VALUE = "__paste__";
export interface AddAutomationElementDialogParams {
type: "trigger" | "condition" | "action";
add: (key: string, target?: HassServiceTarget) => void;
add: (key: string) => void;
clipboardItem: string | undefined;
}
const loadDialog = () => import("./add-automation-element-dialog");

View File

@@ -93,12 +93,8 @@ export default class HaAutomationSidebarAction extends LitElement {
".",
2
);
title = `${domainToName(this.hass.localize, domain)}: ${
this.hass.localize(
`component.${domain}.services.${service}.name`,
this.hass.services[domain][service].description_placeholders
) ||
this.hass.localize(`component.${domain}.services.${service}.name`) ||
this.hass.services[domain][service]?.name ||
title
}`;

View File

@@ -72,6 +72,7 @@ import "./types/ha-automation-trigger-event";
import "./types/ha-automation-trigger-geo_location";
import "./types/ha-automation-trigger-homeassistant";
import "./types/ha-automation-trigger-list";
import "./types/ha-automation-trigger-mqtt";
import "./types/ha-automation-trigger-numeric_state";
import "./types/ha-automation-trigger-persistent_notification";
import "./types/ha-automation-trigger-platform";

View File

@@ -1,9 +1,5 @@
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
import deepClone from "deep-clone-simple";
import type {
HassServiceTarget,
UnsubscribeFunc,
} from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -24,7 +20,6 @@ import {
type Trigger,
type TriggerList,
} from "../../../../data/automation";
import { subscribeLabFeatures } from "../../../../data/labs";
import type { TriggerDescriptions } from "../../../../data/trigger";
import { isTriggerList, subscribeTriggers } from "../../../../data/trigger";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
@@ -71,54 +66,16 @@ export default class HaAutomationTrigger extends SubscribeMixin(LitElement) {
private _triggerKeys = new WeakMap<Trigger, string>();
private _unsub?: Promise<UnsubscribeFunc>;
@state() private _triggerDescriptions: TriggerDescriptions = {};
// @ts-ignore
@state() private _newTriggersAndConditions = false;
public disconnectedCallback() {
super.disconnectedCallback();
this._unsubscribe();
}
protected hassSubscribe() {
return [
subscribeLabFeatures(this.hass!.connection, (features) => {
this._newTriggersAndConditions =
features.find(
(feature) =>
feature.domain === "automation" &&
feature.preview_feature === "new_triggers_conditions"
)?.enabled ?? false;
}),
subscribeTriggers(this.hass, (triggers) => this._addTriggers(triggers)),
];
}
private _subscribeDescriptions() {
this._unsubscribe();
this._triggerDescriptions = {};
this._unsub = subscribeTriggers(this.hass, (descriptions) => {
this._triggerDescriptions = {
...this._triggerDescriptions,
...descriptions,
};
});
}
private _unsubscribe() {
if (this._unsub) {
this._unsub.then((unsub) => unsub());
this._unsub = undefined;
}
}
protected willUpdate(changedProperties: PropertyValues): void {
super.willUpdate(changedProperties);
if (changedProperties.has("_newTriggersAndConditions")) {
this._subscribeDescriptions();
}
private _addTriggers(triggers: TriggerDescriptions) {
this._triggerDescriptions = { ...this._triggerDescriptions, ...triggers };
}
protected firstUpdated(changedProps: PropertyValues) {
@@ -217,14 +174,13 @@ export default class HaAutomationTrigger extends SubscribeMixin(LitElement) {
});
}
private _addTrigger = (value: string, target?: HassServiceTarget) => {
private _addTrigger = (value: string) => {
let triggers: Trigger[];
if (value === PASTE_VALUE) {
triggers = this.triggers.concat(deepClone(this._clipboard!.trigger));
} else if (isDynamic(value)) {
triggers = this.triggers.concat({
trigger: getValueFromDynamic(value),
target,
});
} else {
const trigger = value as Exclude<Trigger, TriggerList>["trigger"];

View File

@@ -0,0 +1,58 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import type { MqttTrigger } from "../../../../../data/automation";
import type { HomeAssistant } from "../../../../../types";
import type { TriggerElement } from "../ha-automation-trigger-row";
const SCHEMA = [
{ name: "topic", required: true, selector: { text: {} } },
{ name: "payload", selector: { text: {} } },
] as const;
@customElement("ha-automation-trigger-mqtt")
export class HaMQTTTrigger extends LitElement implements TriggerElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public trigger!: MqttTrigger;
@property({ type: Boolean }) public disabled = false;
public static get defaultConfig(): MqttTrigger {
return { trigger: "mqtt", topic: "" };
}
protected render() {
return html`
<ha-form
.schema=${SCHEMA}
.data=${this.trigger}
.hass=${this.hass}
.disabled=${this.disabled}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
const newTrigger = ev.detail.value;
fireEvent(this, "value-changed", { value: newTrigger });
}
private _computeLabelCallback = (
schema: SchemaUnion<typeof SCHEMA>
): string =>
this.hass.localize(
`ui.panel.config.automation.editor.triggers.type.mqtt.${schema.name}`
);
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-trigger-mqtt": HaMQTTTrigger;
}
}

View File

@@ -1,16 +1,149 @@
import type { PropertyValues } from "lit";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../../../../common/array/ensure-array";
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { hasTemplate } from "../../../../../common/string/has-template";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import type { NumericStateTrigger } from "../../../../../data/automation";
import type { HomeAssistant } from "../../../../../types";
import { ensureArray } from "../../../../../common/array/ensure-array";
const SCHEMA = [
{
name: "entity_id",
required: true,
selector: { entity: { multiple: true } },
},
{
name: "attribute",
context: { filter_entity: "entity_id" },
selector: {
attribute: {
hide_attributes: [
"access_token",
"auto_update",
"available_modes",
"away_mode",
"changed_by",
"code_arm_required",
"code_format",
"color_mode",
"color_modes",
"current_activity",
"device_class",
"editable",
"effect_list",
"effect",
"entity_id",
"entity_picture",
"event_type",
"event_types",
"fan_mode",
"fan_modes",
"fan_speed_list",
"forecast",
"friendly_name",
"frontend_stream_type",
"has_date",
"has_time",
"hs_color",
"hvac_mode",
"hvac_modes",
"icon",
"id",
"latest_version",
"max_color_temp_kelvin",
"max_mireds",
"max_temp",
"media_album_name",
"media_artist",
"media_content_type",
"media_position_updated_at",
"media_title",
"min_color_temp_kelvin",
"min_mireds",
"min_temp",
"mode",
"next_dawn",
"next_dusk",
"next_midnight",
"next_noon",
"next_rising",
"next_setting",
"operation_list",
"operation_mode",
"options",
"percentage_step",
"precipitation_unit",
"preset_mode",
"preset_modes",
"pressure_unit",
"release_notes",
"release_summary",
"release_url",
"restored",
"rgb_color",
"rgbw_color",
"shuffle",
"skipped_version",
"sound_mode_list",
"sound_mode",
"source_list",
"source_type",
"source",
"state_class",
"step",
"supported_color_modes",
"supported_features",
"swing_mode",
"swing_modes",
"target_temp_step",
"temperature_unit",
"title",
"token",
"unit_of_measurement",
"user_id",
"uuid",
"visibility_unit",
"wind_speed_unit",
"xy_color",
],
},
},
},
{
name: "above",
selector: {
number: {
mode: "box",
min: Number.MIN_SAFE_INTEGER,
max: Number.MAX_SAFE_INTEGER,
step: 0.1,
entity: { domains: ["input_number", "number", "sensor"] },
},
},
},
{
name: "below",
selector: {
number: {
mode: "box",
min: Number.MIN_SAFE_INTEGER,
max: Number.MAX_SAFE_INTEGER,
step: 0.1,
entity: { domains: ["input_number", "number", "sensor"] },
},
},
},
{
name: "value_template",
selector: { template: {} },
},
{ name: "for", selector: { duration: {} } },
] as const;
@customElement("ha-automation-trigger-numeric_state")
export class HaNumericStateTrigger extends LitElement {
@@ -20,224 +153,7 @@ export class HaNumericStateTrigger extends LitElement {
@property({ type: Boolean }) public disabled = false;
@state() private _inputAboveIsEntity?: boolean;
@state() private _inputBelowIsEntity?: boolean;
private _schema = memoizeOne(
(
localize: LocalizeFunc,
entityId: string | string[],
inputAboveIsEntity?: boolean,
inputBelowIsEntity?: boolean
) =>
[
{
name: "entity_id",
required: true,
selector: { entity: { multiple: true } },
},
{
name: "attribute",
selector: {
attribute: {
entity_id: entityId ? entityId[0] : undefined,
hide_attributes: [
"access_token",
"auto_update",
"available_modes",
"away_mode",
"changed_by",
"code_arm_required",
"code_format",
"color_mode",
"color_modes",
"current_activity",
"device_class",
"editable",
"effect_list",
"effect",
"entity_id",
"entity_picture",
"event_type",
"event_types",
"fan_mode",
"fan_modes",
"fan_speed_list",
"forecast",
"friendly_name",
"frontend_stream_type",
"has_date",
"has_time",
"hs_color",
"hvac_mode",
"hvac_modes",
"icon",
"id",
"latest_version",
"max_color_temp_kelvin",
"max_mireds",
"max_temp",
"media_album_name",
"media_artist",
"media_content_type",
"media_position_updated_at",
"media_title",
"min_color_temp_kelvin",
"min_mireds",
"min_temp",
"mode",
"next_dawn",
"next_dusk",
"next_midnight",
"next_noon",
"next_rising",
"next_setting",
"operation_list",
"operation_mode",
"options",
"percentage_step",
"precipitation_unit",
"preset_mode",
"preset_modes",
"pressure_unit",
"release_notes",
"release_summary",
"release_url",
"restored",
"rgb_color",
"rgbw_color",
"shuffle",
"skipped_version",
"sound_mode_list",
"sound_mode",
"source_list",
"source_type",
"source",
"state_class",
"step",
"supported_color_modes",
"supported_features",
"swing_mode",
"swing_modes",
"target_temp_step",
"temperature_unit",
"title",
"token",
"unit_of_measurement",
"user_id",
"uuid",
"visibility_unit",
"wind_speed_unit",
"xy_color",
],
},
},
},
{
name: "lower_limit",
type: "select",
required: true,
options: [
[
"value",
localize(
"ui.panel.config.automation.editor.triggers.type.numeric_state.type_value"
),
],
[
"input",
localize(
"ui.panel.config.automation.editor.triggers.type.numeric_state.type_input"
),
],
],
},
...(inputAboveIsEntity
? ([
{
name: "above",
selector: {
entity: { domain: ["input_number", "number", "sensor"] },
},
},
] as const)
: ([
{
name: "above",
selector: {
number: {
mode: "box",
min: Number.MIN_SAFE_INTEGER,
max: Number.MAX_SAFE_INTEGER,
step: 0.1,
},
},
},
] as const)),
{
name: "upper_limit",
type: "select",
required: true,
options: [
[
"value",
localize(
"ui.panel.config.automation.editor.triggers.type.numeric_state.type_value"
),
],
[
"input",
localize(
"ui.panel.config.automation.editor.triggers.type.numeric_state.type_input"
),
],
],
},
...(inputBelowIsEntity
? ([
{
name: "below",
selector: {
entity: { domain: ["input_number", "number", "sensor"] },
},
},
] as const)
: ([
{
name: "below",
selector: {
number: {
mode: "box",
min: Number.MIN_SAFE_INTEGER,
max: Number.MAX_SAFE_INTEGER,
step: 0.1,
},
},
},
] as const)),
{
name: "value_template",
selector: { template: {} },
},
{ name: "for", selector: { duration: {} } },
] as const
);
public willUpdate(changedProperties: PropertyValues) {
this._inputAboveIsEntity =
this._inputAboveIsEntity ??
(typeof this.trigger.above === "string" &&
((this.trigger.above as string).startsWith("input_number.") ||
(this.trigger.above as string).startsWith("number.") ||
(this.trigger.above as string).startsWith("sensor.")));
this._inputBelowIsEntity =
this._inputBelowIsEntity ??
(typeof this.trigger.below === "string" &&
((this.trigger.below as string).startsWith("input_number.") ||
(this.trigger.below as string).startsWith("number.") ||
(this.trigger.below as string).startsWith("sensor.")));
if (!changedProperties.has("trigger")) {
return;
}
@@ -258,39 +174,20 @@ export class HaNumericStateTrigger extends LitElement {
};
}
private _data = memoizeOne(
(
inputAboveIsEntity: boolean,
inputBelowIsEntity: boolean,
trigger: NumericStateTrigger
) => ({
lower_limit: inputAboveIsEntity ? "input" : "value",
upper_limit: inputBelowIsEntity ? "input" : "value",
...trigger,
entity_id: ensureArray(trigger.entity_id),
for: createDurationData(trigger.for),
})
);
private _data = memoizeOne((trigger: NumericStateTrigger) => ({
...trigger,
entity_id: ensureArray(trigger.entity_id),
for: createDurationData(trigger.for),
}));
public render() {
const schema = this._schema(
this.hass.localize,
this.trigger.entity_id,
this._inputAboveIsEntity,
this._inputBelowIsEntity
);
const data = this._data(
this._inputAboveIsEntity!,
this._inputBelowIsEntity!,
this.trigger
);
const data = this._data(this.trigger);
return html`
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${schema}
.schema=${SCHEMA}
.disabled=${this.disabled}
@value-changed=${this._valueChanged}
.computeLabel=${this._computeLabelCallback}
@@ -302,12 +199,6 @@ export class HaNumericStateTrigger extends LitElement {
ev.stopPropagation();
const newTrigger = { ...ev.detail.value };
this._inputAboveIsEntity = newTrigger.lower_limit === "input";
this._inputBelowIsEntity = newTrigger.upper_limit === "input";
delete newTrigger.lower_limit;
delete newTrigger.upper_limit;
if (newTrigger.value_template === "") {
delete newTrigger.value_template;
}
@@ -316,7 +207,7 @@ export class HaNumericStateTrigger extends LitElement {
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
schema: SchemaUnion<typeof SCHEMA>
): string => {
switch (schema.name) {
case "entity_id":

View File

@@ -25,16 +25,6 @@ const showOptionalToggle = (field: TriggerDescription["fields"][string]) =>
!field.required &&
!("boolean" in field.selector && field.default);
const DEFAULT_KEYS: (keyof PlatformTrigger)[] = [
"trigger",
"target",
"alias",
"id",
"variables",
"enabled",
"options",
] as const;
@customElement("ha-automation-trigger-platform")
export class HaPlatformTrigger extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -62,31 +52,6 @@ export class HaPlatformTrigger extends LitElement {
if (!changedProperties.has("trigger")) {
return;
}
let newValue: PlatformTrigger | undefined;
for (const key in this.trigger) {
// Migrate old options to `options`
if (DEFAULT_KEYS.includes(key as keyof PlatformTrigger)) {
continue;
}
if (newValue === undefined) {
newValue = {
...this.trigger,
options: { [key]: this.trigger[key] },
};
} else {
newValue.options![key] = this.trigger[key];
}
delete newValue[key];
}
if (newValue !== undefined) {
fireEvent(this, "value-changed", {
value: newValue,
});
this.trigger = newValue;
}
const oldValue = changedProperties.get("trigger") as
| undefined
| this["trigger"];
@@ -104,46 +69,6 @@ export class HaPlatformTrigger extends LitElement {
} else {
this._manifest = undefined;
}
if (
oldValue?.trigger !== this.trigger?.trigger &&
this.trigger &&
this.description?.fields
) {
let updatedDefaultValue = false;
const updatedOptions = {};
const loadDefaults = !("options" in this.trigger);
// Set mandatory bools without a default value to false
Object.entries(this.description.fields).forEach(([key, field]) => {
if (
field.selector &&
field.required &&
field.default === undefined &&
"boolean" in field.selector &&
updatedOptions[key] === undefined
) {
updatedDefaultValue = true;
updatedOptions[key] = false;
} else if (
loadDefaults &&
field.selector &&
field.default !== undefined &&
updatedOptions[key] === undefined
) {
updatedDefaultValue = true;
updatedOptions[key] = field.default;
}
});
if (updatedDefaultValue) {
fireEvent(this, "value-changed", {
value: {
...this.trigger,
options: updatedOptions,
},
});
}
}
}
protected render() {
@@ -429,10 +354,6 @@ export class HaPlatformTrigger extends LitElement {
}
static styles = css`
:host {
display: block;
margin: 0px calc(-1 * var(--ha-space-4));
}
ha-settings-row {
padding: 0 var(--ha-space-4);
}

View File

@@ -1,52 +0,0 @@
import { mdiKey } from "@mdi/js";
import { getConfigEntries } from "../../../../../../data/config_entries";
import type { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import { fetchESPHomeEncryptionKey } from "../../../../../../data/esphome";
import type { HomeAssistant } from "../../../../../../types";
import { showESPHomeEncryptionKeyDialog } from "../../../../integrations/integration-panels/esphome/show-dialog-esphome-encryption-key";
import type { DeviceAction } from "../../../ha-config-device-page";
export const getESPHomeDeviceActions = async (
el: HTMLElement,
hass: HomeAssistant,
device: DeviceRegistryEntry
): Promise<DeviceAction[]> => {
const actions: DeviceAction[] = [];
const configEntries = await getConfigEntries(hass, {
domain: "esphome",
});
const configEntry = configEntries.find((entry) =>
device.config_entries.includes(entry.entry_id)
);
if (!configEntry) {
return [];
}
const entryId = configEntry.entry_id;
try {
const encryptionKey = await fetchESPHomeEncryptionKey(hass, entryId);
if (encryptionKey.encryption_key) {
actions.push({
label: hass.localize(
"ui.panel.config.devices.esphome.show_encryption_key"
),
icon: mdiKey,
action: () =>
showESPHomeEncryptionKeyDialog(el, {
entry_id: entryId,
encryption_key: encryptionKey.encryption_key,
}),
});
}
} catch (err) {
// eslint-disable-next-line no-console
console.error("Failed to fetch ESPHome encryption key:", err);
}
return actions;
};

View File

@@ -1162,17 +1162,6 @@ export class HaConfigDevicePage extends LitElement {
);
deviceActions.push(...actions);
}
if (domains.includes("esphome")) {
const esphome = await import(
"./device-detail/integration-elements/esphome/device-actions"
);
const actions = await esphome.getESPHomeDeviceActions(
this,
this.hass,
device
);
deviceActions.push(...actions);
}
if (domains.includes("matter")) {
const matter = await import(
"./device-detail/integration-elements/matter/device-actions"

View File

@@ -1,19 +1,19 @@
import { consume } from "@lit/context";
import {
mdiCancel,
mdiChevronRight,
mdiDelete,
mdiDotsVertical,
mdiMenuDown,
mdiPlus,
mdiTextureBox,
mdiCancel,
mdiDelete,
} from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, state, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
@@ -68,8 +68,8 @@ import type {
DeviceRegistryEntry,
} from "../../../data/device_registry";
import {
removeConfigEntryFromDevice,
updateDeviceRegistryEntry,
removeConfigEntryFromDevice,
} from "../../../data/device_registry";
import type { EntityRegistryEntry } from "../../../data/entity_registry";
import {
@@ -86,8 +86,8 @@ import {
showAlertDialog,
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-tabs-subpage-data-table";
import type { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-data-table";
import "../../../layouts/hass-tabs-subpage-data-table";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route } from "../../../types";
@@ -318,7 +318,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
})
);
const deviceEntityLookup: DeviceEntityLookup<EntityRegistryEntry> = {};
const deviceEntityLookup: DeviceEntityLookup = {};
for (const entity of entities) {
if (!entity.device_id) {
continue;

View File

@@ -1,140 +0,0 @@
import { mdiClose, mdiContentCopy } 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 { copyToClipboard } from "../../../../../common/util/copy-clipboard";
import "../../../../../components/ha-button";
import "../../../../../components/ha-dialog-footer";
import "../../../../../components/ha-dialog-header";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-wa-dialog";
import { haStyleDialog } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import { showToast } from "../../../../../util/toast";
import type { ESPHomeEncryptionKeyDialogParams } from "./show-dialog-esphome-encryption-key";
@customElement("dialog-esphome-encryption-key")
class DialogESPHomeEncryptionKey extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: ESPHomeEncryptionKeyDialogParams;
public async showDialog(
params: ESPHomeEncryptionKeyDialogParams
): Promise<void> {
this._params = params;
}
public closeDialog(): void {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._params) {
return nothing;
}
return html`
<ha-wa-dialog
open
@closed=${this.closeDialog}
header-title=${this.hass.localize(
"ui.panel.config.devices.esphome.encryption_key_title"
)}
>
<ha-dialog-header slot="heading">
<ha-icon-button
slot="navigationIcon"
dialogAction="cancel"
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
<span slot="title">
${this.hass.localize(
"ui.panel.config.devices.esphome.encryption_key_title"
)}
</span>
</ha-dialog-header>
<div class="content">
<p>
${this.hass.localize(
"ui.panel.config.devices.esphome.encryption_key_description"
)}
</p>
<div class="key-row">
<div class="key-container">
<code>${this._params.encryption_key}</code>
</div>
<ha-icon-button
@click=${this._copyToClipboard}
.label=${this.hass.localize("ui.common.copy")}
.path=${mdiContentCopy}
></ha-icon-button>
</div>
</div>
<ha-dialog-footer slot="footer">
<ha-button slot="primaryAction" data-dialog="close">
${this.hass.localize("ui.common.close")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
`;
}
private async _copyToClipboard(): Promise<void> {
if (!this._params?.encryption_key) {
return;
}
await copyToClipboard(this._params.encryption_key);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
});
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
.content {
display: flex;
flex-direction: column;
gap: var(--ha-space-6);
}
.key-row {
display: flex;
gap: var(--ha-space-2);
align-items: center;
}
.key-container {
flex: 1;
border-radius: var(--ha-space-2);
border: 1px solid var(--divider-color);
background-color: var(
--code-editor-background-color,
var(--secondary-background-color)
);
padding: var(--ha-space-3);
overflow: auto;
}
p {
margin: 0;
color: var(--secondary-text-color);
line-height: var(--ha-line-height-condensed);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-esphome-encryption-key": DialogESPHomeEncryptionKey;
}
}

View File

@@ -1,20 +0,0 @@
import { fireEvent } from "../../../../../common/dom/fire_event";
export interface ESPHomeEncryptionKeyDialogParams {
entry_id: string;
encryption_key: string;
}
export const loadESPHomeEncryptionKeyDialog = () =>
import("./dialog-esphome-encryption-key");
export const showESPHomeEncryptionKeyDialog = (
element: HTMLElement,
dialogParams: ESPHomeEncryptionKeyDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-esphome-encryption-key",
dialogImport: loadESPHomeEncryptionKeyDialog,
dialogParams,
});
};

View File

@@ -55,6 +55,7 @@ import {
subscribeEntityRegistry,
updateEntityRegistryEntry,
} from "../../../../../../data/entity_registry";
import { SubscribeMixin } from "../../../../../../mixins/subscribe-mixin";
import "./zwave-js-add-node-added-insecure";
import "./zwave-js-add-node-code-input";
import "./zwave-js-add-node-configure-device";
@@ -68,7 +69,7 @@ import "./zwave-js-add-node-select-security-strategy";
const INCLUSION_TIMEOUT_MINUTES = 5;
@customElement("dialog-zwave_js-add-node")
class DialogZWaveJSAddNode extends LitElement {
class DialogZWaveJSAddNode extends SubscribeMixin(LitElement) {
// #region variables
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -102,8 +103,6 @@ class DialogZWaveJSAddNode extends LitElement {
@state() private _securityClasses: SecurityClass[] = [];
@state() private _entities: EntityRegistryEntry[] = [];
@state() private _codeInput = "";
@query("ha-dialog") private _dialog?: HaDialog;
@@ -114,14 +113,22 @@ class DialogZWaveJSAddNode extends LitElement {
private _onStop?: () => void;
private _subscribedAddZwaveNode?: Promise<UnsubscribeFunc | undefined>;
private _subscribed?: Promise<UnsubscribeFunc | undefined>;
private _newDeviceSubscription?: Promise<UnsubscribeFunc | undefined>;
private _subscribedEntityRegistry?: UnsubscribeFunc;
@state() private _entities: EntityRegistryEntry[] = [];
// #endregion
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection, (entities) => {
this._entities = entities;
}),
];
}
protected render() {
if (!this._entryId) {
return nothing;
@@ -432,6 +439,11 @@ class DialogZWaveJSAddNode extends LitElement {
></zwave-js-add-node-loading>`;
}
public connectedCallback(): void {
super.connectedCallback();
window.addEventListener("beforeunload", this._onBeforeUnload);
}
private _onBeforeUnload = (event: BeforeUnloadEvent) => {
if (this._step && this._shouldPreventClose(this._step)) {
event.preventDefault();
@@ -456,14 +468,6 @@ class DialogZWaveJSAddNode extends LitElement {
}
public async showDialog(params: ZWaveJSAddNodeDialogParams): Promise<void> {
window.addEventListener("beforeunload", this._onBeforeUnload);
this._subscribedEntityRegistry = subscribeEntityRegistry(
this.hass.connection,
(entities) => {
this._entities = entities;
}
);
if (this._step) {
// already started
return;
@@ -558,7 +562,7 @@ class DialogZWaveJSAddNode extends LitElement {
this._step = "select_method";
break;
case "search_devices":
this._unsubscribeAddZwaveNode();
this._unsubscribe();
if (
this._supportsSmartStart &&
this.hass.auth.external?.config.hasBarCodeScanner
@@ -600,7 +604,7 @@ class DialogZWaveJSAddNode extends LitElement {
}
private _searchDevicesShowSecurityOptions() {
this._unsubscribeAddZwaveNode();
this._unsubscribe();
this._step = "choose_security_strategy";
}
@@ -622,7 +626,7 @@ class DialogZWaveJSAddNode extends LitElement {
this._lowSecurity = false;
const s2Device = qrProvisioningInformation || dsk;
this._subscribedAddZwaveNode = subscribeAddZwaveNode(
this._subscribed = subscribeAddZwaveNode(
this.hass,
this._entryId!,
(message) => {
@@ -631,7 +635,7 @@ class DialogZWaveJSAddNode extends LitElement {
this._step = s2Device ? "search_s2_device" : "search_devices";
break;
case "inclusion failed":
this._unsubscribeAddZwaveNode();
this._unsubscribe();
this._step = "failed";
break;
case "inclusion stopped":
@@ -673,7 +677,7 @@ class DialogZWaveJSAddNode extends LitElement {
this._lowSecurityReason = message.node.low_security_reason;
break;
case "interview completed":
this._unsubscribeAddZwaveNode();
this._unsubscribe();
this._step = "configure_device";
break;
}
@@ -690,7 +694,7 @@ class DialogZWaveJSAddNode extends LitElement {
});
this._addNodeTimeoutHandle = window.setTimeout(
() => {
this._unsubscribeAddZwaveNode();
this._unsubscribe();
this._error = this.hass.localize(
"ui.panel.config.zwave_js.add_node.timeout_error",
{ minutes: INCLUSION_TIMEOUT_MINUTES }
@@ -1019,10 +1023,10 @@ class DialogZWaveJSAddNode extends LitElement {
}
}
private _unsubscribeAddZwaveNode(): void {
if (this._subscribedAddZwaveNode) {
this._subscribedAddZwaveNode.then((unsub) => unsub && unsub());
this._subscribedAddZwaveNode = undefined;
private _unsubscribe(): void {
if (this._subscribed) {
this._subscribed.then((unsub) => unsub && unsub());
this._subscribed = undefined;
if (this._entryId) {
stopZwaveInclusion(this.hass, this._entryId);
@@ -1056,17 +1060,8 @@ class DialogZWaveJSAddNode extends LitElement {
window.removeEventListener("beforeunload", this._onBeforeUnload);
}
private _unsubscribeDialog() {
if (this._subscribedEntityRegistry) {
this._subscribedEntityRegistry();
this._subscribedEntityRegistry = undefined;
}
}
private _dialogClosed() {
window.removeEventListener("beforeunload", this._onBeforeUnload);
this._unsubscribeAddZwaveNode();
this._unsubscribeDialog();
this._unsubscribe();
this._open = false;
this._entryId = undefined;
this._step = undefined;
@@ -1105,8 +1100,7 @@ class DialogZWaveJSAddNode extends LitElement {
super.disconnectedCallback();
window.removeEventListener("beforeunload", this._onBeforeUnload);
this._unsubscribeAddZwaveNode();
this._unsubscribeDialog();
this._unsubscribe();
}
static get styles(): CSSResultGroup {

View File

@@ -94,7 +94,7 @@ class HaConfigLabs extends SubscribeMixin(LitElement) {
<hass-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config/system"
back-path="/config"
.header=${this.hass.localize("ui.panel.config.labs.caption")}
>
${sortedFeatures.length

View File

@@ -1,7 +1,6 @@
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeKeys } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
import type { AssistPipeline } from "../../../../data/assist_pipeline";
@@ -79,18 +78,6 @@ export class AssistPipelineDetailSTT extends LitElement {
private _supportedLanguagesChanged(ev) {
this._supportedLanguages = ev.detail.value;
if (
!this.data?.stt_language ||
!this._supportedLanguages?.includes(this.data.stt_language)
) {
// wait for update of conversation_engine
setTimeout(() => {
const value = { ...this.data };
value.stt_language = this._supportedLanguages?.[0] ?? null;
fireEvent(this, "value-changed", { value });
}, 0);
}
}
static styles = css`

View File

@@ -1,7 +1,6 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeKeys } from "../../../../common/translations/localize";
import "../../../../components/ha-button";
import "../../../../components/ha-form/ha-form";
@@ -123,18 +122,6 @@ export class AssistPipelineDetailTTS extends LitElement {
private _supportedLanguagesChanged(ev) {
this._supportedLanguages = ev.detail.value;
if (
!this.data?.tts_language ||
!this._supportedLanguages?.includes(this.data?.tts_language)
) {
// wait for update of conversation_engine
setTimeout(() => {
const value = { ...this.data };
value.tts_language = this._supportedLanguages?.[0] ?? null;
fireEvent(this, "value-changed", { value });
}, 0);
}
}
static styles = css`

View File

@@ -135,11 +135,6 @@ class HaPanelDevAction extends LitElement {
? computeObjectId(this._serviceData?.action)
: undefined;
const descriptionPlaceholders =
domain && serviceName
? this.hass.services[domain][serviceName].description_placeholders
: undefined;
return html`
<div class="content">
<p>
@@ -312,14 +307,12 @@ class HaPanelDevAction extends LitElement {
<td><pre>${field.key}</pre></td>
<td>
${this.hass.localize(
`component.${domain}.services.${serviceName}.fields.${field.key}.description`,
descriptionPlaceholders
`component.${domain}.services.${serviceName}.fields.${field.key}.description`
) || field.description}
</td>
<td>
${this.hass.localize(
`component.${domain}.services.${serviceName}.fields.${field.key}.example`,
descriptionPlaceholders
`component.${domain}.services.${serviceName}.fields.${field.key}.example`
) || field.example}
</td>
</tr>`
@@ -650,11 +643,7 @@ class HaPanelDevAction extends LitElement {
} catch (_err: any) {
value =
this.hass.localize(
`component.${domain}.services.${serviceName}.fields.${field.key}.example`,
domain && serviceName
? this.hass.services[domain][serviceName]
.description_placeholders
: undefined
`component.${domain}.services.${serviceName}.fields.${field.key}.example`
) || field.example;
}
example[field.key] = value;

View File

@@ -1,77 +1,60 @@
import { mdiDownload, mdiPencil } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { mdiPencil, mdiDownload } from "@mdi/js";
import { customElement, property, state } from "lit/decorators";
import { goBack, navigate } from "../../common/navigate";
import "../../components/ha-alert";
import "../../components/ha-menu-button";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-list-item";
import "../../components/ha-menu-button";
import "../../components/ha-top-app-bar-fixed";
import "../../components/ha-alert";
import type { LovelaceConfig } from "../../data/lovelace/config/types";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "../lovelace/components/hui-energy-period-selector";
import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container";
import { goBack, navigate } from "../../common/navigate";
import type {
BatterySourceTypeEnergyPreference,
DeviceConsumptionEnergyPreference,
EnergyPreferences,
GasSourceTypeEnergyPreference,
GridSourceTypeEnergyPreference,
SolarSourceTypeEnergyPreference,
BatterySourceTypeEnergyPreference,
GasSourceTypeEnergyPreference,
WaterSourceTypeEnergyPreference,
DeviceConsumptionEnergyPreference,
EnergyCollection,
} from "../../data/energy";
import {
computeConsumptionData,
getEnergyDataCollection,
getSummedData,
} from "../../data/energy";
import type { LovelaceConfig } from "../../data/lovelace/config/types";
import type { LovelaceViewConfig } from "../../data/lovelace/config/view";
import type { StatisticValue } from "../../data/recorder";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant, PanelInfo } from "../../types";
import { fileDownload } from "../../util/file_download";
import "../lovelace/components/hui-energy-period-selector";
import "../lovelace/hui-root";
import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container";
import type { StatisticValue } from "../../data/recorder";
export const DEFAULT_ENERGY_COLLECTION_KEY = "energy_dashboard";
const EMPTY_PREFERENCES: EnergyPreferences = {
energy_sources: [],
device_consumption: [],
device_consumption_water: [],
};
const OVERVIEW_VIEW = {
path: "overview",
strategy: {
type: "energy-overview",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
},
} as LovelaceViewConfig;
const ELECTRICITY_VIEW = {
path: "electricity",
back_path: "/energy",
strategy: {
type: "energy-electricity",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
},
} as LovelaceViewConfig;
const WATER_VIEW = {
back_path: "/energy",
path: "water",
strategy: {
type: "energy-water",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
},
} as LovelaceViewConfig;
const WIZARD_VIEW = {
type: "panel",
path: "setup",
cards: [{ type: "custom:energy-setup-wizard-card" }],
const ENERGY_LOVELACE_CONFIG: LovelaceConfig = {
views: [
{
strategy: {
type: "energy-overview",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
},
},
{
strategy: {
type: "energy-electricity",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
},
path: "electricity",
},
{
type: "panel",
path: "setup",
cards: [{ type: "custom:energy-setup-wizard-card" }],
},
],
};
@customElement("ha-panel-energy")
@@ -80,96 +63,70 @@ class PanelEnergy extends LitElement {
@property({ type: Boolean, reflect: true }) public narrow = false;
@property({ attribute: false }) public panel?: PanelInfo;
@state() private _lovelace?: Lovelace;
@state() private _searchParms = new URLSearchParams(window.location.search);
@state() private _error?: string;
@property({ attribute: false }) public route?: {
path: string;
prefix: string;
};
@state()
private _prefs?: EnergyPreferences;
private _energyCollection?: EnergyCollection;
@state()
private _error?: string;
get _viewPath(): string | undefined {
const viewPath: string | undefined = this.route!.path.split("/")[1];
return viewPath ? decodeURI(viewPath) : undefined;
}
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
// Initial setup
public connectedCallback() {
super.connectedCallback();
this._loadPrefs();
}
public async willUpdate(changedProps: PropertyValues) {
if (!this.hasUpdated) {
this.hass.loadFragmentTranslation("lovelace");
this._loadConfig();
return;
}
if (!changedProps.has("hass")) {
return;
}
const oldHass = changedProps.get("hass") as this["hass"];
if (oldHass && oldHass.localize !== this.hass.localize) {
if (oldHass?.locale !== this.hass.locale) {
this._setLovelace();
} else if (oldHass && oldHass.localize !== this.hass.localize) {
this._reloadView();
}
}
private _fetchEnergyPrefs = async (): Promise<
EnergyPreferences | undefined
> => {
const collection = getEnergyDataCollection(this.hass, {
key: DEFAULT_ENERGY_COLLECTION_KEY,
});
try {
await collection.refresh();
} catch (err: any) {
if (err.code === "not_found") {
return undefined;
private async _loadPrefs() {
if (this._viewPath === "setup") {
await import("./cards/energy-setup-wizard-card");
} else {
this._energyCollection = getEnergyDataCollection(this.hass, {
key: DEFAULT_ENERGY_COLLECTION_KEY,
});
try {
// Have to manually refresh here as we don't want to subscribe yet
await this._energyCollection.refresh();
} catch (err: any) {
if (err.code === "not_found") {
navigate("/energy/setup");
}
this._error = err.message;
return;
}
const prefs = this._energyCollection.prefs!;
if (
prefs.device_consumption.length === 0 &&
prefs.energy_sources.length === 0
) {
// No energy sources available, start from scratch
navigate("/energy/setup");
}
throw err;
}
return collection.prefs;
};
private async _loadConfig() {
try {
this._error = undefined;
const prefs = await this._fetchEnergyPrefs();
this._prefs = prefs || EMPTY_PREFERENCES;
} catch (err) {
// eslint-disable-next-line no-console
console.error("Failed to load prefs:", err);
this._prefs = EMPTY_PREFERENCES;
this._error = (err as Error).message || "Unknown error";
}
await this._setLovelace();
// Navigate to first view if not there yet
const firstPath = this._lovelace!.config?.views?.[0]?.path;
const viewPath: string | undefined = this.route!.path.split("/")[1];
if (viewPath !== firstPath) {
navigate(`${this.route!.prefix}/${firstPath}`);
}
}
private async _setLovelace() {
const config = await this._generateLovelaceConfig();
this._lovelace = {
config: config,
rawConfig: config,
editMode: false,
urlPath: "energy",
mode: "generated",
locale: this.hass.locale,
enableFullEditMode: () => undefined,
saveConfig: async () => undefined,
deleteConfig: async () => undefined,
setEditMode: () => undefined,
showToast: () => undefined,
};
}
private _back(ev) {
@@ -178,49 +135,31 @@ class PanelEnergy extends LitElement {
}
protected render() {
if (this._error) {
return html`
<div class="centered">
<ha-alert alert-type="error">
An error occurred loading energy preferences: ${this._error}
</ha-alert>
</div>
`;
}
if (!this._prefs) {
if (!this._energyCollection?.prefs) {
// Still loading
return html`
<div class="centered">
<ha-spinner size="large"></ha-spinner>
</div>
`;
return html`<div class="centered">
<ha-spinner size="large"></ha-spinner>
</div>`;
}
if (!this._lovelace) {
return nothing;
const { prefs } = this._energyCollection;
const isSingleView = prefs.energy_sources.every((source) =>
["grid", "solar", "battery"].includes(source.type)
);
let viewPath = this._viewPath;
if (isSingleView) {
// if only electricity sources, show electricity view directly
viewPath = "electricity";
}
const viewPath: string | undefined = this.route!.path.split("/")[1];
const views = this._lovelace.config?.views || [];
const viewIndex = Math.max(
views.findIndex((view) => view.path === viewPath),
ENERGY_LOVELACE_CONFIG.views.findIndex((view) => view.path === viewPath),
0
);
const showBack = this._searchParms.has("historyBack") || viewIndex > 0;
const showBack =
this._searchParms.has("historyBack") || (!isSingleView && viewIndex > 0);
return html`
<hui-root
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.route=${this.route}
.panel=${this.panel}
@reload-energy-panel=${this._reloadConfig}
>
<div class="toolbar" slot="toolbar">
<div class="header">
<div class="toolbar">
${showBack
? html`
<ha-icon-button-arrow-prev
@@ -246,17 +185,14 @@ class PanelEnergy extends LitElement {
.collectionKey=${DEFAULT_ENERGY_COLLECTION_KEY}
>
${this.hass.user?.is_admin
? html`
<ha-list-item
slot="overflow-menu"
graphic="icon"
@request-selected=${this._navigateConfig}
>
<ha-svg-icon slot="graphic" .path=${mdiPencil}>
</ha-svg-icon>
${this.hass!.localize("ui.panel.energy.configure")}
</ha-list-item>
`
? html` <ha-list-item
slot="overflow-menu"
graphic="icon"
@request-selected=${this._navigateConfig}
>
<ha-svg-icon slot="graphic" .path=${mdiPencil}> </ha-svg-icon>
${this.hass!.localize("ui.panel.energy.configure")}
</ha-list-item>`
: nothing}
<ha-list-item
slot="overflow-menu"
@@ -268,40 +204,45 @@ class PanelEnergy extends LitElement {
</ha-list-item>
</hui-energy-period-selector>
</div>
</hui-root>
</div>
<hui-view-container
.hass=${this.hass}
@reload-energy-panel=${this._reloadView}
>
${this._error
? html`<div class="centered">
<ha-alert alert-type="error">
An error occurred while fetching your energy preferences:
${this._error}
</ha-alert>
</div>`
: this._lovelace
? html`<hui-view
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.index=${viewIndex}
></hui-view>`
: nothing}
</hui-view-container>
`;
}
private async _generateLovelaceConfig(): Promise<LovelaceConfig> {
if (
!this._prefs ||
(this._prefs.device_consumption.length === 0 &&
this._prefs.energy_sources.length === 0)
) {
await import("./cards/energy-setup-wizard-card");
return {
views: [WIZARD_VIEW],
};
}
const isElectricityOnly = this._prefs.energy_sources.every((source) =>
["grid", "solar", "battery"].includes(source.type)
);
if (isElectricityOnly) {
return {
views: [ELECTRICITY_VIEW],
};
}
const hasWater =
this._prefs.energy_sources.some((source) => source.type === "water") ||
this._prefs.device_consumption_water?.length > 0;
const views: LovelaceViewConfig[] = [OVERVIEW_VIEW, ELECTRICITY_VIEW];
if (hasWater) {
views.push(WATER_VIEW);
}
return { views };
private _setLovelace() {
this._lovelace = {
config: ENERGY_LOVELACE_CONFIG,
rawConfig: ENERGY_LOVELACE_CONFIG,
editMode: false,
urlPath: "energy",
mode: "generated",
locale: this.hass.locale,
enableFullEditMode: () => undefined,
saveConfig: async () => undefined,
deleteConfig: async () => undefined,
setEditMode: () => undefined,
showToast: () => undefined,
};
}
private _navigateConfig(ev) {
@@ -311,9 +252,7 @@ class PanelEnergy extends LitElement {
private async _dumpCSV(ev) {
ev.stopPropagation();
const energyData = getEnergyDataCollection(this.hass, {
key: "energy_dashboard",
});
const energyData = this._energyCollection!;
if (!energyData.prefs || !energyData.state.stats) {
return;
@@ -609,8 +548,13 @@ class PanelEnergy extends LitElement {
fileDownload(url, "energy.csv");
}
private _reloadConfig() {
this._loadConfig();
private _reloadView() {
// Force strategy to be re-run by making a copy of the view
const config = this._lovelace!.config;
this._lovelace = {
...this._lovelace!,
config: { ...config, views: config.views.map((view) => ({ ...view })) },
};
}
static get styles(): CSSResultGroup {
@@ -636,6 +580,45 @@ class PanelEnergy extends LitElement {
-webkit-user-select: none;
-moz-user-select: none;
}
.header {
background-color: var(--app-header-background-color);
color: var(--app-header-text-color, white);
border-bottom: var(--app-header-border-bottom, none);
position: fixed;
top: 0;
width: calc(
var(--mdc-top-app-bar-width, 100%) - var(
--safe-area-inset-right,
0px
)
);
padding-top: var(--safe-area-inset-top);
z-index: 4;
transition: box-shadow 200ms linear;
display: flex;
flex-direction: row;
-webkit-backdrop-filter: var(--app-header-backdrop-filter, none);
backdrop-filter: var(--app-header-backdrop-filter, none);
padding-top: var(--safe-area-inset-top);
padding-right: var(--safe-area-inset-right);
}
:host([narrow]) .header {
width: calc(
var(--mdc-top-app-bar-width, 100%) - var(
--safe-area-inset-left,
0px
) - var(--safe-area-inset-right, 0px)
);
padding-left: var(--safe-area-inset-left);
}
:host([scrolled]) .header {
box-shadow: var(
--mdc-top-app-bar-fixed-box-shadow,
0px 2px 4px -1px rgba(0, 0, 0, 0.2),
0px 4px 5px 0px rgba(0, 0, 0, 0.14),
0px 1px 10px 0px rgba(0, 0, 0, 0.12)
);
}
.toolbar {
height: var(--header-height);
display: flex;
@@ -654,6 +637,24 @@ class PanelEnergy extends LitElement {
line-height: var(--ha-line-height-normal);
flex-grow: 1;
}
hui-view-container {
position: relative;
display: flex;
min-height: 100vh;
box-sizing: border-box;
padding-top: calc(var(--header-height) + var(--safe-area-inset-top));
padding-right: var(--safe-area-inset-right);
padding-inline-end: var(--safe-area-inset-right);
padding-bottom: var(--safe-area-inset-bottom);
}
:host([narrow]) hui-view-container {
padding-left: var(--safe-area-inset-left);
padding-inline-start: var(--safe-area-inset-left);
}
hui-view {
flex: 1 1 100%;
max-width: 100%;
}
.centered {
width: 100%;
height: 100%;

View File

@@ -55,9 +55,6 @@ export class EnergyElectricityViewStrategy extends ReactiveElement {
const hasPowerDevices = prefs.device_consumption.find(
(device) => device.stat_rate
);
const showFloorsNAreas = !prefs.device_consumption.some(
(d) => d.included_in_stat
);
view.cards!.push({
type: "energy-compare",
@@ -70,8 +67,6 @@ export class EnergyElectricityViewStrategy extends ReactiveElement {
title: hass.localize("ui.panel.energy.cards.power_sankey_title"),
type: "power-sankey",
collection_key: collectionKey,
group_by_floor: showFloorsNAreas,
group_by_area: showFloorsNAreas,
grid_options: {
columns: 24,
},
@@ -161,6 +156,9 @@ export class EnergyElectricityViewStrategy extends ReactiveElement {
// Only include if we have at least 1 device in the config.
if (prefs.device_consumption.length) {
const showFloorsNAreas = !prefs.device_consumption.some(
(d) => d.included_in_stat
);
view.cards!.push({
title: hass.localize("ui.panel.energy.cards.energy_sankey_title"),
type: "energy-sankey",

View File

@@ -6,16 +6,9 @@ import type { HomeAssistant } from "../../../types";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../ha-panel-energy";
const sourceHasCost = (source: Record<string, any>): boolean =>
Boolean(
source.stat_cost ||
source.stat_compensation ||
source.entity_energy_price ||
source.number_energy_price
);
@customElement("energy-overview-view-strategy")
export class EnergyViewStrategy extends ReactiveElement {
static async generate(
@@ -51,6 +44,10 @@ export class EnergyViewStrategy extends ReactiveElement {
source.type === "grid" &&
(source.flow_from?.length || source.flow_to?.length)
) as GridSourceTypeEnergyPreference;
const hasReturn = hasGrid && hasGrid.flow_to.length > 0;
const hasSolar = prefs.energy_sources.some(
(source) => source.type === "solar"
);
const hasGas = prefs.energy_sources.some((source) => source.type === "gas");
const hasBattery = prefs.energy_sources.some(
(source) => source.type === "battery"
@@ -67,13 +64,6 @@ export class EnergyViewStrategy extends ReactiveElement {
const hasPowerDevices = prefs.device_consumption.find(
(device) => device.stat_rate
);
const hasCost = prefs.energy_sources.some(
(source) =>
sourceHasCost(source) ||
(source.type === "grid" &&
(source.flow_from?.some(sourceHasCost) ||
source.flow_to?.some(sourceHasCost)))
);
const overviewSection: LovelaceSectionConfig = {
type: "grid",
@@ -81,15 +71,10 @@ export class EnergyViewStrategy extends ReactiveElement {
cards: [],
};
if (hasPowerSources && hasPowerDevices) {
const showFloorsNAreas = !prefs.device_consumption.some(
(d) => d.included_in_stat
);
overviewSection.cards!.push({
title: hass.localize("ui.panel.energy.cards.power_sankey_title"),
type: "power-sankey",
collection_key: collectionKey,
group_by_floor: showFloorsNAreas,
group_by_area: showFloorsNAreas,
grid_options: {
columns: 24,
},
@@ -103,11 +88,10 @@ export class EnergyViewStrategy extends ReactiveElement {
collection_key: collectionKey,
});
}
if (hasCost) {
if (hasGrid || hasSolar || hasBattery || hasGas || hasWater) {
overviewSection.cards!.push({
type: "energy-sources-table",
collection_key: collectionKey,
show_only_totals: true,
});
}
view.sections!.push(overviewSection);
@@ -143,11 +127,44 @@ export class EnergyViewStrategy extends ReactiveElement {
modes: ["bar"],
});
} else if (hasGrid) {
electricitySection.cards!.push({
title: hass.localize("ui.panel.energy.cards.energy_usage_graph_title"),
type: "energy-usage-graph",
const gauges: LovelaceCardConfig[] = [];
// Only include if we have a grid source & return.
if (hasReturn) {
gauges.push({
type: "energy-grid-neutrality-gauge",
view_layout: { position: "sidebar" },
collection_key: collectionKey,
});
}
gauges.push({
type: "energy-carbon-consumed-gauge",
view_layout: { position: "sidebar" },
collection_key: collectionKey,
});
// Only include if we have a solar source.
if (hasSolar) {
if (hasReturn) {
gauges.push({
type: "energy-solar-consumed-gauge",
view_layout: { position: "sidebar" },
collection_key: collectionKey,
});
}
gauges.push({
type: "energy-self-sufficiency-gauge",
view_layout: { position: "sidebar" },
collection_key: collectionKey,
});
}
electricitySection.cards!.push({
type: "grid",
columns: 2,
square: false,
cards: gauges,
});
}
view.sections!.push(electricitySection);
@@ -178,10 +195,6 @@ export class EnergyViewStrategy extends ReactiveElement {
{
type: "heading",
heading: hass.localize("ui.panel.energy.overview.water"),
tap_action: {
action: "navigate",
navigation_path: "/energy/water",
},
},
{
title: hass.localize(

View File

@@ -1,86 +0,0 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import { getEnergyDataCollection } from "../../../data/energy";
import type { HomeAssistant } from "../../../types";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../ha-panel-energy";
@customElement("energy-water-view-strategy")
export class EnergyWaterViewStrategy extends ReactiveElement {
static async generate(
_config: LovelaceStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const view: LovelaceViewConfig = { cards: [] };
const collectionKey =
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
const energyCollection = getEnergyDataCollection(hass, {
key: collectionKey,
});
const prefs = energyCollection.prefs;
// No water sources available
if (
!prefs ||
(!prefs.device_consumption_water?.length &&
!prefs.energy_sources.some((source) => source.type === "water"))
) {
return view;
}
view.type = "sidebar";
const hasWater = prefs.energy_sources.some(
(source) => source.type === "water"
);
view.cards!.push({
type: "energy-compare",
collection_key: collectionKey,
});
if (hasWater) {
view.cards!.push({
title: hass.localize("ui.panel.energy.cards.energy_water_graph_title"),
type: "energy-water-graph",
collection_key: collectionKey,
});
}
if (hasWater) {
view.cards!.push({
title: hass.localize(
"ui.panel.energy.cards.energy_sources_table_title"
),
type: "energy-sources-table",
collection_key: collectionKey,
types: ["water"],
});
}
// Only include if we have at least 1 water device in the config.
if (prefs.device_consumption_water?.length) {
const showFloorsNAreas = !prefs.device_consumption_water.some(
(d) => d.included_in_stat
);
view.cards!.push({
title: hass.localize("ui.panel.energy.cards.water_sankey_title"),
type: "water-sankey",
collection_key: collectionKey,
group_by_floor: showFloorsNAreas,
group_by_area: showFloorsNAreas,
});
}
return view;
}
}
declare global {
interface HTMLElementTagNameMap {
"energy-water-view-strategy": EnergyWaterViewStrategy;
}
}

View File

@@ -2,7 +2,6 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/entity/ha-entities-picker";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-wa-dialog";
@@ -79,16 +78,6 @@ export class DialogEditHome
@value-changed=${this._favoriteEntitiesChanged}
></ha-entities-picker>
<ha-alert alert-type="info">
${this.hass.localize("ui.panel.home.editor.areas_hint", {
areas_page: html`<a
href="/config/areas?historyBack=1"
@click=${this.closeDialog}
>${this.hass.localize("ui.panel.home.editor.areas_page")}</a
>`,
})}
</ha-alert>
<ha-dialog-footer slot="footer">
<ha-button
appearance="plain"
@@ -151,11 +140,6 @@ export class DialogEditHome
ha-entities-picker {
display: block;
}
ha-alert {
display: block;
margin-top: var(--ha-space-4);
}
`,
];
}

View File

@@ -400,9 +400,7 @@ class HaLogbookRenderer extends LitElement {
? `${domainToName(this.hass.localize, item.context_domain)}:
${
this.hass.localize(
`component.${item.context_domain}.services.${item.context_service}.name`,
this.hass.services[item.context_domain][item.context_service]
.description_placeholders
`component.${item.context_domain}.services.${item.context_service}.name`
) ||
this.hass.services[item.context_domain]?.[item.context_service]?.name ||
item.context_service

View File

@@ -217,9 +217,6 @@ export class HuiEnergyDevicesGraphCard
show: true,
type: "value",
name: "kWh",
axisPointer: {
show: false,
},
};
options.yAxis = {
show: true,
@@ -554,12 +551,9 @@ export class HuiEnergyDevicesGraphCard
e.detail.seriesType === "pie" &&
e.detail.event?.target?.type === "tspan" // label
) {
const id = (e.detail.data as any).id as string;
if (id !== "untracked") {
fireEvent(this, "hass-more-info", {
entityId: id,
});
}
fireEvent(this, "hass-more-info", {
entityId: (e.detail.data as any).id as string,
});
}
}

View File

@@ -166,24 +166,10 @@ export class HuiEnergySourcesTableCard
cost: number | null,
compareCost: number | null,
showCosts: boolean,
compare: boolean,
bulletColor?: { border: string; background: string },
isFinalTotal?: boolean
compare: boolean
) {
return html` <tr
class="mdc-data-table__row ${bulletColor && !isFinalTotal ? "" : "total"}"
>
<td class="mdc-data-table__cell cell-bullet">
${bulletColor
? html`<div
class="bullet"
style=${styleMap({
borderColor: bulletColor.border,
backgroundColor: bulletColor.background,
})}
></div>`
: nothing}
</td>
return html` <tr class="mdc-data-table__row total">
<td class="mdc-data-table__cell"></td>
<th class="mdc-data-table__cell" scope="row">${label}</th>
${compare
? html`<td class="mdc-data-table__cell mdc-data-table__cell--numeric">
@@ -353,8 +339,6 @@ export class HuiEnergySourcesTableCard
};
};
const showOnlyTotals = this._config.show_only_totals;
const _renderSimpleCategory = (type: "solar" | "gas" | "water") =>
html` ${types[type]?.map((source, idx) => {
const cost_stat =
@@ -377,10 +361,6 @@ export class HuiEnergySourcesTableCard
totalCostsCompare[type] += costCompare;
}
if (showOnlyTotals) {
return nothing;
}
return this._renderRow(
computedStyles,
type,
@@ -406,27 +386,7 @@ export class HuiEnergySourcesTableCard
hasCosts[type] ? totalCosts[type] : null,
hasCosts[type] ? totalCostsCompare[type] : null,
showCosts,
compare,
showOnlyTotals
? {
border: getEnergyColor(
computedStyles,
this.hass.themes.darkMode,
false,
false,
colorPropertyMap[type],
0
),
background: getEnergyColor(
computedStyles,
this.hass.themes.darkMode,
true,
false,
colorPropertyMap[type],
0
),
}
: undefined
compare
)
: ""}`;
@@ -514,10 +474,6 @@ export class HuiEnergySourcesTableCard
totalBattery += energyFrom - energyTo;
totalBatteryCompare += energyFromCompare - energyToCompare;
if (showOnlyTotals) {
return nothing;
}
return html` ${this._renderRow(
computedStyles,
"battery_out",
@@ -555,27 +511,7 @@ export class HuiEnergySourcesTableCard
null,
null,
showCosts,
compare,
showOnlyTotals
? {
border: getEnergyColor(
computedStyles,
this.hass.themes.darkMode,
false,
false,
colorPropertyMap.battery_out,
0
),
background: getEnergyColor(
computedStyles,
this.hass.themes.darkMode,
true,
false,
colorPropertyMap.battery_out,
0
),
}
: undefined
compare
)
: ""}
${types.grid?.map(
@@ -607,11 +543,6 @@ export class HuiEnergySourcesTableCard
totalGridCost += cost;
totalGridCostCompare += costCompare;
}
if (showOnlyTotals) {
return nothing;
}
return this._renderRow(
computedStyles,
"grid_consumption",
@@ -652,11 +583,6 @@ export class HuiEnergySourcesTableCard
totalGridCost -= cost;
totalGridCostCompare -= costCompare;
}
if (showOnlyTotals) {
return nothing;
}
return this._renderRow(
computedStyles,
"grid_return",
@@ -685,27 +611,7 @@ export class HuiEnergySourcesTableCard
hasGridCost ? totalGridCost : null,
hasGridCost ? totalGridCostCompare : null,
showCosts,
compare,
showOnlyTotals
? {
border: getEnergyColor(
computedStyles,
this.hass.themes.darkMode,
false,
false,
colorPropertyMap.grid_consumption,
0
),
background: getEnergyColor(
computedStyles,
this.hass.themes.darkMode,
true,
false,
colorPropertyMap.grid_consumption,
0
),
}
: undefined
compare
)
: ""}
${_renderSimpleCategory("gas")} ${_renderSimpleCategory("water")}
@@ -723,9 +629,7 @@ export class HuiEnergySourcesTableCard
totalGridCostCompare +
totalCostsCompare.water,
showCosts,
compare,
undefined,
true
compare
)
: ""}
</tbody>
@@ -768,8 +672,8 @@ export class HuiEnergySourcesTableCard
border-top: 1px solid var(--divider-color);
}
ha-card {
max-height: 100%;
overflow: auto;
height: 100%;
overflow: hidden;
}
.card-header {
padding-bottom: 0;

View File

@@ -23,9 +23,6 @@ const DEFAULT_CONFIG: Partial<PowerSankeyCardConfig> = {
group_by_area: true,
};
// Minimum power threshold in kW to display a device node
const MIN_POWER_THRESHOLD = 0.01;
interface PowerData {
solar: number;
from_grid: number;
@@ -254,75 +251,23 @@ class HuiPowerSankeyCard
let untrackedConsumption = homeNode.value;
const deviceNodes: Node[] = [];
const parentLinks: Record<string, string> = {};
// Build a map of device relationships for hierarchy resolution
// Key: stat_consumption (energy), Value: { stat_rate, included_in_stat }
const deviceMap = new Map<
string,
{ stat_rate?: string; included_in_stat?: string }
>();
prefs.device_consumption.forEach((device) => {
deviceMap.set(device.stat_consumption, {
stat_rate: device.stat_rate,
included_in_stat: device.included_in_stat,
});
});
// Set of stat_rate entities that will be rendered as nodes
const renderedStatRates = new Set<string>();
prefs.device_consumption.forEach((device) => {
if (device.stat_rate) {
const value = this._getCurrentPower(device.stat_rate);
if (value >= MIN_POWER_THRESHOLD) {
renderedStatRates.add(device.stat_rate);
}
}
});
// Find the effective parent for power hierarchy
// Walks up the chain to find an ancestor with stat_rate that will be rendered
const findEffectiveParent = (
includedInStat: string | undefined
): string | undefined => {
let currentParent = includedInStat;
while (currentParent) {
const parentDevice = deviceMap.get(currentParent);
if (!parentDevice) {
return undefined;
}
// If this parent has a stat_rate and will be rendered, use it
if (
parentDevice.stat_rate &&
renderedStatRates.has(parentDevice.stat_rate)
) {
return parentDevice.stat_rate;
}
// Otherwise, continue up the chain
currentParent = parentDevice.included_in_stat;
}
return undefined;
};
prefs.device_consumption.forEach((device, idx) => {
if (!device.stat_rate) {
return;
}
const value = this._getCurrentPower(device.stat_rate);
if (value < MIN_POWER_THRESHOLD) {
if (value < 0.01) {
return;
}
// Find the effective parent (may be different from direct parent if parent has no stat_rate)
const effectiveParent = findEffectiveParent(device.included_in_stat);
const node = {
id: device.stat_rate,
label: device.name || this._getEntityLabel(device.stat_rate),
value,
color: getGraphColorByIndex(idx, computedStyle),
index: 4,
parent: effectiveParent,
parent: device.included_in_stat,
};
if (node.parent) {
parentLinks[node.id] = node.parent;

View File

@@ -80,10 +80,9 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard {
throw new Error("Entities need to be an array");
}
const computedStyles = getComputedStyle(this);
this._calendars = config!.entities.map((entity, idx) => ({
entity_id: entity,
backgroundColor: getColorByIndex(idx, computedStyles),
backgroundColor: getColorByIndex(idx),
}));
if (this._config?.entities !== config.entities) {

View File

@@ -394,8 +394,7 @@ class HuiMapCard extends LitElement implements LovelaceCard {
if (color) {
return color;
}
const computedStyles = getComputedStyle(this);
color = getColorByIndex(this._colorIndex, computedStyles);
color = getColorByIndex(this._colorIndex);
this._colorIndex++;
this._colorDict[entityId] = color;
return color;

View File

@@ -194,7 +194,6 @@ export interface EnergySourcesTableCardConfig extends EnergyCardBaseConfig {
type: "energy-sources-table";
title?: string;
types?: (keyof EnergySourceByType)[];
show_only_totals?: boolean;
}
export interface EnergySolarGaugeCardConfig extends EnergyCardBaseConfig {

View File

@@ -98,32 +98,17 @@ class HuiWaterSankeyCard
const nodes: Node[] = [];
const links: Link[] = [];
// Calculate total water consumption from all sources or devices
const totalDownstreamConsumption = prefs.device_consumption_water.reduce(
(total, device) => {
const value =
device.stat_consumption in this._data!.stats
? calculateStatisticSumGrowth(
this._data!.stats[device.stat_consumption]
) || 0
: 0;
return total + value;
},
0
);
const totalSourceSupply = waterSources.reduce((total, source) => {
// Calculate total water consumption from all devices
let totalWaterConsumption = 0;
prefs.device_consumption_water.forEach((device) => {
const value =
source.stat_energy_from in this._data!.stats
device.stat_consumption in this._data!.stats
? calculateStatisticSumGrowth(
this._data!.stats[source.stat_energy_from]
this._data!.stats[device.stat_consumption]
) || 0
: 0;
return total + value;
}, 0);
const totalWaterConsumption = Math.max(
totalDownstreamConsumption,
totalSourceSupply
);
totalWaterConsumption += value;
});
// Create home/consumption node
const homeNode: Node = {

View File

@@ -67,10 +67,7 @@ export const handleAction = async (
await hass.loadBackendTranslation("title");
const localize = await hass.loadBackendTranslation("services");
serviceName = `${domainToName(localize, domain)}: ${
localize(
`component.${domain}.services.${service}.name`,
hass.services[domain][service].description_placeholders
) ||
localize(`component.${domain}.services.${service}.name`) ||
serviceDomains[domain][service].name ||
service
}`;

View File

@@ -37,7 +37,6 @@ import {
removeSearchParam,
} from "../../common/url/search-params";
import { debounce } from "../../common/util/debounce";
import { isMobileClient } from "../../util/is_mobile";
import { afterNextRender } from "../../common/util/render-status";
import "../../components/ha-button";
import "../../components/ha-button-menu";
@@ -127,7 +126,7 @@ interface UndoStackItem {
@customElement("hui-root")
class HUIRoot extends LitElement {
@property({ attribute: false }) public panel?: PanelInfo;
@property({ attribute: false }) public panel?: PanelInfo<LovelacePanelConfig>;
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -295,8 +294,7 @@ class HUIRoot extends LitElement {
overflowAction: this._handleShowQuickBar,
visible: !this._editMode,
overflow: this.narrow,
suffix:
this.hass.enableShortcuts && !isMobileClient ? "(E)" : undefined,
suffix: this.hass.enableShortcuts ? "(E)" : undefined,
},
{
icon: mdiCommentProcessingOutline,
@@ -306,8 +304,7 @@ class HUIRoot extends LitElement {
visible:
!this._editMode && this._conversation(this.hass.config.components),
overflow: this.narrow,
suffix:
this.hass.enableShortcuts && !isMobileClient ? "(A)" : undefined,
suffix: this.hass.enableShortcuts ? "(A)" : undefined,
},
{
icon: mdiRefresh,
@@ -543,72 +540,68 @@ class HUIRoot extends LitElement {
})}
>
<div class="header">
<slot name="toolbar">
<div class="toolbar">
${this._editMode
? html`
<div class="main-title">
${dashboardTitle ||
this.hass!.localize("ui.panel.lovelace.editor.header")}
<ha-icon-button
slot="actionItems"
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_lovelace.edit_title"
)}
.path=${mdiPencil}
class="edit-icon"
@click=${this._editDashboard}
></ha-icon-button>
</div>
<div class="action-items">${this._renderActionItems()}</div>
`
: html`
${isSubview
? html`
<ha-icon-button-arrow-prev
.hass=${this.hass}
slot="navigationIcon"
@click=${this._goBack}
></ha-icon-button-arrow-prev>
`
: html`
<ha-menu-button
slot="navigationIcon"
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`}
${isSubview
? html`
<div class="main-title">${curViewConfig.title}</div>
`
: hasTabViews
? tabs
: html`
<div class="main-title">
${views[0]?.title ?? dashboardTitle}
</div>
`}
<div class="action-items">${this._renderActionItems()}</div>
`}
</div>
<div class="toolbar">
${this._editMode
? html`
<div class="tab-bar">
${tabs}
<div class="main-title">
${dashboardTitle ||
this.hass!.localize("ui.panel.lovelace.editor.header")}
<ha-icon-button
slot="nav"
id="add-view"
@click=${this._addView}
slot="actionItems"
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.add"
"ui.panel.lovelace.editor.edit_lovelace.edit_title"
)}
.path=${mdiPlus}
.path=${mdiPencil}
class="edit-icon"
@click=${this._editDashboard}
></ha-icon-button>
</div>
<div class="action-items">${this._renderActionItems()}</div>
`
: nothing}
</slot>
: html`
${isSubview
? html`
<ha-icon-button-arrow-prev
.hass=${this.hass}
slot="navigationIcon"
@click=${this._goBack}
></ha-icon-button-arrow-prev>
`
: html`
<ha-menu-button
slot="navigationIcon"
.hass=${this.hass}
.narrow=${this.narrow}
></ha-menu-button>
`}
${isSubview
? html`<div class="main-title">${curViewConfig.title}</div>`
: hasTabViews
? tabs
: html`
<div class="main-title">
${views[0]?.title ?? dashboardTitle}
</div>
`}
<div class="action-items">${this._renderActionItems()}</div>
`}
</div>
${this._editMode
? html`
<div class="tab-bar">
${tabs}
<ha-icon-button
slot="nav"
id="add-view"
@click=${this._addView}
.label=${this.hass!.localize(
"ui.panel.lovelace.editor.edit_view.add"
)}
.path=${mdiPlus}
></ha-icon-button>
</div>
`
: nothing}
</div>
<hui-view-container
class=${this._editMode ? "has-tab-bar" : ""}

View File

@@ -42,8 +42,6 @@ const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
import("../../energy/strategies/energy-overview-view-strategy"),
"energy-electricity": () =>
import("../../energy/strategies/energy-electricity-view-strategy"),
"energy-water": () =>
import("../../energy/strategies/energy-water-view-strategy"),
map: () => import("./map/map-view-strategy"),
iframe: () => import("./iframe/iframe-view-strategy"),
area: () => import("./areas/area-view-strategy"),

View File

@@ -1,6 +1,5 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import { getAreasFloorHierarchy } from "../../../../common/areas/areas-floor-hierarchy";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import {
findEntities,
@@ -24,8 +23,8 @@ import type {
WeatherForecastCardConfig,
} from "../../cards/types";
import type { CommonControlSectionStrategyConfig } from "../usage_prediction/common-controls-section-strategy";
import { getAreasFloorHierarchy } from "../../../../common/areas/areas-floor-hierarchy";
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
import type { Condition } from "../../common/validate-condition";
export interface HomeOverviewViewStrategyConfig {
type: "home-overview";
@@ -71,12 +70,8 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
const floorCount = home.floors.length + (home.areas.length ? 1 : 0);
const maxColumns = 3;
const largeScreenCondition: Condition = {
condition: "screen",
media_query: "(min-width: 871px)",
};
// Allow between 2 and 3 columns (the max should be set to define the width of the header)
const maxColumns = 2;
const floorsSections: LovelaceSectionConfig[] = [];
for (const floorStructure of home.floors) {
@@ -131,6 +126,12 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
});
}
const favoriteSection: LovelaceSectionConfig = {
type: "grid",
column_span: maxColumns,
cards: [],
};
const favoriteEntities = (config.favorite_entities || []).filter(
(entityId) => hass.states[entityId] !== undefined
);
@@ -175,70 +176,74 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
({
type: "home-summary",
summary: "light",
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "/light?historyBack=1",
},
grid_options: {
columns: 12,
rows: 2,
columns: 4,
},
} satisfies HomeSummaryCard),
hasClimate &&
({
type: "home-summary",
summary: "climate",
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "/climate?historyBack=1",
},
grid_options: {
columns: 12,
rows: 2,
columns: 4,
},
} satisfies HomeSummaryCard),
hasSecurity &&
({
type: "home-summary",
summary: "security",
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "/security?historyBack=1",
},
grid_options: {
columns: 12,
rows: 2,
columns: 4,
},
} satisfies HomeSummaryCard),
hasMediaPlayers &&
({
type: "home-summary",
summary: "media_players",
vertical: true,
tap_action: {
action: "navigate",
navigation_path: "media-players",
},
grid_options: {
columns: 12,
rows: 2,
columns: 4,
},
} satisfies HomeSummaryCard),
].filter(Boolean) as LovelaceCardConfig[];
const forYouSection: LovelaceSectionConfig = {
const summarySection: LovelaceSectionConfig = {
type: "grid",
cards: [
{
type: "heading",
heading: hass.localize("ui.panel.lovelace.strategy.home.for_you"),
heading_style: "title",
visibility: [largeScreenCondition],
},
],
};
const widgetSection: LovelaceSectionConfig = {
column_span: maxColumns,
cards: [],
};
if (summaryCards.length) {
widgetSection.cards!.push(...summaryCards);
summarySection.cards!.push(
{
type: "heading",
heading: hass.localize("ui.panel.lovelace.strategy.home.summaries"),
},
...summaryCards
);
}
const weatherFilter = generateEntityFilter(hass, {
@@ -246,16 +251,28 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
entity_category: "none",
});
const widgetSection: LovelaceSectionConfig = {
type: "grid",
column_span: maxColumns,
cards: [],
};
const weatherEntity = Object.keys(hass.states)
.filter(weatherFilter)
.sort()[0];
if (weatherEntity) {
widgetSection.cards!.push({
type: "weather-forecast",
entity: weatherEntity,
forecast_type: "daily",
} as WeatherForecastCardConfig);
widgetSection.cards!.push(
{
type: "heading",
heading: "",
heading_style: "subtitle",
},
{
type: "weather-forecast",
entity: weatherEntity,
forecast_type: "daily",
} as WeatherForecastCardConfig
);
}
const energyPrefs = isComponentLoaded(hass, "energy")
@@ -282,19 +299,11 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
const sections = (
[
{
type: "grid",
cards: [
// Heading to add some spacing on large screens
{
type: "heading",
heading_style: "subtitle",
visibility: [largeScreenCondition],
},
],
},
favoriteSection.cards && favoriteSection,
commonControlsSection,
summarySection.cards && summarySection,
...floorsSections,
widgetSection.cards && widgetSection,
] satisfies (LovelaceSectionRawConfig | undefined)[]
).filter(Boolean) as LovelaceSectionRawConfig[];
@@ -310,11 +319,6 @@ export class HomeOverviewViewStrategy extends ReactiveElement {
content: `## ${hass.localize("ui.panel.lovelace.strategy.home.welcome_user", { user: "{{ user }}" })}`,
} satisfies MarkdownCardConfig,
},
sidebar: {
sections: [forYouSection, widgetSection],
content_label: hass.localize("ui.panel.lovelace.strategy.home.home"),
sidebar_label: hass.localize("ui.panel.lovelace.strategy.home.for_you"),
},
};
}
}

View File

@@ -31,7 +31,6 @@ import {
import type { HuiSection } from "../sections/hui-section";
import type { Lovelace } from "../types";
import "./hui-view-header";
import "./hui-view-sidebar";
export const DEFAULT_MAX_COLUMNS = 4;
@@ -47,8 +46,6 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
@property({ attribute: false }) public isStrategy = false;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public sections: HuiSection[] = [];
@property({ attribute: false }) public cards: HuiCard[] = [];
@@ -61,12 +58,6 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
@state() _dragging = false;
@state() private _showSidebar = false;
private _contentScrollTop = 0;
private _sidebarScrollTop = 0;
private _columnsController = new ResizeController(this, {
callback: (entries) => {
const totalWidth = entries[0]?.contentRect.width;
@@ -123,7 +114,6 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
"section-visibility-changed",
this._sectionVisibilityChanged
);
this._showSidebar = Boolean(window.history.state?.sidebar);
}
disconnectedCallback(): void {
@@ -145,31 +135,16 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
const sections = this.sections;
const totalSectionCount =
this._sectionColumnCount +
(this.lovelace?.editMode ? 1 : 0) +
(this._config?.sidebar ? 1 : 0);
this._sectionColumnCount + (this.lovelace?.editMode ? 1 : 0);
const editMode = this.lovelace.editMode;
const maxColumnCount = this._columnsController.value ?? 1;
const columnCount = Math.min(maxColumnCount, totalSectionCount);
// On mobile with sidebar, use full width for whichever view is active
const contentColumnCount =
this._config?.sidebar && !this.narrow
? Math.max(1, columnCount - 1)
: columnCount;
return html`
<div
class="wrapper ${classMap({
"top-margin": Boolean(this._config?.top_margin),
"has-sidebar": Boolean(this._config?.sidebar),
narrow: this.narrow,
})}"
style=${styleMap({
"--column-count": columnCount,
"--content-column-count": contentColumnCount,
})}
>
<hui-view-header
.hass=${this.hass}
@@ -177,54 +152,38 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
.lovelace=${this.lovelace}
.viewIndex=${this.index}
.config=${this._config?.header}
style=${styleMap({
"--max-column-count": maxColumnCount,
})}
></hui-view-header>
${this.narrow && this._config?.sidebar
? html`
<div class="mobile-tabs">
<ha-control-select
.value=${this._showSidebar ? "sidebar" : "content"}
@value-changed=${this._viewChanged}
.options=${[
{
value: "content",
label: this._config.sidebar.content_label,
},
{
value: "sidebar",
label: this._config.sidebar.sidebar_label,
},
]}
>
</ha-control-select>
</div>
`
: nothing}
<div class="container">
<ha-sortable
.disabled=${!editMode}
@item-moved=${this._sectionMoved}
group="section"
handle-selector=".handle"
draggable-selector=".section"
.rollback=${false}
<ha-sortable
.disabled=${!editMode}
@item-moved=${this._sectionMoved}
group="section"
handle-selector=".handle"
draggable-selector=".section"
.rollback=${false}
>
<div
class="container ${classMap({
dense: Boolean(this._config?.dense_section_placement),
})}"
style=${styleMap({
"--total-section-count": totalSectionCount,
"--max-column-count": maxColumnCount,
})}
>
<div
class="content ${classMap({
dense: Boolean(this._config?.dense_section_placement),
"mobile-hidden": this.narrow && this._showSidebar,
})}"
>
${repeat(
sections,
(section) => this._getSectionKey(section),
(section, idx) => {
const columnSpan = Math.min(
section.config.column_span || 1,
contentColumnCount
);
const rowSpan = section.config.row_span || 1;
${repeat(
sections,
(section) => this._getSectionKey(section),
(section, idx) => {
const columnSpan = Math.min(
section.config.column_span || 1,
maxColumnCount
);
const rowSpan = section.config.row_span || 1;
return html`
return html`
<div
class="section"
style=${styleMap({
@@ -249,89 +208,72 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
</div>
</div>
`;
}
)}
${editMode
? html`
<ha-sortable
group="card"
@item-added=${this._handleCardAdded}
draggable-selector=".card"
.rollback=${false}
>
<div class="create-section-container">
<div class="drop-helper" aria-hidden="true">
<p>
${this.hass.localize(
"ui.panel.lovelace.editor.section.drop_card_create_section"
)}
</p>
</div>
<button
class="create-section"
@click=${this._createSection}
aria-label=${this.hass.localize(
"ui.panel.lovelace.editor.section.create_section"
}
)}
${editMode
? html`
<ha-sortable
group="card"
@item-added=${this._handleCardAdded}
draggable-selector=".card"
.rollback=${false}
>
<div class="create-section-container">
<div class="drop-helper" aria-hidden="true">
<p>
${this.hass.localize(
"ui.panel.lovelace.editor.section.drop_card_create_section"
)}
.title=${this.hass.localize(
"ui.panel.lovelace.editor.section.create_section"
)}
>
<ha-ripple></ha-ripple>
<ha-svg-icon .path=${mdiViewGridPlus}></ha-svg-icon>
</button>
</p>
</div>
</ha-sortable>
`
: nothing}
</div>
</ha-sortable>
${this._config?.sidebar
? html`
<hui-view-sidebar
class=${classMap({
"mobile-hidden": this.narrow && !this._showSidebar,
})}
.hass=${this.hass}
.badges=${this.badges}
.lovelace=${this.lovelace}
.viewIndex=${this.index}
.config=${this._config.sidebar}
></hui-view-sidebar>
`
: nothing}
</div>
<div class="imported-cards-section">
${editMode && this._config?.cards?.length
? html`
<div class="section imported-cards">
<div class="imported-card-header">
<p class="title">
<ha-svg-icon .path=${mdiEyeOff}></ha-svg-icon>
${this.hass.localize(
"ui.panel.lovelace.editor.section.imported_cards_title"
<button
class="create-section"
@click=${this._createSection}
aria-label=${this.hass.localize(
"ui.panel.lovelace.editor.section.create_section"
)}
.title=${this.hass.localize(
"ui.panel.lovelace.editor.section.create_section"
)}
>
<ha-ripple></ha-ripple>
<ha-svg-icon .path=${mdiViewGridPlus}></ha-svg-icon>
</button>
</div>
</ha-sortable>
`
: nothing}
${editMode && this._config?.cards?.length
? html`
<div class="section imported-cards">
<div class="imported-card-header">
<p class="title">
<ha-svg-icon .path=${mdiEyeOff}></ha-svg-icon>
${this.hass.localize(
"ui.panel.lovelace.editor.section.imported_cards_title"
)}
</p>
<p class="subtitle">
${this.hass.localize(
"ui.panel.lovelace.editor.section.imported_cards_description"
)}
</p>
</div>
<hui-section
.lovelace=${this.lovelace}
.hass=${this.hass}
.config=${this._importedCardSectionConfig(
this._config.cards
)}
</p>
<p class="subtitle">
${this.hass.localize(
"ui.panel.lovelace.editor.section.imported_cards_description"
)}
</p>
.viewIndex=${this.index}
preview
import-only
></hui-section>
</div>
<hui-section
.lovelace=${this.lovelace}
.hass=${this.hass}
.config=${this._importedCardSectionConfig(
this._config.cards
)}
.viewIndex=${this.index}
preview
import-only
></hui-section>
</div>
`
: nothing}
</div>
`
: nothing}
</div>
</ha-sortable>
</div>
`;
}
@@ -410,40 +352,6 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
this.lovelace!.saveConfig(newConfig);
}
private _viewChanged(ev: CustomEvent) {
const newValue = ev.detail.value;
const shouldShowSidebar = newValue === "sidebar";
if (shouldShowSidebar !== this._showSidebar) {
this._toggleView();
}
}
private _toggleView() {
// Save current scroll position
if (this._showSidebar) {
this._sidebarScrollTop = window.scrollY;
} else {
this._contentScrollTop = window.scrollY;
}
this._showSidebar = !this._showSidebar;
// Add sidebar state to history
window.history.replaceState(
{ ...window.history.state, sidebar: this._showSidebar },
""
);
// Restore scroll position after view updates
this.updateComplete.then(() => {
const scrollY = this._showSidebar
? this._sidebarScrollTop
: this._contentScrollTop;
window.scrollTo(0, scrollY);
});
}
static styles = css`
:host {
--row-height: var(--ha-view-sections-row-height, 56px);
@@ -461,19 +369,14 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
}
}
.wrapper {
.wrapper.top-margin {
display: block;
padding: var(--row-gap) var(--column-gap);
box-sizing: content-box;
margin: 0 auto;
max-width: calc(
var(--column-count) * var(--column-max-width) +
(var(--column-count) - 1) * var(--column-gap)
);
margin-top: var(--top-margin);
}
.wrapper.top-margin {
margin-top: var(--top-margin);
.container > * {
position: relative;
width: 100%;
}
.section {
@@ -487,91 +390,22 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
}
.container {
display: grid;
grid-template-columns: [content-start] repeat(
var(--content-column-count),
1fr
);
gap: var(--row-gap) var(--column-gap);
padding: var(--row-gap) 0;
}
.wrapper.has-sidebar .container {
grid-template-columns:
[content-start] repeat(var(--content-column-count), 1fr)
[sidebar-start] 1fr;
}
/* On mobile with sidebar, content and sidebar both take full width */
.wrapper.narrow.has-sidebar .container {
grid-template-columns: 1fr;
}
hui-view-sidebar {
grid-column: sidebar-start / -1;
}
.wrapper.narrow hui-view-sidebar {
grid-column: 1 / -1;
padding-bottom: calc(
var(--ha-space-4) + 56px + var(--ha-space-4) +
env(safe-area-inset-bottom)
);
}
.mobile-hidden {
display: none !important;
}
.mobile-tabs {
position: fixed;
bottom: calc(var(--ha-space-3) + env(safe-area-inset-bottom));
left: 50%;
transform: translateX(-50%);
padding: 0;
z-index: 1;
}
.mobile-tabs ha-control-select {
width: max-content;
min-width: 280px;
max-width: 90%;
--control-select-thickness: var(--ha-space-14);
--control-select-border-radius: var(--ha-border-radius-pill);
--control-select-background: var(--card-background-color);
--control-select-background-opacity: 1;
--control-select-color: var(--primary-color);
--control-select-padding: 6px;
box-shadow: rgba(0, 0, 0, 0.3) 0px 4px 10px 0px;
}
ha-sortable {
display: contents;
}
.content {
grid-column: content-start / sidebar-start;
grid-row: 1 / -1;
--column-count: min(var(--max-column-count), var(--total-section-count));
display: grid;
align-items: start;
justify-content: center;
grid-template-columns: repeat(var(--content-column-count), 1fr);
grid-template-columns: repeat(var(--column-count), 1fr);
grid-auto-flow: row;
gap: var(--row-gap) var(--column-gap);
}
.wrapper.narrow .content {
grid-column: 1 / -1;
}
.wrapper.narrow.has-sidebar .content {
padding-bottom: calc(
var(--ha-space-4) + 56px + var(--ha-space-4) +
env(safe-area-inset-bottom)
padding: var(--row-gap) var(--column-gap);
box-sizing: content-box;
margin: 0 auto;
max-width: calc(
var(--column-count) * var(--column-max-width) +
(var(--column-count) - 1) * var(--column-gap)
);
}
.content.dense {
.container.dense {
grid-auto-flow: row dense;
}
@@ -649,7 +483,13 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
hui-view-header {
display: block;
padding: 0 var(--column-gap);
padding-top: var(--row-gap);
margin: auto;
max-width: calc(
var(--max-column-count) * var(--column-max-width) +
(var(--max-column-count) - 1) * var(--column-gap)
);
}
.imported-cards {

View File

@@ -1,57 +0,0 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import type { LovelaceViewSidebarConfig } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
import "../sections/hui-section";
import type { Lovelace } from "../types";
export const DEFAULT_VIEW_SIDEBAR_LAYOUT = "start";
@customElement("hui-view-sidebar")
export class HuiViewSidebar extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public lovelace!: Lovelace;
@property({ attribute: false }) public config?: LovelaceViewSidebarConfig;
@property({ attribute: false }) public viewIndex!: number;
render() {
if (!this.lovelace) return nothing;
// Use preview mode instead of setting lovelace to avoid the sections to be
// editable as it is not yet supported
return html`
<div class="container">
${repeat(
this.config?.sections || [],
(section) => html`
<hui-section
.config=${section}
.hass=${this.hass}
.preview=${this.lovelace.editMode}
.viewIndex=${this.viewIndex}
></hui-section>
`
)}
</div>
`;
}
static styles = css`
.container {
display: flex;
flex-direction: column;
gap: var(--row-gap, 8px);
width: 100%;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"hui-view-sidebar": HuiViewSidebar;
}
}

View File

@@ -1,17 +1,14 @@
import { mdiViewDashboard } from "@mdi/js";
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../components/ha-divider";
import "../../components/ha-icon";
import "../../components/ha-list-item";
import "../../components/ha-select";
import "../../components/ha-settings-row";
import "../../components/ha-svg-icon";
import { saveFrontendUserData } from "../../data/frontend";
import type { LovelaceDashboard } from "../../data/lovelace/dashboard";
import { fetchDashboards } from "../../data/lovelace/dashboard";
import { getPanelIcon, getPanelTitle } from "../../data/panel";
import { getPanelTitle } from "../../data/panel";
import type { HomeAssistant, PanelInfo } from "../../types";
import { PANEL_DASHBOARDS } from "../config/lovelace/dashboards/ha-config-lovelace-dashboards";
@@ -40,57 +37,54 @@ class HaPickDashboardRow extends LitElement {
<span slot="description">
${this.hass.localize("ui.panel.profile.dashboard.description")}
</span>
<ha-select
.label=${this.hass.localize(
"ui.panel.profile.dashboard.dropdown_label"
)}
.value=${value}
@selected=${this._dashboardChanged}
naturalMenuWidth
>
<ha-list-item .value=${USE_SYSTEM_VALUE}>
${this.hass.localize("ui.panel.profile.dashboard.system")}
</ha-list-item>
<ha-divider></ha-divider>
<ha-list-item value="lovelace" graphic="icon">
<ha-svg-icon slot="graphic" .path=${mdiViewDashboard}></ha-svg-icon>
${this.hass.localize("ui.panel.profile.dashboard.lovelace")}
</ha-list-item>
${PANEL_DASHBOARDS.map((panel) => {
const panelInfo = this.hass.panels[panel] as PanelInfo | undefined;
if (!panelInfo) {
return nothing;
}
return html`
<ha-list-item value=${panelInfo.url_path} graphic="icon">
<ha-icon
slot="graphic"
.icon=${getPanelIcon(panelInfo)}
></ha-icon>
${getPanelTitle(this.hass, panelInfo)}
${this._dashboards
? html`<ha-select
.label=${this.hass.localize(
"ui.panel.profile.dashboard.dropdown_label"
)}
.disabled=${!this._dashboards?.length}
.value=${value}
@selected=${this._dashboardChanged}
naturalMenuWidth
>
<ha-list-item .value=${USE_SYSTEM_VALUE}>
${this.hass.localize("ui.panel.profile.dashboard.system")}
</ha-list-item>
`;
})}
${this._dashboards?.length
? html`
<ha-divider></ha-divider>
${this._dashboards.map((dashboard) => {
if (!this.hass.user!.is_admin && dashboard.require_admin) {
return "";
}
return html`
<ha-list-item .value=${dashboard.url_path} graphic="icon">
<ha-icon
slot="graphic"
.icon=${dashboard.icon || "mdi:view-dashboard"}
></ha-icon>
${dashboard.title}
</ha-list-item>
`;
})}
`
: nothing}
</ha-select>
<ha-divider></ha-divider>
<ha-list-item value="lovelace">
${this.hass.localize("ui.panel.profile.dashboard.lovelace")}
</ha-list-item>
${PANEL_DASHBOARDS.map((panel) => {
const panelInfo = this.hass.panels[panel] as
| PanelInfo
| undefined;
if (!panelInfo) {
return nothing;
}
return html`
<ha-list-item value=${panelInfo.url_path}>
${getPanelTitle(this.hass, panelInfo)}
</ha-list-item>
`;
})}
<ha-divider></ha-divider>
${this._dashboards.map((dashboard) => {
if (!this.hass.user!.is_admin && dashboard.require_admin) {
return "";
}
return html`
<ha-list-item .value=${dashboard.url_path}>
${dashboard.title}
</ha-list-item>
`;
})}
</ha-select>`
: html`<ha-select
.label=${this.hass.localize(
"ui.panel.profile.dashboard.dropdown_label"
)}
disabled
></ha-select>`}
</ha-settings-row>
`;
}

View File

@@ -14,8 +14,6 @@ interface PassThroughNode {
id: string;
value: number;
depth: number;
sourceId: string;
targetId: string;
}
interface GraphLink extends GraphEdge {
@@ -154,8 +152,6 @@ export function createPassThroughNode(
id: `${sourceId}-${targetId}-${depth}`,
value,
depth,
sourceId,
targetId,
};
}
@@ -241,79 +237,6 @@ export function groupNodesBySection(
return nodesPerSection;
}
export function sortNodesInSections(
nodesPerSection: Record<number, Node[]>,
depths: number[]
): Record<number, Node[]> {
const sortedSections: Record<number, Node[]> = {};
depths.forEach((depth, depthIndex) => {
const sectionNodes = nodesPerSection[depth] || [];
// Sort nodes to minimize crossings
const sortedNodes = [...sectionNodes].sort((a, b) => {
const aIsPassthrough = isPassThroughNode(a);
const bIsPassthrough = isPassThroughNode(b);
// Both are passthrough nodes - sort by source position
if (aIsPassthrough && bIsPassthrough) {
// Find positions of source nodes in previous section (use already sorted section)
if (depthIndex > 0) {
const prevDepth = depths[depthIndex - 1];
const prevSection =
sortedSections[prevDepth] || nodesPerSection[prevDepth] || [];
const aSourceIndex = prevSection.findIndex((n) => {
const nodeId = isPassThroughNode(n) ? n.id : (n as GraphNode).id;
return nodeId === a.sourceId;
});
const bSourceIndex = prevSection.findIndex((n) => {
const nodeId = isPassThroughNode(n) ? n.id : (n as GraphNode).id;
return nodeId === b.sourceId;
});
if (
aSourceIndex !== bSourceIndex &&
aSourceIndex !== -1 &&
bSourceIndex !== -1
) {
return aSourceIndex - bSourceIndex;
}
}
// Fall back to target node positions in next section (not sorted yet, use original)
if (depthIndex < depths.length - 1) {
const nextDepth = depths[depthIndex + 1];
const nextSection = nodesPerSection[nextDepth] || [];
const aTargetIndex = nextSection.findIndex((n) => {
const nodeId = isPassThroughNode(n) ? n.id : (n as GraphNode).id;
return nodeId === a.targetId;
});
const bTargetIndex = nextSection.findIndex((n) => {
const nodeId = isPassThroughNode(n) ? n.id : (n as GraphNode).id;
return nodeId === b.targetId;
});
if (
aTargetIndex !== bTargetIndex &&
aTargetIndex !== -1 &&
bTargetIndex !== -1
) {
return aTargetIndex - bTargetIndex;
}
}
}
return 0;
});
sortedSections[depth] = sortedNodes;
});
return sortedSections;
}
export function createSectionNodes(nodes: Node[]): SectionNode[] {
return nodes.map(
(node: Node): SectionNode => ({
@@ -414,11 +337,10 @@ function processNodes(
);
const nodesPerSection = groupNodesBySection(nodes, passThroughNodes);
const sortedNodesPerSection = sortNodesInSections(nodesPerSection, depths);
let globalValueToSizeRatio = 0;
const sections = depths.map((depth) => {
const sectionNodes = createSectionNodes(sortedNodesPerSection[depth] || []);
const sectionNodes = createSectionNodes(nodesPerSection[depth] || []);
const availableSpace = sectionSize - (sectionNodes.length + 1) * nodeGap;
const totalValue = sectionNodes.reduce(
(acc: number, node: SectionNode) => acc + node.value,

View File

@@ -92,62 +92,6 @@ export const colorStyles = css`
--black-color: #000000;
--white-color: #ffffff;
/* colors - used for graphs, calendars, maps, etc */
--color-1: #4269d0;
--color-2: #f4bd4a;
--color-3: #ff725c;
--color-4: #6cc5b0;
--color-5: #a463f2;
--color-6: #ff8ab7;
--color-7: #9c6b4e;
--color-8: #97bbf5;
--color-9: #01ab63;
--color-10: #094bad;
--color-11: #c99000;
--color-12: #d84f3e;
--color-13: #49a28f;
--color-14: #048732;
--color-15: #d96895;
--color-16: #8043ce;
--color-17: #7599d1;
--color-18: #7a4c31;
--color-19: #6989f4;
--color-20: #ffd444;
--color-21: #ff957c;
--color-22: #8fe9d3;
--color-23: #62cc71;
--color-24: #ffadda;
--color-25: #c884ff;
--color-26: #badeff;
--color-27: #bf8b6d;
--color-28: #927acc;
--color-29: #97ee3f;
--color-30: #bf3947;
--color-31: #9f5b00;
--color-32: #f48758;
--color-33: #8caed6;
--color-34: #f2b94f;
--color-35: #eff26e;
--color-36: #e43872;
--color-37: #d9b100;
--color-38: #9d7a00;
--color-39: #698cff;
--color-40: #00d27e;
--color-41: #d06800;
--color-42: #009f82;
--color-43: #c49200;
--color-44: #cbe8ff;
--color-45: #fecddf;
--color-46: #c27eb6;
--color-47: #8cd2ce;
--color-48: #c4b8d9;
--color-49: #f883b0;
--color-50: #a49100;
--color-51: #f48800;
--color-52: #27d0df;
--color-53: #a04a9b;
--color-54: #4269d0;
/* history colors */
--history-unavailable-color: transparent;

View File

@@ -52,8 +52,6 @@ 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-quiet: var(--ha-color-text-secondary);
--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));
@@ -64,7 +62,5 @@ export const waColorStyles = css`
--wa-focus-ring-color: var(--ha-color-neutral-60);
--wa-shadow-l: 4px 8px 12px 0 rgba(0, 0, 0, 0.3);
--wa-color-text-normal: var(--ha-color-text-primary);
}
`;

View File

@@ -9,16 +9,12 @@ export const waMainStyles = css`
--wa-focus-ring-offset: 2px;
--wa-focus-ring: var(--wa-focus-ring-style) var(--wa-focus-ring-width) var(--wa-focus-ring-color);
--wa-space-xs: var(--ha-space-2);
--wa-space-m: var(--ha-space-4);
--wa-space-l: var(--ha-space-6);
--wa-space-xl: var(--ha-space-8);
--wa-form-control-padding-block: 0.75em;
--wa-form-control-value-line-height: var(--ha-line-height-condensed);
--wa-font-weight-action: var(--ha-font-weight-medium);
--wa-transition-normal: 150ms;
--wa-transition-fast: 75ms;
--wa-transition-easing: ease;
@@ -32,7 +28,6 @@ export const waMainStyles = css`
--wa-line-height-condensed: var(--ha-line-height-condensed);
--wa-font-size-m: var(--ha-font-size-m);
--wa-shadow-s: var(--ha-box-shadow-s);
--wa-shadow-m: var(--ha-box-shadow-m);
--wa-shadow-l: var(--ha-box-shadow-l);

View File

@@ -14,7 +14,6 @@ import type { HassElement } from "./hass-element";
import { ShortcutManager } from "../common/keyboard/shortcuts";
import { extractSearchParamsObject } from "../common/url/search-params";
import { showVoiceCommandDialog } from "../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
import { canOverrideAlphanumericInput } from "../common/dom/can-override-input";
import { showShortcutsDialog } from "../dialogs/shortcuts/show-shortcuts-dialog";
import type { Redirects } from "../panels/my/ha-panel-my";
@@ -88,7 +87,6 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
private _showVoiceCommandDialog(e: KeyboardEvent) {
if (
!this.hass?.enableShortcuts ||
!canOverrideAlphanumericInput(e.composedPath()) ||
!this._conversation(this.hass.config.components)
) {
return;
@@ -106,7 +104,7 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
e: KeyboardEvent,
mode: QuickBarMode = QuickBarMode.Entity
) {
if (!this._canShowQuickBar(e)) {
if (!this._canShowQuickBar()) {
return;
}
@@ -119,7 +117,7 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
}
private _showShortcutDialog(e: KeyboardEvent) {
if (!this._canShowQuickBar(e)) {
if (!this._canShowQuickBar()) {
return;
}
@@ -132,10 +130,7 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
}
private async _createMyLink(e: KeyboardEvent) {
if (
!this.hass?.enableShortcuts ||
!canOverrideAlphanumericInput(e.composedPath())
) {
if (!this.hass?.enableShortcuts) {
return;
}
@@ -194,11 +189,7 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
});
}
private _canShowQuickBar(e: KeyboardEvent) {
return (
this.hass?.user?.is_admin &&
this.hass.enableShortcuts &&
canOverrideAlphanumericInput(e.composedPath())
);
private _canShowQuickBar() {
return this.hass?.user?.is_admin && this.hass.enableShortcuts;
}
};

View File

@@ -529,6 +529,10 @@
"text": {
"show_password": "Show password",
"hide_password": "Hide password"
},
"number": {
"value": "Value",
"entity_value": "Entity value"
}
},
"logbook": {
@@ -2227,9 +2231,7 @@
"title": "Edit home page",
"description": "Configure your home page display preferences.",
"favorite_entities_helper": "Display your favorite entities. Home Assistant will still suggest based on commonly used up to 8 slots.",
"save_failed": "Failed to save home page configuration",
"areas_hint": "You can rearrange your floors and areas in the order that best represents your house on the {areas_page}.",
"areas_page": "areas page"
"save_failed": "Failed to save home page configuration"
}
},
"my": {
@@ -4028,26 +4030,7 @@
"item_pasted": "{item} pasted",
"ctrl": "Ctrl",
"del": "Del",
"targets": "Targets",
"select_target": "Select a target",
"home": "Home",
"unassigned": "Unassigned",
"blocks": "Blocks",
"show_more": "Show more",
"unassigned_entities": "Unassigned entities",
"unassigned_devices": "Unassigned devices",
"empty_section_search": {
"block": "No blocks found for {term}",
"entity": "No entities found for {term}",
"device": "No devices found for {term}",
"area": "No areas or floors found for {term}",
"label": "No labels found for {term}"
},
"load_target_items_failed": "Failed to load target items for",
"other_areas": "Other areas",
"services": "Services",
"helpers": "Helpers",
"entity_hidden": "[%key:ui::panel::config::devices::entities::hidden%]",
"triggers": {
"name": "Triggers",
"header": "When",
@@ -4055,10 +4038,7 @@
"learn_more": "Learn more about triggers",
"triggered": "Triggered",
"add": "Add trigger",
"empty_search": {
"global": "No triggers and targets found for {term}",
"item": "No triggers found for {term}"
},
"empty_search": "No triggers found for {term}",
"id": "Trigger ID",
"optional": "Optional",
"edit_id": "Edit ID",
@@ -4079,7 +4059,6 @@
"copied_to_clipboard": "Trigger copied to clipboard",
"cut_to_clipboard": "Trigger cut to clipboard",
"select": "Select a trigger",
"no_items_for_target": "No triggers available for",
"groups": {
"device": {
"label": "Device"
@@ -4087,9 +4066,6 @@
"entity": {
"label": "Entity"
},
"helpers": {
"label": "Helpers"
},
"time_location": {
"label": "Time and location"
},
@@ -4178,11 +4154,7 @@
"label": "Numeric state",
"above": "Above",
"below": "Below",
"lower_limit": "Lower limit",
"upper_limit": "Upper limit",
"value_template": "Value template",
"type_value": "Fixed number",
"type_input": "Numeric value of another entity",
"description": {
"picker": "When the numeric value of an entity''s state (or attribute''s value) crosses a given threshold.",
"above": "When {attribute, select, \n undefined {} \n other {{attribute} from }\n }{entity} {numberOfEntities, plural,\n one {is}\n other {are}\n} above {above}{duration, select, \n undefined {} \n other { for {duration}}\n }",
@@ -4324,10 +4296,7 @@
"description": "All conditions added here need to be satisfied for the automation to run. A condition can be satisfied or not at any given time, for example: ''If {user} is home''. You can use building blocks to create more complex conditions.",
"learn_more": "Learn more about conditions",
"add": "Add condition",
"empty_search": {
"global": "No conditions, blocks and targets found for {term}",
"item": "No conditions found for {term}"
},
"empty_search": "No conditions and blocks found for {term}",
"add_building_block": "Add building block",
"test": "Test",
"testing_error": "Condition did not pass",
@@ -4350,7 +4319,6 @@
"copied_to_clipboard": "Condition copied to clipboard",
"cut_to_clipboard": "Condition cut to clipboard",
"select": "Select a condition",
"no_items_for_target": "No conditions available for",
"groups": {
"device": {
"label": "Device"
@@ -4361,9 +4329,6 @@
"time_location": {
"label": "Time and location"
},
"helpers": {
"label": "Helpers"
},
"other": {
"label": "Other conditions"
},
@@ -4499,10 +4464,7 @@
"description": "All actions added here will be performed in sequence when the automation runs. An action usually controls one of your areas, devices, or entities, for example: 'Turn on the lights'. You can use building blocks to create more complex sequences of actions.",
"learn_more": "Learn more about actions",
"add": "Add action",
"empty_search": {
"global": "No actions, blocks and targets found for {term}",
"item": "No actions found for {term}"
},
"empty_search": "No actions and blocks found for {term}",
"add_building_block": "Add building block",
"invalid_action": "Invalid action",
"run": "Run action",
@@ -4527,7 +4489,6 @@
"copied_to_clipboard": "Action copied to clipboard",
"cut_to_clipboard": "Action cut to clipboard",
"select": "Select an action",
"no_items_for_target": "No actions available for",
"groups": {
"device_id": {
"label": "Device"
@@ -5468,11 +5429,6 @@
"partial_failure": "Some devices failed to delete successfully. Check system logs for more information."
}
}
},
"esphome": {
"show_encryption_key": "Show encryption key",
"encryption_key_title": "ESPHome Encryption Key",
"encryption_key_description": "This is the encryption key for your ESPHome device. Keep it in a safe place, as you may need it when transferring devices between Home Assistant instances."
}
},
"entities": {
@@ -7132,9 +7088,7 @@
"unamed_device": "Unnamed device",
"others": "Others",
"scenes": "Scenes",
"automations": "Automations",
"for_you": "For you",
"home": "Home"
"automations": "Automations"
},
"common_controls": {
"not_loaded": "Usage Prediction integration is not loaded.",
@@ -7232,9 +7186,9 @@
"solar_total": "Solar total",
"water_total": "Water total",
"source": "Source",
"energy": "Usage",
"energy": "Energy",
"cost": "Cost",
"previous_energy": "Previous usage",
"previous_energy": "Previous energy",
"previous_cost": "Previous cost",
"battery_total": "Battery total",
"total_costs": "Total costs"

View File

@@ -1,14 +1,8 @@
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { afterEach, describe, expect, test, vi } from "vitest";
let askWrite;
const HASS_URL = `${location.protocol}//${location.host}`;
describe("token_storage.askWrite", () => {
beforeEach(() => {
vi.stubGlobal("__HASS_URL__", HASS_URL);
});
afterEach(() => {
vi.resetModules();
});

View File

@@ -4,12 +4,9 @@ import { FallbackStorage } from "../../../test_helper/local-storage-fallback";
let saveTokens;
const HASS_URL = `${location.protocol}//${location.host}`;
describe("token_storage.saveTokens", () => {
beforeEach(() => {
window.localStorage = new FallbackStorage();
vi.stubGlobal("__HASS_URL__", HASS_URL);
});
afterEach(() => {

View File

@@ -2,8 +2,6 @@ import { describe, it, expect, test, vi, afterEach, beforeEach } from "vitest";
import type { AuthData } from "home-assistant-js-websocket";
import { FallbackStorage } from "../../../test_helper/local-storage-fallback";
const HASS_URL = `${location.protocol}//${location.host}`;
describe("token_storage", () => {
beforeEach(() => {
vi.stubGlobal(
@@ -13,7 +11,6 @@ describe("token_storage", () => {
writeEnabled: undefined,
})
);
vi.stubGlobal("__HASS_URL__", HASS_URL);
window.localStorage = new FallbackStorage();
});

View File

@@ -2,63 +2,34 @@ import { describe, test, expect } from "vitest";
import {
getColorByIndex,
getGraphColorByIndex,
COLORS_COUNT,
COLORS,
} from "../../../src/common/color/colors";
import { theme2hex } from "../../../src/common/color/convert-color";
describe("getColorByIndex", () => {
test("return the correct color from CSS variable", () => {
const style = {
getPropertyValue: (prop) => {
if (prop === "--color-1") return "#4269d0";
if (prop === "--color-11") return "#c99000";
return "";
},
} as CSSStyleDeclaration;
expect(getColorByIndex(0, style)).toBe(theme2hex("#4269d0"));
expect(getColorByIndex(10, style)).toBe(theme2hex("#c99000"));
test("return the correct color for a given index", () => {
expect(getColorByIndex(0)).toBe(COLORS[0]);
expect(getColorByIndex(10)).toBe(COLORS[10]);
});
test("wrap around if the index is greater than the total count", () => {
const style = {
getPropertyValue: (prop) => {
if (prop === "--color-1") return "#4269d0";
if (prop === "--color-5") return "#a463f2";
return "";
},
} as CSSStyleDeclaration;
// Index 54 should wrap to color 1
expect(getColorByIndex(COLORS_COUNT, style)).toBe(theme2hex("#4269d0"));
// Index 58 should wrap to color 5
expect(getColorByIndex(COLORS_COUNT + 4, style)).toBe(theme2hex("#a463f2"));
test("wrap around if the index is greater than the length of COLORS", () => {
expect(getColorByIndex(COLORS.length)).toBe(COLORS[0]);
expect(getColorByIndex(COLORS.length + 4)).toBe(COLORS[4]);
});
});
describe("getGraphColorByIndex", () => {
test("return color from --graph-color variable when it exists", () => {
test("return the correct theme color if it exists", () => {
const style = {
getPropertyValue: (prop) => (prop === "--graph-color-1" ? "#123456" : ""),
} as CSSStyleDeclaration;
expect(getGraphColorByIndex(0, style)).toBe(theme2hex("#123456"));
});
test("fallback to --color variable when --graph-color does not exist", () => {
test("return the default color if the theme color does not exist", () => {
const style = {
getPropertyValue: (prop) => (prop === "--color-5" ? "#abcdef" : ""),
} as CSSStyleDeclaration;
// Index 4 should try --graph-color-5, then fallback to --color-5
expect(getGraphColorByIndex(4, style)).toBe(theme2hex("#abcdef"));
});
test("prefer --graph-color over --color when both exist", () => {
const style = {
getPropertyValue: (prop) => {
if (prop === "--graph-color-1") return "#111111";
if (prop === "--color-1") return "#222222";
return "";
},
} as CSSStyleDeclaration;
// Should prefer --graph-color-1
expect(getGraphColorByIndex(0, style)).toBe(theme2hex("#111111"));
getPropertyValue: () => "",
} as unknown as CSSStyleDeclaration;
expect(getGraphColorByIndex(0, style)).toBe(theme2hex(COLORS[0]));
});
});

View File

@@ -67,8 +67,6 @@ describe("Sankey Layout Functions", () => {
id: "test",
value: 10,
depth: 1,
sourceId: "source",
targetId: "target",
};
expect(isPassThroughNode(passThroughNode)).toBe(true);
});
@@ -144,14 +142,7 @@ describe("Sankey Layout Functions", () => {
];
const passThroughNodes = [
{
id: "pt1",
depth: 1,
passThrough: true,
value: 5,
sourceId: "node1",
targetId: "node2",
},
{ id: "pt1", depth: 1, passThrough: true, value: 5 },
];
const result = groupNodesBySection(
@@ -204,8 +195,6 @@ describe("Sankey Layout Functions", () => {
passThrough: true,
value: 5,
depth: 1,
sourceId: "source",
targetId: "target",
};
const result = createSectionNodes([passThroughNode]);
@@ -327,15 +316,13 @@ describe("Sankey Layout Functions", () => {
describe("createPassThroughNode", () => {
it("should create a pass-through node", () => {
const result = createPassThroughNode("source", "target", 2, 15);
const result = createPassThroughNode("source-target", "section1", 2, 15);
expect(result).toEqual({
passThrough: true,
id: "source-target-2",
id: "source-target-section1-2",
value: 15,
depth: 2,
sourceId: "source",
targetId: "target",
});
});
});

533
yarn.lock
View File

@@ -5,10 +5,10 @@ __metadata:
version: 8
cacheKey: 10
"@acemir/cssom@npm:^0.9.23":
version: 0.9.23
resolution: "@acemir/cssom@npm:0.9.23"
checksum: 10/752294359ddf691f402bf871f347c1d5ea8532ead5e197cf60eeb7ab25bdd6f6e98fc7fad74042f2e423bb45fe7e4a049db645e01fa0781a0ed4ea2cec911047
"@acemir/cssom@npm:^0.9.19":
version: 0.9.19
resolution: "@acemir/cssom@npm:0.9.19"
checksum: 10/1c7066c002c170d5a775533cb83ff7eb7415a8cc514995325c3d135803fa4a3feffccf21ba0b520b804cdc5fccef8476d7ef83d0e4876dd7bd007ab64d535588
languageName: node
linkType: hard
@@ -38,7 +38,7 @@ __metadata:
languageName: node
linkType: hard
"@asamuzakjp/dom-selector@npm:^6.7.4":
"@asamuzakjp/dom-selector@npm:^6.7.3":
version: 6.7.4
resolution: "@asamuzakjp/dom-selector@npm:6.7.4"
dependencies:
@@ -1215,15 +1215,15 @@ __metadata:
languageName: node
linkType: hard
"@codemirror/autocomplete@npm:6.20.0":
version: 6.20.0
resolution: "@codemirror/autocomplete@npm:6.20.0"
"@codemirror/autocomplete@npm:6.19.1":
version: 6.19.1
resolution: "@codemirror/autocomplete@npm:6.19.1"
dependencies:
"@codemirror/language": "npm:^6.0.0"
"@codemirror/state": "npm:^6.0.0"
"@codemirror/view": "npm:^6.17.0"
"@lezer/common": "npm:^1.0.0"
checksum: 10/ba3603b860c30dd4f8b7c20085680d2f491022db95fe1f3aa6a58363c64678efb3ba795d715755c8a02121631317cf7fbe44cfa3b4cdb01ebca2b4ed36ea5d8a
checksum: 10/6b5a6f3eb869057696c8e1e601eec97f4c68eebb6ca3890d0aea264b2e7888c555a57c74ae53731d3bc2cc98466be1778cc64b0f7f591ba90b61cd334e7095a8
languageName: node
linkType: hard
@@ -1282,15 +1282,15 @@ __metadata:
languageName: node
linkType: hard
"@codemirror/view@npm:6.38.8, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0":
version: 6.38.8
resolution: "@codemirror/view@npm:6.38.8"
"@codemirror/view@npm:6.38.6, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0":
version: 6.38.6
resolution: "@codemirror/view@npm:6.38.6"
dependencies:
"@codemirror/state": "npm:^6.5.0"
crelt: "npm:^1.0.6"
style-mod: "npm:^4.1.0"
w3c-keyname: "npm:^2.2.4"
checksum: 10/81b1508015a378e4719d0239254173f0c5cd340c2abf96eb488fe5fb474bdb37ec1f010b9890ced774accd7aeb9443e7337cb6a89544b954273e5ddabece7cea
checksum: 10/5a047337a98de111817ce8c8d39e6429c90ca0b0a4d2678d6e161e9e5961b1d476a891f447ab7a05cac395d4a93530e7c68bedd93191285265f0742a308ad00b
languageName: node
linkType: hard
@@ -1940,9 +1940,9 @@ __metadata:
languageName: node
linkType: hard
"@home-assistant/webawesome@npm:3.0.0-ha.0":
version: 3.0.0-ha.0
resolution: "@home-assistant/webawesome@npm:3.0.0-ha.0"
"@home-assistant/webawesome@npm:3.0.0":
version: 3.0.0
resolution: "@home-assistant/webawesome@npm:3.0.0"
dependencies:
"@ctrl/tinycolor": "npm:4.1.0"
"@floating-ui/dom": "npm:^1.6.13"
@@ -1953,7 +1953,7 @@ __metadata:
lit: "npm:^3.2.1"
nanoid: "npm:^5.1.5"
qr-creator: "npm:^1.0.0"
checksum: 10/2034d498d5b26bb0573ebc2c9aadd144604bb48c04becbae0c67b16857d8e5d6562626e795974362c3fc41e9b593a9005595d8b5ff434b1569b2d724af13043b
checksum: 10/03400894cfee8548fd5b1f5c56d31d253830e704b18ba69d36ce6b761d8b1bef2fb52cffba8d9b033033bb582f2f51a2d6444d82622f66d70150e2104fcb49e2
languageName: node
linkType: hard
@@ -2365,10 +2365,10 @@ __metadata:
languageName: node
linkType: hard
"@lokalise/node-api@npm:15.4.0":
version: 15.4.0
resolution: "@lokalise/node-api@npm:15.4.0"
checksum: 10/fe7e36bb137310244079fba9978a10fdf65ca6566e075e8e25ed0fd461e7168649ca43929a0a3a0eaf2df72055996ef4d8a72302e7b502863feb9f9a6471aff1
"@lokalise/node-api@npm:15.3.1":
version: 15.3.1
resolution: "@lokalise/node-api@npm:15.3.1"
checksum: 10/9175559660cfbde3f6451ee0ade96ca5ccf6686f3a8f07a23ae6abf3a58db5b5dc71683cdb7f19252765250df7b77dc67539a80e24c3b44a1a97bb2f2d9cd090
languageName: node
linkType: hard
@@ -4520,10 +4520,10 @@ __metadata:
languageName: node
linkType: hard
"@types/emscripten@npm:^1.41.5":
version: 1.41.5
resolution: "@types/emscripten@npm:1.41.5"
checksum: 10/60dfcced39b2d0c94f31907c5cf5d97fe06ed35ed6cb134650a1a96bae861302722d4a6f459b484ea09588cde70cbcc034974454a746be3ce9688b52398d2953
"@types/emscripten@npm:^1.41.2":
version: 1.41.2
resolution: "@types/emscripten@npm:1.41.2"
checksum: 10/95bc5acddaab2155a1fbe0c816717a2552b8d4df484d4bb8b6962b9f2e65c7a8d81872d6e80d706384608e9ea0cde07f852b3069e12921c2b445c6fb48bd985b
languageName: node
linkType: hard
@@ -4945,106 +4945,106 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/eslint-plugin@npm:8.47.0":
version: 8.47.0
resolution: "@typescript-eslint/eslint-plugin@npm:8.47.0"
"@typescript-eslint/eslint-plugin@npm:8.46.3":
version: 8.46.3
resolution: "@typescript-eslint/eslint-plugin@npm:8.46.3"
dependencies:
"@eslint-community/regexpp": "npm:^4.10.0"
"@typescript-eslint/scope-manager": "npm:8.47.0"
"@typescript-eslint/type-utils": "npm:8.47.0"
"@typescript-eslint/utils": "npm:8.47.0"
"@typescript-eslint/visitor-keys": "npm:8.47.0"
"@typescript-eslint/scope-manager": "npm:8.46.3"
"@typescript-eslint/type-utils": "npm:8.46.3"
"@typescript-eslint/utils": "npm:8.46.3"
"@typescript-eslint/visitor-keys": "npm:8.46.3"
graphemer: "npm:^1.4.0"
ignore: "npm:^7.0.0"
natural-compare: "npm:^1.4.0"
ts-api-utils: "npm:^2.1.0"
peerDependencies:
"@typescript-eslint/parser": ^8.47.0
"@typescript-eslint/parser": ^8.46.3
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/53d86116a39429c0cdde5969f9ea2cf712f7c7cb2ed023088a876686a4771df131dbefda7645ba0724e2a5a0532e14bdffcb92c708060a46a8607dc5243083d1
checksum: 10/0c1eb81a43f1d04fdd79c4e59f9f0687b86735ae6c98d94fe5eb021da2f83e0e2426a2922fe94296fb0a9ab131d53fe4cde8b54d0948d7b23e01e648a318bd1c
languageName: node
linkType: hard
"@typescript-eslint/parser@npm:8.47.0":
version: 8.47.0
resolution: "@typescript-eslint/parser@npm:8.47.0"
"@typescript-eslint/parser@npm:8.46.3":
version: 8.46.3
resolution: "@typescript-eslint/parser@npm:8.46.3"
dependencies:
"@typescript-eslint/scope-manager": "npm:8.47.0"
"@typescript-eslint/types": "npm:8.47.0"
"@typescript-eslint/typescript-estree": "npm:8.47.0"
"@typescript-eslint/visitor-keys": "npm:8.47.0"
"@typescript-eslint/scope-manager": "npm:8.46.3"
"@typescript-eslint/types": "npm:8.46.3"
"@typescript-eslint/typescript-estree": "npm:8.46.3"
"@typescript-eslint/visitor-keys": "npm:8.46.3"
debug: "npm:^4.3.4"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/e7213296a27b78511b8c9b2627ff6530d0eb31e6b076eef6f34f11ca7fbcb7e998a2fa079bfc1563a53f9d88326aa4af995241bcdf08a15c3e5be0d13fdff2d7
checksum: 10/d36edeba9ce37d219115fb101a4496bca2685969b217d0f64c0c255867a8793a8b41a95b86e26775a09b3abbb7c5b93ef712ea9a0fba3d055dcf385b17825075
languageName: node
linkType: hard
"@typescript-eslint/project-service@npm:8.47.0":
version: 8.47.0
resolution: "@typescript-eslint/project-service@npm:8.47.0"
"@typescript-eslint/project-service@npm:8.46.3":
version: 8.46.3
resolution: "@typescript-eslint/project-service@npm:8.46.3"
dependencies:
"@typescript-eslint/tsconfig-utils": "npm:^8.47.0"
"@typescript-eslint/types": "npm:^8.47.0"
"@typescript-eslint/tsconfig-utils": "npm:^8.46.3"
"@typescript-eslint/types": "npm:^8.46.3"
debug: "npm:^4.3.4"
peerDependencies:
typescript: ">=4.8.4 <6.0.0"
checksum: 10/e2f935dae66ce27e6c0cce8b750da0e8fe84b6e0fa248bf8210b84eec3c4d2e2679a878185f445ce507d132215a676dcf8a21d47ab70c547da47ede000a128e1
checksum: 10/2f041dfc664209b6a213cf585df28d0913ddf81916b83119c897a10dd9ad20dcd0ee3c523ee95440f498da6ba9d6e50cf08852418c0a2ebddd92c7a7cd295736
languageName: node
linkType: hard
"@typescript-eslint/scope-manager@npm:8.47.0":
version: 8.47.0
resolution: "@typescript-eslint/scope-manager@npm:8.47.0"
"@typescript-eslint/scope-manager@npm:8.46.3":
version: 8.46.3
resolution: "@typescript-eslint/scope-manager@npm:8.46.3"
dependencies:
"@typescript-eslint/types": "npm:8.47.0"
"@typescript-eslint/visitor-keys": "npm:8.47.0"
checksum: 10/e97ae0f746f6bb5706181a973bcc0c1268706ef7e8c18594b37168bb0b41b1673d3f0ba1a2575ee3bd121066500fdc75af313f6ad283198942a5cdb65ade7621
"@typescript-eslint/types": "npm:8.46.3"
"@typescript-eslint/visitor-keys": "npm:8.46.3"
checksum: 10/6bb6c3210bfcca59cf60860b51bfae8d28b01d074a8608b6f24b3290952ff74103e08d390d11cbf613812fca04aa55ad14ad9da04c3041e23acdca235ab1ff78
languageName: node
linkType: hard
"@typescript-eslint/tsconfig-utils@npm:8.47.0, @typescript-eslint/tsconfig-utils@npm:^8.47.0":
version: 8.47.0
resolution: "@typescript-eslint/tsconfig-utils@npm:8.47.0"
"@typescript-eslint/tsconfig-utils@npm:8.46.3, @typescript-eslint/tsconfig-utils@npm:^8.46.3":
version: 8.46.3
resolution: "@typescript-eslint/tsconfig-utils@npm:8.46.3"
peerDependencies:
typescript: ">=4.8.4 <6.0.0"
checksum: 10/7f44441da3778928937419f8ebc62939538cf30087e56c0ca56f599ce98111b82f496902a9e15d713822b9cd14b17937d57b722468450a48748f8e50fd7161af
checksum: 10/e7a16eadf79483d4b61dee56a08d032bafe26d44d634e7863a5875dbb44393570896641272a4e9810f4eac76a4109f59ad667b036d7627ef1647dc672ea19c5e
languageName: node
linkType: hard
"@typescript-eslint/type-utils@npm:8.47.0":
version: 8.47.0
resolution: "@typescript-eslint/type-utils@npm:8.47.0"
"@typescript-eslint/type-utils@npm:8.46.3":
version: 8.46.3
resolution: "@typescript-eslint/type-utils@npm:8.46.3"
dependencies:
"@typescript-eslint/types": "npm:8.47.0"
"@typescript-eslint/typescript-estree": "npm:8.47.0"
"@typescript-eslint/utils": "npm:8.47.0"
"@typescript-eslint/types": "npm:8.46.3"
"@typescript-eslint/typescript-estree": "npm:8.46.3"
"@typescript-eslint/utils": "npm:8.46.3"
debug: "npm:^4.3.4"
ts-api-utils: "npm:^2.1.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/07dcdd1ac071bbaf87b6b320d107129787a62cc403ce78e081cbe5e2ed0c576d660654e4117e6224c4c23d46919d7130b70801835d2fc41d9344c47ff946ce81
checksum: 10/b29cd001c715033ec9cd5fdf2723915f1b4c6c9342283ed00d20e4b942117625facba9a2cf3914b06633c2af9a167430f8f134323627adb0be85f73da4e89d72
languageName: node
linkType: hard
"@typescript-eslint/types@npm:8.47.0, @typescript-eslint/types@npm:^8.47.0":
version: 8.47.0
resolution: "@typescript-eslint/types@npm:8.47.0"
checksum: 10/fc42416c01c512cfe1533bdf521925bca999adc68ffefa246e48552783f1fe9d22487d912611c5cb35fca481604aae3cab88279a53ce76c7cd7510b76775c078
"@typescript-eslint/types@npm:8.46.3, @typescript-eslint/types@npm:^8.46.3":
version: 8.46.3
resolution: "@typescript-eslint/types@npm:8.46.3"
checksum: 10/3de35df2ec2f2937c8f6eb262cd49f34500a18d01e0d8da6f348afd621f6c222c41d4ea15203ebbf0bd59814aa2b4c83fde7eb6d4aad1fa1514ee7a742887c6a
languageName: node
linkType: hard
"@typescript-eslint/typescript-estree@npm:8.47.0":
version: 8.47.0
resolution: "@typescript-eslint/typescript-estree@npm:8.47.0"
"@typescript-eslint/typescript-estree@npm:8.46.3":
version: 8.46.3
resolution: "@typescript-eslint/typescript-estree@npm:8.46.3"
dependencies:
"@typescript-eslint/project-service": "npm:8.47.0"
"@typescript-eslint/tsconfig-utils": "npm:8.47.0"
"@typescript-eslint/types": "npm:8.47.0"
"@typescript-eslint/visitor-keys": "npm:8.47.0"
"@typescript-eslint/project-service": "npm:8.46.3"
"@typescript-eslint/tsconfig-utils": "npm:8.46.3"
"@typescript-eslint/types": "npm:8.46.3"
"@typescript-eslint/visitor-keys": "npm:8.46.3"
debug: "npm:^4.3.4"
fast-glob: "npm:^3.3.2"
is-glob: "npm:^4.0.3"
@@ -5053,32 +5053,32 @@ __metadata:
ts-api-utils: "npm:^2.1.0"
peerDependencies:
typescript: ">=4.8.4 <6.0.0"
checksum: 10/a480e83f1fca8a389642cbb18855ef25214c4765694b1d4a74051d2653a4fbbbf3a3cc4e544d1ecb79d49958fbf819246043c0d823d4384aa1c7b5ff79d02fcc
checksum: 10/b55cf72fe3dff0b9bdf9b1793e43fdb2789fa6d706ba7d69fb94801bea82041056a95659bd8fe1e6f026787b2e8d0f8d060149841095a0a82044e3469b8d82cd
languageName: node
linkType: hard
"@typescript-eslint/utils@npm:8.47.0":
version: 8.47.0
resolution: "@typescript-eslint/utils@npm:8.47.0"
"@typescript-eslint/utils@npm:8.46.3":
version: 8.46.3
resolution: "@typescript-eslint/utils@npm:8.46.3"
dependencies:
"@eslint-community/eslint-utils": "npm:^4.7.0"
"@typescript-eslint/scope-manager": "npm:8.47.0"
"@typescript-eslint/types": "npm:8.47.0"
"@typescript-eslint/typescript-estree": "npm:8.47.0"
"@typescript-eslint/scope-manager": "npm:8.46.3"
"@typescript-eslint/types": "npm:8.46.3"
"@typescript-eslint/typescript-estree": "npm:8.46.3"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/e165bbcaaafb88761f12272bc4b3be1631d8a8ea319765c80cfe5bf7a5858f437486eeae177643baa213570a664f0254b41bf0541e9238b57080bb30d1a2c8ab
checksum: 10/369c962bc20a2a6022ef4533ad55ab4e3d2403e7e200505b29fae6f0b8fc99be8fe149d929781f5ead0d3f88f2c74904f60aaa3771e6773e2b7dd8f61f07a534
languageName: node
linkType: hard
"@typescript-eslint/visitor-keys@npm:8.47.0":
version: 8.47.0
resolution: "@typescript-eslint/visitor-keys@npm:8.47.0"
"@typescript-eslint/visitor-keys@npm:8.46.3":
version: 8.46.3
resolution: "@typescript-eslint/visitor-keys@npm:8.46.3"
dependencies:
"@typescript-eslint/types": "npm:8.47.0"
"@typescript-eslint/types": "npm:8.46.3"
eslint-visitor-keys: "npm:^4.2.1"
checksum: 10/1e184cdebc4ab15da8a46ae2624ba4543c6bea83ced80a1602da99b72c00b5f6ea913ae021823c555a35a65bb9a9df09d119713998c44b00eba25e1407844294
checksum: 10/02659a4cc4780d677907ed7e356e18b941e0ed18883acfda0d74d3e388144f90aa098b8fcdc2f4c01e9e6b60ac6154d1afb009feb6169c483260a5c8b4891171
languageName: node
linkType: hard
@@ -5368,12 +5368,12 @@ __metadata:
languageName: node
linkType: hard
"@vitest/coverage-v8@npm:4.0.13":
version: 4.0.13
resolution: "@vitest/coverage-v8@npm:4.0.13"
"@vitest/coverage-v8@npm:4.0.8":
version: 4.0.8
resolution: "@vitest/coverage-v8@npm:4.0.8"
dependencies:
"@bcoe/v8-coverage": "npm:^1.0.2"
"@vitest/utils": "npm:4.0.13"
"@vitest/utils": "npm:4.0.8"
ast-v8-to-istanbul: "npm:^0.3.8"
debug: "npm:^4.4.3"
istanbul-lib-coverage: "npm:^3.2.2"
@@ -5384,34 +5384,34 @@ __metadata:
std-env: "npm:^3.10.0"
tinyrainbow: "npm:^3.0.3"
peerDependencies:
"@vitest/browser": 4.0.13
vitest: 4.0.13
"@vitest/browser": 4.0.8
vitest: 4.0.8
peerDependenciesMeta:
"@vitest/browser":
optional: true
checksum: 10/5bbbf72f8da7931c120636b12feb655ae102b4f1ef67481c03fd9a007727fe9d707653896798ff6c477fff3a0f260151ae753e0b69a2493522b5007c84bf9a43
checksum: 10/f97d933c7a776eccbd293fd3f77c0dcdc683e528094ce802b2b309e282c01cca0e8ad51a03b61ec623da7034527879824df2e30f70062d7ca587db40bfa5ffd2
languageName: node
linkType: hard
"@vitest/expect@npm:4.0.13":
version: 4.0.13
resolution: "@vitest/expect@npm:4.0.13"
"@vitest/expect@npm:4.0.8":
version: 4.0.8
resolution: "@vitest/expect@npm:4.0.8"
dependencies:
"@standard-schema/spec": "npm:^1.0.0"
"@types/chai": "npm:^5.2.2"
"@vitest/spy": "npm:4.0.13"
"@vitest/utils": "npm:4.0.13"
chai: "npm:^6.2.1"
"@vitest/spy": "npm:4.0.8"
"@vitest/utils": "npm:4.0.8"
chai: "npm:^6.2.0"
tinyrainbow: "npm:^3.0.3"
checksum: 10/13c0e66e4eeaf56bbf1c57aeabe271a186966de97bd273266910aa45391479167cf3749f18b404035bde5f1822ce4a370b1a1e9101c8f8ef658c885b687b4e50
checksum: 10/342934870fb2b11b7a47db4df2a9df2b711087fe48118568ac013386cd659b9bff8f8252bef643e9519a88d018d54e6758a733c9dedf907e9d5dc53040aa0dc3
languageName: node
linkType: hard
"@vitest/mocker@npm:4.0.13":
version: 4.0.13
resolution: "@vitest/mocker@npm:4.0.13"
"@vitest/mocker@npm:4.0.8":
version: 4.0.8
resolution: "@vitest/mocker@npm:4.0.8"
dependencies:
"@vitest/spy": "npm:4.0.13"
"@vitest/spy": "npm:4.0.8"
estree-walker: "npm:^3.0.3"
magic-string: "npm:^0.30.21"
peerDependencies:
@@ -5422,54 +5422,54 @@ __metadata:
optional: true
vite:
optional: true
checksum: 10/339f86523bfca84fc9b7efa10f0b4362078c4d5137556d026cc4c163bb13f878412769bbbffeb7772c27c8360bd75a50a8acf7e9fc8b237236dffed4a07f95b7
checksum: 10/6a624e04a6fa78cc45205961bd0638486674d1cbf4589599772fdd49f58f433c821bcb1eea013ac772171fb9c81154e6f9ffcf1704f27e245860e982c3988cd4
languageName: node
linkType: hard
"@vitest/pretty-format@npm:4.0.13":
version: 4.0.13
resolution: "@vitest/pretty-format@npm:4.0.13"
"@vitest/pretty-format@npm:4.0.8":
version: 4.0.8
resolution: "@vitest/pretty-format@npm:4.0.8"
dependencies:
tinyrainbow: "npm:^3.0.3"
checksum: 10/734c6a4d1f7c567bbff7a7855aa90360a46d4f0c221db66d9d63ff7a690a4fd89e36e9eb8db9acf5647bcab9eb3c6625e8d06cc7e1b46321bf7d514375524c8d
checksum: 10/7e438ba6875a72b58cfe429dedc1de3025c8f87f523db24c687b2ad298e0c1a3e171e7e22ab938518a52c44383acff58d3e9936620dc45f4e97f1669e3e275da
languageName: node
linkType: hard
"@vitest/runner@npm:4.0.13":
version: 4.0.13
resolution: "@vitest/runner@npm:4.0.13"
"@vitest/runner@npm:4.0.8":
version: 4.0.8
resolution: "@vitest/runner@npm:4.0.8"
dependencies:
"@vitest/utils": "npm:4.0.13"
"@vitest/utils": "npm:4.0.8"
pathe: "npm:^2.0.3"
checksum: 10/f6d92620653b18ff7e897c7e9e74d52603a4ffecb5bedde28f2ad763584728a2581de75b19de0da896fac5f68b5e5c1dce3f4fc71fc616c31e90a9823dcf6ebf
checksum: 10/cb66c1121c2701bb2400fb0969b7504aee34b400278d03f1ed19d78f8180adb88dbbd0f3f5d4ff1db49a5ae50f0e139964ff7ae32aa1d99f6e9d91d6a57c1ffe
languageName: node
linkType: hard
"@vitest/snapshot@npm:4.0.13":
version: 4.0.13
resolution: "@vitest/snapshot@npm:4.0.13"
"@vitest/snapshot@npm:4.0.8":
version: 4.0.8
resolution: "@vitest/snapshot@npm:4.0.8"
dependencies:
"@vitest/pretty-format": "npm:4.0.13"
"@vitest/pretty-format": "npm:4.0.8"
magic-string: "npm:^0.30.21"
pathe: "npm:^2.0.3"
checksum: 10/1d7337389432cb02ab42650d32e4c09af2ac45cfb5880c85fca089c901d236252b71e849bc45a992ad371834e84308f2b723c741ea090f50675a1509a1d29d7c
checksum: 10/d703a4bb4979f94cc9b3c8ecaf5ce9c4741066772f1d4414db7faa1d1ed209ad5da9c85a98118e340f5f5652d680a1895efa39c060b6e3700a95806eefdf40a7
languageName: node
linkType: hard
"@vitest/spy@npm:4.0.13":
version: 4.0.13
resolution: "@vitest/spy@npm:4.0.13"
checksum: 10/55fce93d47b0812726a213ca96265661a3b64267a392ff5d6fc8270f57492653e1bfab62c0fd0c5788b859d91219b6b43ad932d7921bdc80b47da3cf6dea9dd9
"@vitest/spy@npm:4.0.8":
version: 4.0.8
resolution: "@vitest/spy@npm:4.0.8"
checksum: 10/944223ffef7d64299d92c94ab895209b27a307ef59d2ef6f5c6c006fc1e85612c9547069b0fde7b2d93adfa484b3770b459a716f6b82f8839226132767fb661c
languageName: node
linkType: hard
"@vitest/utils@npm:4.0.13":
version: 4.0.13
resolution: "@vitest/utils@npm:4.0.13"
"@vitest/utils@npm:4.0.8":
version: 4.0.8
resolution: "@vitest/utils@npm:4.0.8"
dependencies:
"@vitest/pretty-format": "npm:4.0.13"
"@vitest/pretty-format": "npm:4.0.8"
tinyrainbow: "npm:^3.0.3"
checksum: 10/0b0a5d4cc23a4b1cb481f1b31e51fe0f8021bc67478b494f08490f6690d866dedc026275850ce3104c34462ee7b415afb35a3692ced642dfa8339890fa88649e
checksum: 10/9f241a8aafbd81caec766143c8c99b8e1e76671460ff6bd1fc0921ea968c2f0d3d8305551b52509c0fb427d436ec9e2f75cafdc273d2f23c1ccaea504269a160
languageName: node
linkType: hard
@@ -6108,12 +6108,12 @@ __metadata:
languageName: node
linkType: hard
"barcode-detector@npm:3.0.8":
version: 3.0.8
resolution: "barcode-detector@npm:3.0.8"
"barcode-detector@npm:3.0.6":
version: 3.0.6
resolution: "barcode-detector@npm:3.0.6"
dependencies:
zxing-wasm: "npm:2.2.4"
checksum: 10/7de6225f659c69a0f4101d080a9e0812f2404c485fa2406424b8b13eaff274f6e7405c94de24827a09def52c32a63b19b9c9fba61c5274b074d558f696ec1684
zxing-wasm: "npm:2.2.2"
checksum: 10/c1d4fbeecbb0be8f91e15c2bfdaaf3b27e3fd6fe224e60c4c25793a3bf90bab844702956de2f596a8e3229d2b4e0d88b42fb629472efc0b36a3751c3b01b7151
languageName: node
linkType: hard
@@ -6479,10 +6479,10 @@ __metadata:
languageName: node
linkType: hard
"chai@npm:^6.2.1":
version: 6.2.1
resolution: "chai@npm:6.2.1"
checksum: 10/f7917749e2468bd3a17ee4769b680e440002960c1294dd11c6d3ad102b5db9ea1a43e3ad9462b7b0f1502e5c845a6e39ce63db9de1def782e44652018c48acb7
"chai@npm:^6.2.0":
version: 6.2.0
resolution: "chai@npm:6.2.0"
checksum: 10/199422854e253d8711ea3f220365c6a850c450abf68b31131d2a0f703cbfc5cb48e6c81567e0adbe80e83cdcae6dba82d069a41a77c16bdf6703329c5c3447ef
languageName: node
linkType: hard
@@ -6699,10 +6699,10 @@ __metadata:
languageName: node
linkType: hard
"color-name@npm:2.1.0":
version: 2.1.0
resolution: "color-name@npm:2.1.0"
checksum: 10/eb014f71d87408e318e95d3f554f188370d354ba8e0ffa4341d0fd19de391bfe2bc96e563d4f6614644d676bc24f475560dffee3fe310c2d6865d007410a9a2b
"color-name@npm:2.0.2":
version: 2.0.2
resolution: "color-name@npm:2.0.2"
checksum: 10/58e5fa3853a0dac813179e75a1fe07ff362abacb9fd456fcaae702b74d4ed5f6de2cbaee07ff2660f3495c7a6ceabc4ef0420165db0018e7150a6d4045f6539e
languageName: node
linkType: hard
@@ -6743,10 +6743,10 @@ __metadata:
languageName: node
linkType: hard
"commander@npm:^14.0.2":
version: 14.0.2
resolution: "commander@npm:14.0.2"
checksum: 10/2d202db5e5f9bb770112a3c1579b893d17ac6f6d932183077308bdd96d0f87f0bbe6a68b5b9ed2cf3b2514be6bb7de637480703c0e2db9741ee1b383237deb26
"commander@npm:^14.0.1":
version: 14.0.1
resolution: "commander@npm:14.0.1"
checksum: 10/783115e9403caeca29c0fcbd4e0358f70c67760e4e4933f3453fcdd5ddba2ec44173c8da5213d7ce5e404f51c7e71203a42c548164dbe27b668b32a8981577f1
languageName: node
linkType: hard
@@ -6902,10 +6902,10 @@ __metadata:
languageName: node
linkType: hard
"core-js@npm:3.47.0":
version: 3.47.0
resolution: "core-js@npm:3.47.0"
checksum: 10/c02dc6a091c7e6799e3527dc06a428c44bbcff7f8f6ee700ff818b90aa2ebaf1f17b0234146e692811da97cda5a39a6095ecadec9fd1a74b1135103eb0e96cb1
"core-js@npm:3.46.0":
version: 3.46.0
resolution: "core-js@npm:3.46.0"
checksum: 10/82993ca487c6cbbf8bbf00e45eeb9705eb63dc2f9c90d7f35696733efbc3f4b52426e1f8dbef0f0b68ea16caa21e4f44cc5490e08120e1cad4a72b031ed8adaa
languageName: node
linkType: hard
@@ -6975,14 +6975,14 @@ __metadata:
languageName: node
linkType: hard
"cssstyle@npm:^5.3.3":
version: 5.3.3
resolution: "cssstyle@npm:5.3.3"
"cssstyle@npm:^5.3.2":
version: 5.3.2
resolution: "cssstyle@npm:5.3.2"
dependencies:
"@asamuzakjp/css-color": "npm:^4.0.3"
"@csstools/css-syntax-patches-for-csstree": "npm:^1.0.14"
css-tree: "npm:^3.1.0"
checksum: 10/8c6133761395f03d50e74ff4c05473b4835b79013efed3f4b79dc452d94a7122082887d89c3c1164bba0e8919be209b75e4f4706197c086fc9f4b50182ab03d6
checksum: 10/2f628254c738556c6394dbe705d7cadd8a8867767feb00bc8a4980a43b405fc891f72687cc2f04a8b15185009e9713f0f96bc9311e7781d848b0d145056b9f1f
languageName: node
linkType: hard
@@ -9184,10 +9184,10 @@ __metadata:
languageName: node
linkType: hard
"hls.js@npm:1.6.15":
version: 1.6.15
resolution: "hls.js@npm:1.6.15"
checksum: 10/c385f8b5ed541d658d2c7f8616d6aef6064855f6d70ad4e1d0e9696d35820394e4d170e53891530098d648108ce68af948d726a4fad73c99fda274c9e68aa49c
"hls.js@npm:1.6.14":
version: 1.6.14
resolution: "hls.js@npm:1.6.14"
checksum: 10/56eedf163912abf72bd5ca0eac44dbaa442cff59bb8d1ff3303614bf18d21dfa3ec7b85db5b71449986c52c63b7efb6bd5392b048bf22c61549f71a001fa94cd
languageName: node
linkType: hard
@@ -9202,13 +9202,13 @@ __metadata:
"@babel/runtime": "npm:7.28.4"
"@braintree/sanitize-url": "npm:7.1.1"
"@bundle-stats/plugin-webpack-filter": "npm:4.21.6"
"@codemirror/autocomplete": "npm:6.20.0"
"@codemirror/autocomplete": "npm:6.19.1"
"@codemirror/commands": "npm:6.10.0"
"@codemirror/language": "npm:6.11.3"
"@codemirror/legacy-modes": "npm:6.5.2"
"@codemirror/search": "npm:6.5.11"
"@codemirror/state": "npm:6.5.2"
"@codemirror/view": "npm:6.38.8"
"@codemirror/view": "npm:6.38.6"
"@date-fns/tz": "npm:1.4.1"
"@egjs/hammerjs": "npm:2.0.17"
"@formatjs/intl-datetimeformat": "npm:6.18.2"
@@ -9226,14 +9226,14 @@ __metadata:
"@fullcalendar/list": "npm:6.1.19"
"@fullcalendar/luxon3": "npm:6.1.19"
"@fullcalendar/timegrid": "npm:6.1.19"
"@home-assistant/webawesome": "npm:3.0.0-ha.0"
"@home-assistant/webawesome": "npm:3.0.0"
"@lezer/highlight": "npm:1.2.3"
"@lit-labs/motion": "npm:1.0.9"
"@lit-labs/observers": "npm:2.0.6"
"@lit-labs/virtualizer": "npm:2.1.1"
"@lit/context": "npm:1.1.6"
"@lit/reactive-element": "npm:2.1.1"
"@lokalise/node-api": "npm:15.4.0"
"@lokalise/node-api": "npm:15.3.1"
"@material/chips": "npm:=14.0.0-canary.53b3cad2f.0"
"@material/data-table": "npm:=14.0.0-canary.53b3cad2f.0"
"@material/mwc-base": "npm:0.27.0"
@@ -9291,18 +9291,18 @@ __metadata:
"@vaadin/combo-box": "npm:24.9.5"
"@vaadin/vaadin-themable-mixin": "npm:24.9.5"
"@vibrant/color": "npm:4.0.0"
"@vitest/coverage-v8": "npm:4.0.13"
"@vitest/coverage-v8": "npm:4.0.8"
"@vue/web-component-wrapper": "npm:1.3.0"
"@webcomponents/scoped-custom-element-registry": "npm:0.0.10"
"@webcomponents/webcomponentsjs": "npm:2.8.0"
app-datepicker: "npm:5.1.1"
babel-loader: "npm:10.0.0"
babel-plugin-template-html-minifier: "npm:4.1.0"
barcode-detector: "npm:3.0.8"
barcode-detector: "npm:3.0.6"
browserslist-useragent-regexp: "npm:4.1.3"
color-name: "npm:2.1.0"
color-name: "npm:2.0.2"
comlink: "npm:4.4.2"
core-js: "npm:3.47.0"
core-js: "npm:3.46.0"
cropperjs: "npm:1.6.2"
culori: "npm:4.0.2"
date-fns: "npm:4.1.0"
@@ -9331,19 +9331,19 @@ __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.15"
hls.js: "npm:1.6.14"
home-assistant-js-websocket: "npm:9.6.0"
html-minifier-terser: "npm:7.2.0"
husky: "npm:9.1.7"
idb-keyval: "npm:6.2.2"
intl-messageformat: "npm:10.7.18"
js-yaml: "npm:4.1.1"
jsdom: "npm:27.2.0"
jsdom: "npm:27.1.0"
jszip: "npm:3.10.1"
leaflet: "npm:1.9.4"
leaflet-draw: "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
leaflet.markercluster: "npm:1.5.3"
lint-staged: "npm:16.2.7"
lint-staged: "npm:16.2.6"
lit: "npm:3.3.1"
lit-analyzer: "npm:2.0.3"
lit-html: "npm:3.3.1"
@@ -9351,7 +9351,7 @@ __metadata:
lodash.template: "npm:4.5.0"
luxon: "npm:3.7.2"
map-stream: "npm:0.0.7"
marked: "npm:17.0.1"
marked: "npm:17.0.0"
memoize-one: "npm:6.0.0"
node-vibrant: "npm:4.0.3"
object-hash: "npm:3.0.0"
@@ -9362,7 +9362,7 @@ __metadata:
qrcode: "npm:1.5.4"
roboto-fontface: "npm:0.10.0"
rrule: "npm:2.8.1"
rspack-manifest-plugin: "npm:5.2.0"
rspack-manifest-plugin: "npm:5.1.0"
serve: "npm:14.2.5"
sinon: "npm:21.0.0"
sortablejs: "patch:sortablejs@npm%3A1.15.6#~/.yarn/patches/sortablejs-npm-1.15.6-3235a8f83b.patch"
@@ -9373,22 +9373,22 @@ __metadata:
tinykeys: "npm:3.0.0"
ts-lit-plugin: "npm:2.0.2"
typescript: "npm:5.9.3"
typescript-eslint: "npm:8.47.0"
typescript-eslint: "npm:8.46.3"
ua-parser-js: "npm:2.0.6"
vite-tsconfig-paths: "npm:5.1.4"
vitest: "npm:4.0.13"
vitest: "npm:4.0.8"
vue: "npm:2.7.16"
vue2-daterange-picker: "npm:0.6.8"
webpack-stats-plugin: "npm:1.1.3"
webpackbar: "npm:7.0.0"
weekstart: "npm:2.0.0"
workbox-build: "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
workbox-cacheable-response: "npm:7.4.0"
workbox-core: "npm:7.4.0"
workbox-expiration: "npm:7.4.0"
workbox-precaching: "npm:7.4.0"
workbox-routing: "npm:7.4.0"
workbox-strategies: "npm:7.4.0"
workbox-cacheable-response: "npm:7.3.0"
workbox-core: "npm:7.3.0"
workbox-expiration: "npm:7.3.0"
workbox-precaching: "npm:7.3.0"
workbox-routing: "npm:7.3.0"
workbox-strategies: "npm:7.3.0"
xss: "npm:1.0.15"
languageName: unknown
linkType: soft
@@ -10418,13 +10418,13 @@ __metadata:
languageName: node
linkType: hard
"jsdom@npm:27.2.0":
version: 27.2.0
resolution: "jsdom@npm:27.2.0"
"jsdom@npm:27.1.0":
version: 27.1.0
resolution: "jsdom@npm:27.1.0"
dependencies:
"@acemir/cssom": "npm:^0.9.23"
"@asamuzakjp/dom-selector": "npm:^6.7.4"
cssstyle: "npm:^5.3.3"
"@acemir/cssom": "npm:^0.9.19"
"@asamuzakjp/dom-selector": "npm:^6.7.3"
cssstyle: "npm:^5.3.2"
data-urls: "npm:^6.0.0"
decimal.js: "npm:^10.6.0"
html-encoding-sniffer: "npm:^4.0.0"
@@ -10447,7 +10447,7 @@ __metadata:
peerDependenciesMeta:
canvas:
optional: true
checksum: 10/78458e0230fd18ceef1e8753ab44eadf436654540a3d1dc5068f49b87c2bd1752b8fff25c9e1f30ee406ae3e80cc3f81668f778b8220a7bf9386a0a826dee088
checksum: 10/9e2bd9824abed594da64fc58f1bdb817c7f82ba3025cef4817e6709807f886ac71b7b6a0f4f69ba604e9137bbcd6b7246b98dec53bde95500f5ac4b39a8ebb00
languageName: node
linkType: hard
@@ -10690,11 +10690,11 @@ __metadata:
languageName: node
linkType: hard
"lint-staged@npm:16.2.7":
version: 16.2.7
resolution: "lint-staged@npm:16.2.7"
"lint-staged@npm:16.2.6":
version: 16.2.6
resolution: "lint-staged@npm:16.2.6"
dependencies:
commander: "npm:^14.0.2"
commander: "npm:^14.0.1"
listr2: "npm:^9.0.5"
micromatch: "npm:^4.0.8"
nano-spawn: "npm:^2.0.0"
@@ -10703,7 +10703,7 @@ __metadata:
yaml: "npm:^2.8.1"
bin:
lint-staged: bin/lint-staged.js
checksum: 10/c1fd7685300800ea6d3f073cb450f9e3d2a83966e7c6785ea9608a08e77e1e8e5f1958f77b98ccd7d423daa53bb36016d6fd96a98d22234d0f7f56d7b3f360f2
checksum: 10/c419f1347166ddd06746d4a3e4ce6441f3d0e82dcdcbc3d3615ddf7b82b36c603df2e43dbb1edad5ed00ae29857479e1f880bcca271f8bf4b0db7d0f77861d21
languageName: node
linkType: hard
@@ -10984,12 +10984,12 @@ __metadata:
languageName: node
linkType: hard
"marked@npm:17.0.1":
version: 17.0.1
resolution: "marked@npm:17.0.1"
"marked@npm:17.0.0":
version: 17.0.0
resolution: "marked@npm:17.0.0"
bin:
marked: bin/marked.js
checksum: 10/8d39cf5a1eb97288d09d47dba57a613b8c45a212287b27aa16b4bc41baaebb147340c359d379da1bc6d351474d169aa3a63f981770b67f7663ed0a479dd950c2
checksum: 10/5544d27547851986c4e994a3f5739ea30bfe2616a9e1d5d5c8ced0fd561b5e971b3c7ee62b4fea1ea530e9886b89102d5c3b3bf962756494ced021f1accd6854
languageName: node
linkType: hard
@@ -12780,9 +12780,9 @@ __metadata:
languageName: node
linkType: hard
"rspack-manifest-plugin@npm:5.2.0":
version: 5.2.0
resolution: "rspack-manifest-plugin@npm:5.2.0"
"rspack-manifest-plugin@npm:5.1.0":
version: 5.1.0
resolution: "rspack-manifest-plugin@npm:5.1.0"
dependencies:
"@rspack/lite-tapable": "npm:^1.0.1"
peerDependencies:
@@ -12790,7 +12790,7 @@ __metadata:
peerDependenciesMeta:
"@rspack/core":
optional: true
checksum: 10/661774e2f3d41db615b2fb2d14800c85524b33f3fd0b67e4161ca775f9ccbba3ecc8db21cd6e1f878e65a13cd6d718c6d66ad55614137cecb615cfd8d8f1cac9
checksum: 10/672de1eb405b48c7a50f03d1813048b1a6711afd1e7f6829471c020bc16de96ecbf8d8a10c3f8cdb6bfcaa6e93461bd22cef92203f17d8494a53e285d21a16ad
languageName: node
linkType: hard
@@ -14223,12 +14223,12 @@ __metadata:
languageName: node
linkType: hard
"type-fest@npm:^5.2.0":
version: 5.2.0
resolution: "type-fest@npm:5.2.0"
"type-fest@npm:^5.0.1":
version: 5.0.1
resolution: "type-fest@npm:5.0.1"
dependencies:
tagged-tag: "npm:^1.0.0"
checksum: 10/4ce526139c05e92a1d92fa905840ff9ae725b8058df5a5571380bca1827db1f2e4e204c9d561fd63e972d45591dd4d38eab5f5529c0af93d6cfb5f99109aa74a
checksum: 10/5ec4def4ce82e6a33cf2e1a50f7ef512226fbe85314e402155aaedd70d4aa7ccea4224a72234d5351b1b4a730b36243d5b011c147e91795d2eee0dba291c6e51
languageName: node
linkType: hard
@@ -14295,18 +14295,18 @@ __metadata:
languageName: node
linkType: hard
"typescript-eslint@npm:8.47.0":
version: 8.47.0
resolution: "typescript-eslint@npm:8.47.0"
"typescript-eslint@npm:8.46.3":
version: 8.46.3
resolution: "typescript-eslint@npm:8.46.3"
dependencies:
"@typescript-eslint/eslint-plugin": "npm:8.47.0"
"@typescript-eslint/parser": "npm:8.47.0"
"@typescript-eslint/typescript-estree": "npm:8.47.0"
"@typescript-eslint/utils": "npm:8.47.0"
"@typescript-eslint/eslint-plugin": "npm:8.46.3"
"@typescript-eslint/parser": "npm:8.46.3"
"@typescript-eslint/typescript-estree": "npm:8.46.3"
"@typescript-eslint/utils": "npm:8.46.3"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/159dad98535dafd68c6228fae4aaf9e02d65d9ac3b02ddf0356b56ce72651dd9860e8bf9e0ee22532d77dd382d7f810e1bf1dd41c3fab381f627a08580c5117e
checksum: 10/2f77eb70c8fd6ec4920d5abf828ef28007df8ff94605246a4ca918fadb996a83f7fb82510a1de69fad7f0159ee8f15246d467ebc42df20a4585919cb6b401715
languageName: node
linkType: hard
@@ -14751,17 +14751,17 @@ __metadata:
languageName: node
linkType: hard
"vitest@npm:4.0.13":
version: 4.0.13
resolution: "vitest@npm:4.0.13"
"vitest@npm:4.0.8":
version: 4.0.8
resolution: "vitest@npm:4.0.8"
dependencies:
"@vitest/expect": "npm:4.0.13"
"@vitest/mocker": "npm:4.0.13"
"@vitest/pretty-format": "npm:4.0.13"
"@vitest/runner": "npm:4.0.13"
"@vitest/snapshot": "npm:4.0.13"
"@vitest/spy": "npm:4.0.13"
"@vitest/utils": "npm:4.0.13"
"@vitest/expect": "npm:4.0.8"
"@vitest/mocker": "npm:4.0.8"
"@vitest/pretty-format": "npm:4.0.8"
"@vitest/runner": "npm:4.0.8"
"@vitest/snapshot": "npm:4.0.8"
"@vitest/spy": "npm:4.0.8"
"@vitest/utils": "npm:4.0.8"
debug: "npm:^4.4.3"
es-module-lexer: "npm:^1.7.0"
expect-type: "npm:^1.2.2"
@@ -14777,20 +14777,17 @@ __metadata:
why-is-node-running: "npm:^2.3.0"
peerDependencies:
"@edge-runtime/vm": "*"
"@opentelemetry/api": ^1.9.0
"@types/debug": ^4.1.12
"@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0
"@vitest/browser-playwright": 4.0.13
"@vitest/browser-preview": 4.0.13
"@vitest/browser-webdriverio": 4.0.13
"@vitest/ui": 4.0.13
"@vitest/browser-playwright": 4.0.8
"@vitest/browser-preview": 4.0.8
"@vitest/browser-webdriverio": 4.0.8
"@vitest/ui": 4.0.8
happy-dom: "*"
jsdom: "*"
peerDependenciesMeta:
"@edge-runtime/vm":
optional: true
"@opentelemetry/api":
optional: true
"@types/debug":
optional: true
"@types/node":
@@ -14809,7 +14806,7 @@ __metadata:
optional: true
bin:
vitest: vitest.mjs
checksum: 10/5842ee10e8509adc225602ec9f186ccbfb6c9733c23ab1587eaaa5406adf626d60d910fcd574223f0d0fe2393eb2af8a97fb3163029becd4c9894d95e4361585
checksum: 10/b2fd9e2bb0740860f998f1ecfb948da330cc2e8dc15376669ef742420ad77fedea491731dfaefff17379457d21f06740afaa725952cbcbdcd499a532b95e5717
languageName: node
linkType: hard
@@ -15378,12 +15375,12 @@ __metadata:
languageName: node
linkType: hard
"workbox-cacheable-response@npm:7.4.0":
version: 7.4.0
resolution: "workbox-cacheable-response@npm:7.4.0"
"workbox-cacheable-response@npm:7.3.0":
version: 7.3.0
resolution: "workbox-cacheable-response@npm:7.3.0"
dependencies:
workbox-core: "npm:7.4.0"
checksum: 10/49bad5d082473d71b8774ec90e7ab53e540369ecb2bb5ad04f146556e78982ae524bf61b209b599601782fffde9a775d33664dba659e629becbad05322c385fe
workbox-core: "npm:7.3.0"
checksum: 10/44cd7bc26e509ca96b1b84e3ff5964296efa645853f114f39789d21c0a214ca5fc047259910b303e220bb4052155cddc5639993fcee076fac496b4895ff17a15
languageName: node
linkType: hard
@@ -15394,10 +15391,10 @@ __metadata:
languageName: node
linkType: hard
"workbox-core@npm:7.4.0":
version: 7.4.0
resolution: "workbox-core@npm:7.4.0"
checksum: 10/160bfbd8957fa59e72b7202caa111e7a099fb405719f7ba2fb489c7dbac0bddfc99fab10122a7b6053821dd89974dc9f9ddb19af574606634d392f89b1d8735f
"workbox-core@npm:7.3.0":
version: 7.3.0
resolution: "workbox-core@npm:7.3.0"
checksum: 10/228fb7018a0568c329e21d47d84980f93ebfef9b1eb3f40ddc3516ca6ae58d51dc7ca4dddc829332775b59a3079e62d105c5e1c5c312805d177b963f8bf54393
languageName: node
linkType: hard
@@ -15411,13 +15408,13 @@ __metadata:
languageName: node
linkType: hard
"workbox-expiration@npm:7.4.0":
version: 7.4.0
resolution: "workbox-expiration@npm:7.4.0"
"workbox-expiration@npm:7.3.0":
version: 7.3.0
resolution: "workbox-expiration@npm:7.3.0"
dependencies:
idb: "npm:^7.0.1"
workbox-core: "npm:7.4.0"
checksum: 10/5c2de6c769d5a5b848f000fbf3723f7eacafa5b5cedcd47a38e99df1ac6b927d19681ca2edc5c50b7802af03acda7499e64b04e25741c2114a2ccd62c2988e03
workbox-core: "npm:7.3.0"
checksum: 10/83e021d700e521a65a89907679d1a580aacc0419428286910ec7c6b0a538326f71f05566434f666ebf6c9fbe819ef3ea81428df1d868f9ea92527afe5d11152d
languageName: node
linkType: hard
@@ -15453,14 +15450,14 @@ __metadata:
languageName: node
linkType: hard
"workbox-precaching@npm:7.4.0":
version: 7.4.0
resolution: "workbox-precaching@npm:7.4.0"
"workbox-precaching@npm:7.3.0":
version: 7.3.0
resolution: "workbox-precaching@npm:7.3.0"
dependencies:
workbox-core: "npm:7.4.0"
workbox-routing: "npm:7.4.0"
workbox-strategies: "npm:7.4.0"
checksum: 10/81bf4f2503603c82e9fa3c1244727fb121a823fc43235aeb2f77df83f90d3bf8fc288e83320f3421f8a7776e174b9e7c2a3294d78025f2696b6ca60d578dc33d
workbox-core: "npm:7.3.0"
workbox-routing: "npm:7.3.0"
workbox-strategies: "npm:7.3.0"
checksum: 10/d14135c471a45de36438c40eed7cb7157cdb336d4216a775486c6307d1ac316794d64231c2e2d0a4c313bb4a4fec623ab77e391cc458b4f2afa64e2487acb2e8
languageName: node
linkType: hard
@@ -15496,12 +15493,12 @@ __metadata:
languageName: node
linkType: hard
"workbox-routing@npm:7.4.0":
version: 7.4.0
resolution: "workbox-routing@npm:7.4.0"
"workbox-routing@npm:7.3.0":
version: 7.3.0
resolution: "workbox-routing@npm:7.3.0"
dependencies:
workbox-core: "npm:7.4.0"
checksum: 10/4adb156d902953b59988a55d8d92c57141c4cecdc042e6157208b5384ed3f9c453e90e57bc819582f8aa56706f01d5a171d4c8a4406bdef1d5db9d757516903e
workbox-core: "npm:7.3.0"
checksum: 10/0d729f9c5cfc5754404ac1f7b729c7740ddc806203792701ac642151fbec939b4aa0fb289eab2295e49180e8154ad9bb1380effb7e0f0362163b79db4291dba7
languageName: node
linkType: hard
@@ -15514,12 +15511,12 @@ __metadata:
languageName: node
linkType: hard
"workbox-strategies@npm:7.4.0":
version: 7.4.0
resolution: "workbox-strategies@npm:7.4.0"
"workbox-strategies@npm:7.3.0":
version: 7.3.0
resolution: "workbox-strategies@npm:7.3.0"
dependencies:
workbox-core: "npm:7.4.0"
checksum: 10/792a302d38e3401dd73e2d64dbb36a47bd7060cd6ea0f33151226ac19ef8a89d2987d3f4f8765e3645113917efcbadc52027887a53e181ad7a817acd6cd08139
workbox-core: "npm:7.3.0"
checksum: 10/61ba672075ef8aaa70ad9221460dab80a7d8920e324e14137460f26ebe8b137e5589fb75c664e0efeaf4402e3d8435a9b1818f9a9c61f88863c0e0315af337e7
languageName: node
linkType: hard
@@ -15806,14 +15803,14 @@ __metadata:
languageName: node
linkType: hard
"zxing-wasm@npm:2.2.4":
version: 2.2.4
resolution: "zxing-wasm@npm:2.2.4"
"zxing-wasm@npm:2.2.2":
version: 2.2.2
resolution: "zxing-wasm@npm:2.2.2"
dependencies:
"@types/emscripten": "npm:^1.41.5"
type-fest: "npm:^5.2.0"
"@types/emscripten": "npm:^1.41.2"
type-fest: "npm:^5.0.1"
peerDependencies:
"@types/emscripten": ">=1.39.6"
checksum: 10/e5928cbb066c854c970cbf724e978e502c3469d69de2469bd37d1f6ab8f5d2a2acdbaa9dea32d35cfc058e2b482e29a9c4f12161d9df3e1e952c30dda3a96d8c
checksum: 10/c542a3a56d30961cd221535fa82fef7fe49abff896339e23236b6a13297e0eba2dc9d51fb0808544321950b51a8f554d57a7859ddb7f8c236cdf66e2509a8b02
languageName: node
linkType: hard