Compare commits

..

1 Commits

Author SHA1 Message Date
Bram Kragten 0725ce2053 Use iOS provided device name for matter, and wait for iOS flow to be done 2026-06-04 16:22:13 +02:00
41 changed files with 621 additions and 1788 deletions
+1 -54
View File
@@ -8,7 +8,6 @@ You are an assistant helping with development of the Home Assistant frontend. Th
- [Quick Reference](#quick-reference)
- [Core Architecture](#core-architecture)
- [State Access: Contexts Instead of `hass`](#state-access-contexts-instead-of-hass)
- [Development Standards](#development-standards)
- [Component Library](#component-library)
- [Common Patterns](#common-patterns)
@@ -53,57 +52,6 @@ The Home Assistant frontend is a modern web application that:
- Communicates with the backend via WebSocket API
- Provides comprehensive theming and internationalization
## State Access: Contexts Instead of `hass`
Every component used to take the whole `hass: HomeAssistant` object — a god-object that re-renders on any unrelated `hass` change, forces tests to mock everything, and hides what a component actually reads. We're moving leaf components to **fine-grained [Lit context](https://lit.dev/docs/data/context/)**: consume only the slice you need and re-render only when it changes.
For new code, consume the matching context instead of adding a `hass` property. `hass` stays for container components that own it and feed the providers; the canonical migration is [`hui-button-card.ts`](src/panels/lovelace/cards/hui-button-card.ts). Infrastructure: contexts in [`src/data/context/index.ts`](src/data/context/index.ts), the `consume…` helpers in [`src/common/decorators/consume-context-entry.ts`](src/common/decorators/consume-context-entry.ts), and `@transform` in [`src/common/decorators/transform.ts`](src/common/decorators/transform.ts). Providers are wired automatically by `contextMixin` on `HassBaseEl` — you only consume.
### Contexts
Consume the narrowest context that covers your reads:
| Context | Replaces |
| ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `statesContext` | `hass.states` |
| `entitiesContext` / `devicesContext` / `areasContext` / `floorsContext` | `hass.entities` / `.devices` / `.areas` / `.floors` (or `registriesContext` for all four) |
| `servicesContext` | `hass.services` |
| `internationalizationContext` | `hass.localize`, `hass.locale`, `hass.language` |
| `formattersContext` | `hass.formatEntityName`, `hass.formatEntityState`, `hass.formatEntityAttributeName`, … |
| `configContext` | `hass.config`, `hass.user`, `hass.auth`, `hass.userData` |
| `connectionContext` | `hass.connection`, `hass.connected`, `hass.hassUrl` |
| `apiContext` | `hass.callService`, `hass.callApi`, `hass.callWS`, `hass.sendWS`, `hass.fetchWithAuth` |
| `uiContext` | `hass.themes`, `hass.selectedTheme`, `hass.panels`, `hass.dockedSidebar`, … |
| `narrowViewportContext` | narrow-layout boolean |
Lazy contexts (subscribe on first consumer, tear down after the last): `labelsContext`, `fullEntitiesContext`, `configEntriesContext`, `manifestsContext`. The single-field contexts (`localizeContext`, `themesContext`, `userContext`, …) are **deprecated** — use the grouped ones above.
### Consuming
Use the `consume…` helpers for entity-scoped and `localize` reads. `entityIdPath` is resolved against `this`, so these watch `this._config.entity`:
```ts
@state() @consumeEntityState({ entityIdPath: ["_config", "entity"] })
private _stateObj?: HassEntity; // consumeEntityStates(...) for a record of several
@state() @consumeEntityRegistryEntry({ entityIdPath: ["_config", "entity"] })
private _entity?: EntityRegistryDisplayEntry;
@state() @consumeLocalize()
private _localize!: LocalizeFunc;
```
For any other single field, pair `@consume` with `@transform`:
```ts
@state()
@consume({ context: uiContext, subscribe: true })
@transform<HomeAssistantUI, Themes>({ transformer: ({ themes }) => themes })
private _themes!: Themes;
```
`@transform`'s `watch` option re-runs the transformer when a host prop changes — needed when an entity id is computed, since `consumeEntityState` only watches the first path segment. To consume a whole group untransformed, drop `@transform` and type it `ContextType<typeof statesContext>`.
## Development Standards
### Code Quality Requirements
@@ -188,7 +136,6 @@ export class HaMyComponent extends LitElement {
### Data Management
- **Use WebSocket API**: All backend communication via home-assistant-js-websocket
- **Prefer contexts over `hass`**: For state reads, consume the relevant Lit context instead of taking the whole `hass` object — see [State Access: Contexts Instead of `hass`](#state-access-contexts-instead-of-hass)
- **Cache appropriately**: Use collections and caching for frequently accessed data
- **Handle errors gracefully**: All API calls should have error handling
- **Update real-time**: Subscribe to state changes for live updates
@@ -329,7 +276,7 @@ fireEvent(this, "show-dialog", {
- **`variant`** (color): `"brand"` (default), `"neutral"`, `"danger"`, `"warning"`, `"success"`
- **`appearance`** (fill style): `"accent"`, `"filled"`, `"outlined"`, `"plain"`
- **`size`**: `"xs"` (extra small, 40px), `"s"` (small, 32px), `"m"` (medium, 40px - default), `"l"` (large, 48px), `"xl"` (extra large, 40px)
- **`size`**: `"small"` (32px), `"medium"` (40px - default), `"large"` (48px)
Common patterns:
+2 -15
View File
@@ -77,22 +77,9 @@ jobs:
env:
GITHUB_REF: ${{ github.ref }}
run: |
# Sleep to give pypi time to populate the new version across mirrors
sleep 240
version=$(echo "$GITHUB_REF" | awk -F"/" '{print $NF}' )
# Wait for the package to become available on PyPI
echo "Waiting for home-assistant-frontend==$version to appear on PyPI..."
for i in $(seq 1 30); do
status=$(curl -s -o /dev/null -w "%{http_code}" "https://pypi.org/pypi/home-assistant-frontend/$version/json")
if [ "$status" = "200" ]; then
echo "Package is available on PyPI!"
break
fi
if [ "$i" = "30" ]; then
echo "Timed out waiting for package to appear on PyPI"
exit 1
fi
echo "Not available yet (HTTP $status), retrying in 30 seconds... ($i/30)"
sleep 30
done
echo "home-assistant-frontend==$version" > ./requirements.txt
# home-assistant/wheels doesn't support SHA pinning
@@ -1,86 +0,0 @@
diff --git a/dist/tinykeys.cjs b/dist/tinykeys.cjs
index 08c98b6eff3b8fb4b727fe8e6b096951d6ef6347..9c44f14862f582766ea1733b6dc0e97f962800d8 100644
--- a/dist/tinykeys.cjs
+++ b/dist/tinykeys.cjs
@@ -61,6 +61,18 @@ function defaultKeybindingsHandlerIgnore(event) {
function getModifierState(event, mod) {
return typeof event.getModifierState === "function" ? event.getModifierState(mod) || ALT_GRAPH_ALIASES.includes(mod) && event.getModifierState("AltGraph") : false;
}
+function splitKeybindingPress(press) {
+ let parts = [];
+ let start = 0;
+ for (let index = 0; index < press.length; index++) {
+ if (press[index] === "+" && /[\w\]]/.test(press[index - 1] || "")) {
+ parts.push(press.slice(start, index));
+ start = index + 1;
+ }
+ }
+ parts.push(press.slice(start));
+ return parts;
+}
/**
* Parses a keybinding string into its parts.
*
@@ -76,10 +88,10 @@ function getModifierState(event, mod) {
*/
function parseKeybinding(str) {
return str.trim().split(" ").map((press) => {
- let parts = press.split(/(?<=\w|\])\+/);
+ let parts = splitKeybindingPress(press);
let last = parts.pop();
let regex = last.match(/^\((.+)\)$/);
- let key = regex ? new RegExp(`^(?:${regex[1]})$`, "iv") : last;
+ let key = regex ? new RegExp(`^(?:${regex[1]})$`, "i") : last;
let requiredModifiers = [];
let optionalModifiers = [];
for (const part of parts) {
@@ -201,5 +213,3 @@ exports.defaultKeybindingsHandlerIgnore = defaultKeybindingsHandlerIgnore;
exports.matchKeybindingPress = matchKeybindingPress;
exports.parseKeybinding = parseKeybinding;
exports.tinykeys = tinykeys;
-
-//# sourceMappingURL=tinykeys.cjs.map
\ No newline at end of file
diff --git a/dist/tinykeys.mjs b/dist/tinykeys.mjs
index c289972d2728e03d9b272268c38fd3392e8845bf..e22897b00aae6cdb0dbbb971445227c07be52918 100644
--- a/dist/tinykeys.mjs
+++ b/dist/tinykeys.mjs
@@ -60,6 +60,18 @@ function defaultKeybindingsHandlerIgnore(event) {
function getModifierState(event, mod) {
return typeof event.getModifierState === "function" ? event.getModifierState(mod) || ALT_GRAPH_ALIASES.includes(mod) && event.getModifierState("AltGraph") : false;
}
+function splitKeybindingPress(press) {
+ let parts = [];
+ let start = 0;
+ for (let index = 0; index < press.length; index++) {
+ if (press[index] === "+" && /[\w\]]/.test(press[index - 1] || "")) {
+ parts.push(press.slice(start, index));
+ start = index + 1;
+ }
+ }
+ parts.push(press.slice(start));
+ return parts;
+}
/**
* Parses a keybinding string into its parts.
*
@@ -75,10 +87,10 @@ function getModifierState(event, mod) {
*/
function parseKeybinding(str) {
return str.trim().split(" ").map((press) => {
- let parts = press.split(/(?<=\w|\])\+/);
+ let parts = splitKeybindingPress(press);
let last = parts.pop();
let regex = last.match(/^\((.+)\)$/);
- let key = regex ? new RegExp(`^(?:${regex[1]})$`, "iv") : last;
+ let key = regex ? new RegExp(`^(?:${regex[1]})$`, "i") : last;
let requiredModifiers = [];
let optionalModifiers = [];
for (const part of parts) {
@@ -196,5 +208,3 @@ function tinykeys(target, keybindingMap, options = {}) {
}
//#endregion
export { createKeybindingsHandler, defaultKeybindingsHandlerIgnore, matchKeybindingPress, parseKeybinding, tinykeys };
-
-//# sourceMappingURL=tinykeys.mjs.map
\ No newline at end of file
+1 -1
View File
@@ -29,7 +29,7 @@ const LICENSE_OVERRIDES = [
// type-fest ships two license files (MIT for code, CC0 for types).
// We use the MIT license since that covers the bundled code.
packageName: "type-fest",
version: "5.7.0",
version: "5.6.0",
licensePath: path.resolve(
paths.root_dir,
"node_modules/type-fest/license-mit"
+7 -7
View File
@@ -70,12 +70,12 @@
"@replit/codemirror-indentation-markers": "6.5.3",
"@swc/helpers": "0.5.23",
"@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "4.1.2",
"@tsparticles/preset-links": "4.1.2",
"@tsparticles/engine": "4.1.1",
"@tsparticles/preset-links": "4.1.1",
"@vibrant/color": "4.0.4",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
"@webcomponents/webcomponentsjs": "2.8.0",
"barcode-detector": "3.2.0",
"barcode-detector": "3.1.3",
"cally": "0.9.2",
"color-name": "2.1.0",
"comlink": "4.4.2",
@@ -88,7 +88,7 @@
"dialog-polyfill": "0.5.6",
"echarts": "6.1.0",
"element-internals-polyfill": "3.0.2",
"fuse.js": "7.4.1",
"fuse.js": "7.4.0",
"google-timezones-json": "1.2.0",
"gulp-zopfli-green": "7.0.0",
"hls.js": "1.6.16",
@@ -114,7 +114,7 @@
"sortablejs": "patch:sortablejs@npm%3A1.15.6#~/.yarn/patches/sortablejs-npm-1.15.6-3235a8f83b.patch",
"stacktrace-js": "2.0.2",
"superstruct": "2.0.2",
"tinykeys": "patch:tinykeys@npm%3A4.0.0#~/.yarn/patches/tinykeys-npm-4.0.0-a6ca3fd771.patch",
"tinykeys": "4.0.0",
"weekstart": "2.0.0",
"workbox-cacheable-response": "7.4.1",
"workbox-core": "7.4.1",
@@ -190,11 +190,11 @@
"rspack-manifest-plugin": "5.2.1",
"serve": "14.2.6",
"sinon": "22.0.0",
"tar": "7.5.16",
"tar": "7.5.15",
"terser-webpack-plugin": "5.6.1",
"ts-lit-plugin": "2.0.2",
"typescript": "6.0.3",
"typescript-eslint": "8.60.1",
"typescript-eslint": "8.60.0",
"vite-tsconfig-paths": "6.1.1",
"vitest": "4.1.8",
"webpack-stats-plugin": "1.1.3",
-59
View File
@@ -1,59 +0,0 @@
import type { HassEntities, HassEntity } from "home-assistant-js-websocket";
import { computeStateDomain } from "./compute_state_domain";
export interface EntityLocation {
latitude: number;
longitude: number;
gpsAccuracy?: number;
}
const findFirstActiveZone = (
inZones: readonly string[],
states: HassEntities
): HassEntity | undefined => {
for (const zoneId of inZones) {
const zone = states[zoneId];
if (
zone &&
!zone.attributes.passive &&
typeof zone.attributes.latitude === "number" &&
typeof zone.attributes.longitude === "number"
) {
return zone;
}
}
return undefined;
};
export const getEntityLocation = (
stateObj: HassEntity,
states: HassEntities
): EntityLocation | undefined => {
const {
latitude,
longitude,
gps_accuracy: gpsAccuracy,
} = stateObj.attributes;
if (typeof latitude === "number" && typeof longitude === "number") {
return { latitude, longitude, gpsAccuracy };
}
if (computeStateDomain(stateObj) !== "person") {
return undefined;
}
const inZones = stateObj.attributes.in_zones;
if (!Array.isArray(inZones) || inZones.length === 0) {
return undefined;
}
const zone = findFirstActiveZone(inZones, states);
if (!zone) {
return undefined;
}
return {
latitude: zone.attributes.latitude,
longitude: zone.attributes.longitude,
};
};
+7 -18
View File
@@ -2,17 +2,12 @@ import { mdiDragHorizontalVariant } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import {
fireEvent,
type HASSDomCurrentTargetEvent,
type HASSDomEvent,
} from "../../common/dom/fire_event";
import { fireEvent } from "../../common/dom/fire_event";
import { isValidEntityId } from "../../common/entity/valid_entity_id";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-sortable";
import "./ha-entity-picker";
import type { HaEntityPicker } from "./ha-entity-picker";
@customElement("ha-entities-picker")
class HaEntitiesPicker extends LitElement {
@@ -156,7 +151,7 @@ class HaEntitiesPicker extends LitElement {
`;
}
private _entityMoved(e: HASSDomEvent<HASSDomEvents["item-moved"]>) {
private _entityMoved(e: CustomEvent) {
e.stopPropagation();
const { oldIndex, newIndex } = e.detail;
const currentEntities = this._currentEntities;
@@ -183,7 +178,7 @@ class HaEntitiesPicker extends LitElement {
return this.value || [];
}
private async _updateEntities(entities: string[]) {
private async _updateEntities(entities) {
this.value = entities;
fireEvent(this, "value-changed", {
@@ -191,12 +186,9 @@ class HaEntitiesPicker extends LitElement {
});
}
private _entityChanged(
event: ValueChangedEvent<string> &
HASSDomCurrentTargetEvent<HaEntityPicker & { curValue: string }>
) {
private _entityChanged(event: ValueChangedEvent<string>) {
event.stopPropagation();
const curValue = event.currentTarget.curValue;
const curValue = (event.currentTarget as any).curValue;
const newValue = event.detail.value;
if (
newValue === curValue ||
@@ -214,15 +206,13 @@ class HaEntitiesPicker extends LitElement {
);
}
private _addEntity(
event: ValueChangedEvent<string> & HASSDomCurrentTargetEvent<HaEntityPicker>
) {
private async _addEntity(event: ValueChangedEvent<string>) {
event.stopPropagation();
const toAdd = event.detail.value;
if (!toAdd) {
return;
}
event.currentTarget.value = "";
(event.currentTarget as any).value = "";
if (!toAdd) {
return;
}
@@ -249,7 +239,6 @@ class HaEntitiesPicker extends LitElement {
}
.entity ha-entity-picker {
flex: 1;
min-width: var(--ha-entities-picker-entity-min-width, auto);
}
.entity-handle {
padding: 8px;
+20 -41
View File
@@ -1,19 +1,17 @@
import type { SelectedDetail } from "@material/mwc-list";
import { consume, type ContextType } from "@lit/context";
import { mdiFilterVariantRemove } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import { fireEvent } from "../common/dom/fire_event";
import { stringCompare } from "../common/string/compare";
import type { LocalizeFunc } from "../common/translations/localize";
import { internationalizationContext, manifestsContext } from "../data/context";
import type { IntegrationManifest } from "../data/integration";
import { domainToName } from "../data/integration";
import { domainToName, fetchIntegrationManifests } from "../data/integration";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-check-list-item";
import "./ha-domain-icon";
import "./ha-expansion-panel";
@@ -23,26 +21,15 @@ import type { HaInputSearch } from "./input/ha-input-search";
@customElement("ha-filter-integrations")
export class HaFilterIntegrations extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public value?: string[];
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean, reflect: true }) public expanded = false;
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@consumeLocalize()
private _localize!: LocalizeFunc;
@consume({ context: manifestsContext, subscribe: true })
@state()
private _manifests?: ContextType<typeof manifestsContext>;
private _manifestList = memoizeOne(
(manifests: ContextType<typeof manifestsContext>) =>
Object.values(manifests)
);
@state() private _manifests?: IntegrationManifest[];
@state() private _shouldRender = false;
@@ -51,10 +38,6 @@ export class HaFilterIntegrations extends LitElement {
@query("ha-list") private _list?: HTMLElement;
protected render() {
const manifests = this._manifests
? this._manifestList(this._manifests)
: undefined;
return html`
<ha-expansion-panel
left-chevron
@@ -63,7 +46,7 @@ export class HaFilterIntegrations extends LitElement {
@expanded-changed=${this._expandedChanged}
>
<div slot="header" class="header">
${this._localize("ui.panel.config.integrations.caption")}
${this.hass.localize("ui.panel.config.integrations.caption")}
${this.value?.length
? html`<div class="badge">${this.value?.length}</div>
<ha-icon-button
@@ -72,7 +55,7 @@ export class HaFilterIntegrations extends LitElement {
></ha-icon-button>`
: nothing}
</div>
${manifests && this._shouldRender
${this._manifests && this._shouldRender
? html`<ha-input-search
appearance="outlined"
.value=${this._filter}
@@ -86,11 +69,10 @@ export class HaFilterIntegrations extends LitElement {
>
${repeat(
this._integrations(
this._localize,
manifests,
this.hass.localize,
this._manifests,
this._filter,
this.value,
this._i18n.locale.language
this.value
),
(i) => i.domain,
(integration) =>
@@ -135,8 +117,9 @@ export class HaFilterIntegrations extends LitElement {
this.expanded = ev.detail.expanded;
}
protected firstUpdated() {
this._i18n.loadBackendTranslation("title");
protected async firstUpdated() {
this._manifests = await fetchIntegrationManifests(this.hass);
this.hass.loadBackendTranslation("title");
}
private _integrations = memoizeOne(
@@ -144,8 +127,7 @@ export class HaFilterIntegrations extends LitElement {
localize: LocalizeFunc,
manifest: IntegrationManifest[],
filter: string | undefined,
_value: string[] | undefined,
language: string
_value
) =>
manifest
.map((mnfst) => ({
@@ -162,20 +144,17 @@ export class HaFilterIntegrations extends LitElement {
mnfst.name.toLowerCase().includes(filter) ||
mnfst.domain.toLowerCase().includes(filter))
)
.sort((a, b) => stringCompare(a.name, b.name, language))
.sort((a, b) =>
stringCompare(a.name, b.name, this.hass.locale.language)
)
);
private _itemSelected(ev: CustomEvent<SelectedDetail<Set<number>>>) {
if (!this._manifests) {
return;
}
const integrations = this._integrations(
this._localize,
this._manifestList(this._manifests),
this.hass.localize,
this._manifests!,
this._filter,
this.value,
this._i18n.locale.language
this.value
);
const visibleDomains = new Set(integrations.map((i) => i.domain));
+23 -72
View File
@@ -9,11 +9,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../common/array/ensure-array";
import {
fireEvent,
type HASSDomCurrentTargetEvent,
type HASSDomEvent,
} from "../common/dom/fire_event";
import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
import { computeObjectId } from "../common/entity/compute_object_id";
import { supportsFeature } from "../common/entity/supports-feature";
@@ -36,11 +32,9 @@ import {
import type { HomeAssistant, ValueChangedEvent } from "../types";
import { documentationUrl } from "../util/documentation-url";
import "./ha-checkbox";
import type { HaCheckbox } from "./ha-checkbox";
import "./ha-icon-button";
import "./ha-markdown";
import "./ha-selector/ha-selector";
import type { HaSelector } from "./ha-selector/ha-selector";
import "./ha-service-picker";
import "./ha-service-section-icon";
import "./ha-settings-row";
@@ -62,22 +56,6 @@ const showOptionalToggle = (field) =>
!field.required &&
!("boolean" in field.selector && field.default);
const FULL_WIDTH_SELECTOR_TYPES = new Set([
"action",
"area",
"device",
"entity",
"floor",
"label",
"target",
]);
const isFullWidthSelector = (selector?: Selector): boolean =>
!!selector &&
Object.keys(selector).some((selectorType) =>
FULL_WIDTH_SELECTOR_TYPES.has(selectorType)
);
interface Field extends Omit<HassService["fields"][string], "selector"> {
key: string;
selector?: Selector;
@@ -497,14 +475,6 @@ export class HaServiceControl extends LitElement {
)) ||
serviceData?.description;
const targetSelector =
serviceData && "target" in serviceData
? this._targetSelector(
serviceData.target as TargetSelector,
this._value?.target
)
: undefined;
return html`${this.hidePicker
? nothing
: html`<ha-service-picker
@@ -542,15 +512,16 @@ export class HaServiceControl extends LitElement {
</div>
`}
${serviceData && "target" in serviceData
? html`<ha-settings-row
.narrow=${this.narrow || isFullWidthSelector(targetSelector)}
>
? html`<ha-settings-row .narrow=${this.narrow}>
<span slot="heading"
>${this.hass.localize("ui.components.service-control.target")}</span
>
<ha-selector
.hass=${this.hass}
.selector=${targetSelector}
.selector=${this._targetSelector(
serviceData.target as TargetSelector,
this._value?.target
)}
.disabled=${this.disabled}
@value-changed=${this._targetChanged}
.value=${this._value?.target}
@@ -693,9 +664,7 @@ export class HaServiceControl extends LitElement {
: undefined;
return dataField.selector
? html`<ha-settings-row
.narrow=${this.narrow || isFullWidthSelector(selector)}
>
? html`<ha-settings-row .narrow=${this.narrow}>
${!showOptional
? hasOptional
? html`<div slot="prefix" class="checkbox-spacer"></div>`
@@ -768,16 +737,14 @@ export class HaServiceControl extends LitElement {
);
};
private _toggleCheckbox(ev: HASSDomCurrentTargetEvent<HTMLElement>) {
private _toggleCheckbox(ev: Event) {
const checkbox = (
ev.currentTarget as HTMLElement
)?.parentElement?.querySelector("ha-checkbox");
checkbox?.click();
}
private _checkboxChanged(
ev: HASSDomCurrentTargetEvent<HaCheckbox & { key: string }>
) {
private _checkboxChanged(ev) {
const checked = ev.currentTarget.checked;
const key = ev.currentTarget.key;
let data;
@@ -900,7 +867,7 @@ export class HaServiceControl extends LitElement {
});
}
private _entityPicked(ev: HASSDomEvent<{ value: string | undefined }>) {
private _entityPicked(ev: CustomEvent) {
ev.stopPropagation();
const newValue = ev.detail.value;
if (this._value?.data?.entity_id === newValue) {
@@ -921,12 +888,7 @@ export class HaServiceControl extends LitElement {
});
}
private _targetChanged(
ev: HASSDomEvent<{
value: HassServiceTarget | undefined;
isValid?: boolean;
}>
) {
private _targetChanged(ev: CustomEvent) {
ev.stopPropagation();
if (ev.detail.isValid === false) {
// Don't clear an object selector that returns invalid YAML
@@ -948,16 +910,13 @@ export class HaServiceControl extends LitElement {
});
}
private _serviceDataChanged(
ev: HASSDomEvent<{ value: unknown; isValid?: boolean }> &
HASSDomCurrentTargetEvent<HaSelector & { key: string }>
) {
private _serviceDataChanged(ev: CustomEvent) {
ev.stopPropagation();
if (ev.detail.isValid === false) {
// Don't clear an object selector that returns invalid YAML
return;
}
const key = ev.currentTarget.key;
const key = (ev.currentTarget as any).key;
const value = ev.detail.value;
if (
this._value?.data?.[key] === value ||
@@ -972,9 +931,7 @@ export class HaServiceControl extends LitElement {
if (
value === "" ||
value === undefined ||
(value !== null &&
typeof value === "object" &&
!Object.keys(value).length)
(typeof value === "object" && !Object.keys(value).length)
) {
delete data[key];
delete this._stickySelector[key];
@@ -988,12 +945,7 @@ export class HaServiceControl extends LitElement {
});
}
private _dataChanged(
ev: HASSDomEvent<{
value: NonNullable<HaServiceControl["value"]>["data"];
isValid?: boolean;
}>
) {
private _dataChanged(ev: CustomEvent) {
ev.stopPropagation();
if (!ev.detail.isValid) {
return;
@@ -1017,15 +969,14 @@ export class HaServiceControl extends LitElement {
static styles = css`
ha-settings-row {
padding: var(--service-control-padding, 0 var(--ha-space-4));
padding: var(--service-control-padding, 0 16px);
}
ha-settings-row[narrow] {
padding-bottom: var(--ha-space-2);
padding-bottom: 8px;
}
ha-settings-row {
--settings-row-content-width: 100%;
--settings-row-prefix-display: contents;
--ha-entities-picker-entity-min-width: 0;
border-top: var(
--service-control-items-border-top,
1px solid var(--divider-color)
@@ -1035,20 +986,20 @@ export class HaServiceControl extends LitElement {
ha-entity-picker,
ha-yaml-editor {
display: block;
margin: var(--service-control-padding, 0 var(--ha-space-4));
margin: var(--service-control-padding, 0 16px);
}
ha-yaml-editor {
padding: var(--ha-space-4) 0;
padding: 16px 0;
}
p {
margin: var(--service-control-padding, 0 var(--ha-space-4));
padding: var(--ha-space-4) 0;
margin: var(--service-control-padding, 0 16px);
padding: 16px 0;
}
:host([hide-picker]) p {
padding-top: 0;
}
.checkbox-spacer {
width: var(--ha-space-8);
width: 32px;
}
.clickable {
cursor: pointer;
@@ -1069,7 +1020,7 @@ export class HaServiceControl extends LitElement {
}
ha-expansion-panel {
--ha-card-border-radius: var(--ha-border-radius-square);
--expansion-panel-summary-padding: 0 var(--ha-space-4);
--expansion-panel-summary-padding: 0 16px;
--expansion-panel-content-padding: 0;
}
`;
+4 -4
View File
@@ -23,7 +23,6 @@ import type { LeafletModuleType } from "../../common/dom/setup-leaflet-map";
import { setupLeafletMap } from "../../common/dom/setup-leaflet-map";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { getEntityLocation } from "../../common/entity/get_entity_location";
import { DecoratedMarker } from "../../common/map/decorated_marker";
import { filterXSS } from "../../common/util/xss";
import type { HomeAssistant, ThemeMode } from "../../types";
@@ -585,17 +584,18 @@ export class HaMap extends ReactiveElement {
const customTitle = typeof entity !== "string" ? entity.name : undefined;
const title = customTitle ?? computeStateName(stateObj);
const {
latitude,
longitude,
passive,
icon,
radius,
entity_picture: entityPicture,
gps_accuracy: gpsAccuracy,
} = stateObj.attributes;
const location = getEntityLocation(stateObj, hass.states);
if (!location) {
if (!(latitude && longitude)) {
continue;
}
const { latitude, longitude, gpsAccuracy } = location;
if (computeStateDomain(stateObj) === "zone") {
// DRAW ZONE
-8
View File
@@ -26,13 +26,6 @@ export interface HomeFrontendSystemData {
shortcuts?: ShortcutItem[];
}
export interface EnergyFrontendSystemData {
// Stable "<view>.<card-type>" keys of energy dashboard cards the user has
// hidden. An absent key or array means nothing is hidden (all cards visible),
// so cards added in the future are shown by default.
hidden_cards?: string[];
}
declare global {
interface FrontendUserData {
core: CoreFrontendUserData;
@@ -41,7 +34,6 @@ declare global {
interface FrontendSystemData {
core: CoreFrontendSystemData;
home: HomeFrontendSystemData;
energy: EnergyFrontendSystemData;
}
}
@@ -3,7 +3,6 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import { getEntityLocation } from "../../../common/entity/get_entity_location";
import "../../../components/ha-button";
import "../../../components/map/ha-map";
import { showZoneEditor } from "../../../data/zone";
@@ -22,13 +21,8 @@ class MoreInfoPerson extends LitElement {
return nothing;
}
const location = getEntityLocation(this.stateObj, this.hass.states);
const hasOwnCoordinates =
typeof this.stateObj.attributes.latitude === "number" &&
typeof this.stateObj.attributes.longitude === "number";
return html`
${location
${this.stateObj.attributes.latitude && this.stateObj.attributes.longitude
? html`
<ha-map
.hass=${this.hass}
@@ -37,7 +31,10 @@ class MoreInfoPerson extends LitElement {
></ha-map>
`
: ""}
${!__DEMO__ && this.hass.user?.is_admin && hasOwnCoordinates
${!__DEMO__ &&
this.hass.user?.is_admin &&
this.stateObj.attributes.latitude &&
this.stateObj.attributes.longitude
? html`
<div class="actions">
<ha-button
@@ -5,6 +5,7 @@ in core bundle slows things down and causes duplicate registration.
This is the entry point for providing external app stuff from app entrypoint.
*/
import type { HASSDomEvent } from "../common/dom/fire_event";
import { fireEvent } from "../common/dom/fire_event";
import { mainWindow } from "../common/dom/get_main_window";
import { navigate } from "../common/navigate";
@@ -15,6 +16,7 @@ import type {
EMIncomingMessageBarCodeScanResult,
EMIncomingMessageCommands,
ImprovDiscoveredDevice,
MatterCommissionFinish,
} from "./external_messaging";
const barCodeListeners = new Set<
@@ -91,6 +93,8 @@ export const handleExternalMessage = (
fireEvent(window, "improv-discovered-device", msg.payload);
} else if (msg.command === "improv/device_setup_done") {
fireEvent(window, "improv-device-setup-done");
} else if (msg.command === "matter/commission/finish") {
fireEvent(window, "matter-commission-finish", msg.payload);
} else if (msg.command === "bar_code/scan_result") {
barCodeListeners.forEach((listener) => listener(msg));
} else if (msg.command === "bar_code/aborted") {
@@ -115,5 +119,10 @@ declare global {
interface HASSDomEvents {
"improv-discovered-device": ImprovDiscoveredDevice;
"improv-device-setup-done": undefined;
"matter-commission-finish": MatterCommissionFinish;
}
interface GlobalEventHandlersEventMap {
"matter-commission-finish": HASSDomEvent<MatterCommissionFinish>;
}
}
+14
View File
@@ -320,6 +320,18 @@ export interface EMIncomingMessageKioskModeSet {
};
}
export interface MatterCommissionFinish {
name: string | null;
success: boolean;
}
export interface EMIncomingMessageMatterCommissionFinish extends EMMessage {
id: number;
type: "command";
command: "matter/commission/finish";
payload: MatterCommissionFinish;
}
export type EMIncomingMessageCommands =
| EMIncomingMessageRestart
| EMIncomingMessageNavigate
@@ -331,6 +343,7 @@ export type EMIncomingMessageCommands =
| EMIncomingMessageBarCodeScanAborted
| EMIncomingMessageImprovDeviceDiscovered
| EMIncomingMessageImprovDeviceSetupDone
| EMIncomingMessageMatterCommissionFinish
| EMIncomingMessageKioskModeSet;
type EMIncomingMessage =
@@ -346,6 +359,7 @@ export interface ExternalConfig {
canWriteTag?: boolean;
hasExoPlayer?: boolean;
canCommissionMatter?: boolean;
hasMatterStatusReport?: boolean;
canImportThreadCredentials?: boolean;
canTransferThreadCredentialsToKeychain?: boolean;
hasAssist?: boolean;
@@ -232,6 +232,7 @@ class HaConfigSectionUpdates extends LitElement {
: nothing}
</div>
<ha-config-updates
.hass=${this.hass}
.narrow=${this.narrow}
.updateEntities=${group.entities}
showAll
@@ -255,6 +256,7 @@ class HaConfigSectionUpdates extends LitElement {
</div>
</div>
<ha-config-updates
.hass=${this.hass}
.narrow=${this.narrow}
.updateEntities=${skippedUpdates}
showAll
@@ -278,6 +280,7 @@ class HaConfigSectionUpdates extends LitElement {
</div>
</div>
<ha-config-updates
.hass=${this.hass}
.narrow=${this.narrow}
.updateEntities=${notInstallableUpdates}
showAll
@@ -326,6 +326,7 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
</a>
</div>
<ha-config-updates
.hass=${this.hass}
.narrow=${this.narrow}
.updateEntities=${canInstallUpdates}
></ha-config-updates>
@@ -1,9 +1,9 @@
import { consume, type ContextType } from "@lit/context";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { consumeLocalize } from "../../../common/decorators/consume-context-entry";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
import { getDeviceArea } from "../../../common/entity/context/get_device_context";
@@ -14,51 +14,62 @@ import "../../../components/ha-spinner";
import "../../../components/item/ha-list-item-button";
import "../../../components/list/ha-list-base";
import "../../../components/progress/ha-progress-ring";
import {
areasContext,
devicesContext,
fullEntitiesContext,
statesContext,
} from "../../../data/context";
import { entityRegistryByEntityId } from "../../../data/entity/entity_registry";
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
import { subscribeDeviceRegistry } from "../../../data/device/device_registry";
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
import { subscribeEntityRegistry } from "../../../data/entity/entity_registry";
import type { UpdateEntity } from "../../../data/update";
import type { LocalizeFunc } from "../../../common/translations/localize";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../types";
@customElement("ha-config-updates")
class HaConfigUpdates extends LitElement {
class HaConfigUpdates extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public updateEntities?: UpdateEntity[];
@consumeLocalize()
private _localize!: LocalizeFunc;
@state() private _devices?: DeviceRegistryEntry[];
@consume({ context: statesContext, subscribe: true })
private _states!: ContextType<typeof statesContext>;
@state() private _entities?: EntityRegistryEntry[];
@consume({ context: devicesContext, subscribe: true })
private _devices!: ContextType<typeof devicesContext>;
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeDeviceRegistry(this.hass.connection, (entries) => {
this._devices = entries;
}),
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entities = entities.filter((entity) => entity.device_id !== null);
}),
];
}
@consume({ context: areasContext, subscribe: true })
private _areas!: ContextType<typeof areasContext>;
private getDeviceEntry = memoizeOne(
(deviceId: string): DeviceRegistryEntry | undefined =>
this._devices?.find((device) => device.id === deviceId)
);
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
private _entities: ContextType<typeof fullEntitiesContext> = [];
private getEntityEntry = memoizeOne(
(entityId: string): EntityRegistryEntry | undefined =>
this._entities?.find((entity) => entity.entity_id === entityId)
);
private _renderUpdateProgress(entity: UpdateEntity) {
if (entity.attributes.update_percentage != null) {
return html`<ha-progress-ring
size="small"
.value=${entity.attributes.update_percentage}
.label=${this._localize("ui.panel.config.updates.update_in_progress")}
.label=${this.hass.localize(
"ui.panel.config.updates.update_in_progress"
)}
></ha-progress-ring>`;
}
if (entity.attributes.in_progress) {
return html`<ha-spinner
size="small"
.ariaLabel=${this._localize(
.ariaLabel=${this.hass.localize(
"ui.panel.config.updates.update_in_progress"
)}
></ha-spinner>`;
@@ -73,23 +84,22 @@ class HaConfigUpdates extends LitElement {
}
const updates = this.updateEntities;
const entities = entityRegistryByEntityId(this._entities);
return html`
<ha-list-base
aria-label=${this._localize("ui.panel.config.updates.caption")}
aria-label=${this.hass.localize("ui.panel.config.updates.caption")}
>
${updates.map((entity) => {
const entityEntry = entities[entity.entity_id];
const entityEntry = this.getEntityEntry(entity.entity_id);
const deviceEntry =
entityEntry && entityEntry.device_id
? this._devices[entityEntry.device_id]
? this.getDeviceEntry(entityEntry.device_id)
: undefined;
const areaName =
deviceEntry && deviceEntry.entry_type !== "service"
? getDeviceArea(deviceEntry, this._areas)?.name ||
this._localize("ui.panel.config.updates.no_area")
? getDeviceArea(deviceEntry, this.hass.areas)?.name ||
this.hass.localize("ui.panel.config.updates.no_area")
: undefined;
return html`
@@ -102,6 +112,7 @@ class HaConfigUpdates extends LitElement {
<state-badge
.title=${entity.attributes.title ||
entity.attributes.friendly_name}
.hass=${this.hass}
.stateObj=${entity}
class=${ifDefined(
this.narrow && entity.attributes.in_progress
@@ -119,8 +130,8 @@ class HaConfigUpdates extends LitElement {
>${deviceEntry
? computeDeviceNameDisplay(
deviceEntry,
this._localize,
this._states
this.hass.localize,
this.hass.states
)
: entity.attributes.friendly_name}</span
>
@@ -128,7 +139,7 @@ class HaConfigUpdates extends LitElement {
${areaName ? html`${areaName}` : nothing}
${entity.attributes.title} ${entity.attributes.latest_version}
${entity.attributes.skipped_version
? `(${this._localize("ui.panel.config.updates.skipped")})`
? `(${this.hass.localize("ui.panel.config.updates.skipped")})`
: nothing}
</span>
${!this.narrow
@@ -846,6 +846,7 @@ export class HaConfigDeviceDashboard extends LitElement {
@expanded-changed=${this._filterExpanded}
></ha-filter-floor-areas>
<ha-filter-integrations
.hass=${this.hass}
.value=${this._filters["ha-filter-integrations"]?.value}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
@@ -1,288 +0,0 @@
import { consume, type ContextType } from "@lit/context";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeKeys } from "../../../../common/translations/localize";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-settings-row";
import "../../../../components/ha-spinner";
import "../../../../components/ha-switch";
import type { HaSwitch } from "../../../../components/ha-switch";
import "../../../../components/ha-tooltip";
import {
connectionContext,
internationalizationContext,
} from "../../../../data/context";
import {
fetchFrontendSystemData,
saveFrontendSystemData,
} from "../../../../data/frontend";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../resources/styles";
import { showToast } from "../../../../util/toast";
import type {
EnergyCardCatalogEntry,
EnergyViewPath,
} from "../../../energy/strategies/energy-cards";
import { ENERGY_CARD_CATALOG } from "../../../energy/strategies/energy-cards";
import type { EnergyCustomiseDialogParams } from "./show-dialog-energy-customise";
const VIEW_GROUPS: { view: EnergyViewPath; labelKey: LocalizeKeys }[] = [
{
view: "overview",
labelKey: "ui.panel.config.energy.customise.groups.overview",
},
{ view: "electricity", labelKey: "ui.panel.config.energy.tabs.electricity" },
{ view: "gas", labelKey: "ui.panel.config.energy.tabs.gas" },
{ view: "water", labelKey: "ui.panel.config.energy.tabs.water" },
{ view: "now", labelKey: "ui.panel.config.energy.customise.groups.now" },
];
@customElement("dialog-energy-customise")
export class DialogEnergyCustomise
extends LitElement
implements HassDialog<EnergyCustomiseDialogParams>
{
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@state()
@consume({ context: connectionContext, subscribe: true })
private _connection!: ContextType<typeof connectionContext>;
@state() private _params?: EnergyCustomiseDialogParams;
@state() private _open = false;
@state() private _error?: string;
@state() private _submitting = false;
// Working copy of the hidden card keys. A switch that is ON means the card is
// visible, i.e. its key is NOT in this set.
@state() private _hidden?: Set<string>;
public showDialog(params: EnergyCustomiseDialogParams): void {
this._params = params;
this._open = true;
this._loadHidden();
}
public closeDialog(): boolean {
this._open = false;
return true;
}
private _dialogClosed(): void {
this._params = undefined;
this._hidden = undefined;
this._error = undefined;
this._submitting = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
private async _loadHidden(): Promise<void> {
this._error = undefined;
// showDialog runs before the dialog is connected to the DOM, so wait for
// the first update to ensure the consumed contexts have resolved.
await this.updateComplete;
if (!this._params) {
return;
}
try {
// The card labels reuse keys from the "energy" translation fragment,
// which is not guaranteed to be loaded on the config page.
const [data] = await Promise.all([
fetchFrontendSystemData(this._connection.connection, "energy"),
this._i18n.loadFragmentTranslation("energy"),
]);
this._hidden = new Set(data?.hidden_cards ?? []);
} catch (err: any) {
this._error = err?.message || "Unknown error";
this._hidden = new Set();
}
}
protected render() {
if (!this._params) {
return nothing;
}
return html`
<ha-dialog
.open=${this._open}
.headerTitle=${this._i18n.localize(
"ui.panel.config.energy.customise.title"
)}
prevent-scrim-close
@closed=${this._dialogClosed}
>
${!this._hidden
? html`<div class="loading">
<ha-spinner size="large"></ha-spinner>
</div>`
: this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: html`<div class="groups">${this._renderGroups()}</div>`}
<ha-dialog-footer slot="footer">
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this.closeDialog}
.disabled=${this._submitting}
>
${this._i18n.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._save}
.disabled=${this._submitting || !this._hidden || !!this._error}
>
${this._i18n.localize("ui.common.save")}
</ha-button>
</ha-dialog-footer>
</ha-dialog>
`;
}
private _renderGroups() {
const prefs = this._params!.preferences;
return VIEW_GROUPS.map((group) => {
const cards = ENERGY_CARD_CATALOG.filter((c) => c.view === group.view);
// Hide the whole group when none of its cards apply to the current config.
if (!cards.some((c) => c.isApplicable(prefs))) {
return nothing;
}
return html`
<ha-expansion-panel
outlined
expanded
.header=${this._i18n.localize(group.labelKey)}
>
<div class="cards">
${cards.map((card) => this._renderCardRow(card))}
</div>
</ha-expansion-panel>
`;
});
}
private _renderCardRow(card: EnergyCardCatalogEntry) {
const applicable = card.isApplicable(this._params!.preferences);
const label = this._i18n.localize(card.labelKey);
const rowId = `row-${card.key}`;
return html`
<ha-settings-row slim id=${rowId}>
<span slot="heading" class=${applicable ? "" : "disabled"}
>${label}</span
>
<ha-switch
.checked=${applicable && !this._hidden!.has(card.key)}
.disabled=${!applicable}
.ariaLabel=${label}
data-card-key=${card.key}
@change=${this._toggleCard}
></ha-switch>
</ha-settings-row>
${applicable
? nothing
: html`
<ha-tooltip .for=${rowId} placement="top">
${this._i18n.localize(
"ui.panel.config.energy.customise.unavailable"
)}
</ha-tooltip>
`}
`;
}
private _toggleCard = (ev: Event): void => {
const target = ev.currentTarget as HaSwitch;
const cardKey = target.dataset.cardKey;
if (!cardKey) {
return;
}
const next = new Set(this._hidden);
if (target.checked) {
next.delete(cardKey);
} else {
next.add(cardKey);
}
this._hidden = next;
};
private async _save(): Promise<void> {
if (!this._hidden) {
return;
}
this._submitting = true;
try {
const hidden = Array.from(this._hidden);
await saveFrontendSystemData(this._connection.connection, "energy", {
hidden_cards: hidden.length ? hidden : undefined,
});
this._params?.saveCallback?.();
this.closeDialog();
} catch (_err) {
showToast(this, {
message: this._i18n.localize(
"ui.panel.config.energy.customise.save_failed"
),
duration: 0,
dismissable: true,
});
} finally {
this._submitting = false;
}
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-dialog {
--dialog-content-padding: var(--ha-space-2) var(--ha-space-6);
}
.loading {
display: flex;
justify-content: center;
padding: var(--ha-space-6);
}
span.disabled {
color: var(--disabled-text-color);
}
.groups {
display: flex;
flex-direction: column;
gap: var(--ha-space-4);
}
ha-expansion-panel {
display: block;
--expansion-panel-content-padding: 0;
--expansion-panel-summary-padding: 0 var(--ha-space-4);
border-radius: var(--ha-border-radius-md);
--ha-card-border-radius: var(--ha-border-radius-md);
}
.cards {
padding: 0 var(--ha-space-4);
}
.cards ha-settings-row {
min-height: 48px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-energy-customise": DialogEnergyCustomise;
}
}
@@ -1,22 +0,0 @@
import { fireEvent } from "../../../../common/dom/fire_event";
import type { EnergyPreferences } from "../../../../data/energy";
export interface EnergyCustomiseDialogParams {
preferences: EnergyPreferences;
// Called after a successful save (e.g. to show a toast on the page).
saveCallback?: () => void;
}
export const loadEnergyCustomiseDialog = () =>
import("./dialog-energy-customise");
export const showEnergyCustomiseDialog = (
element: HTMLElement,
params: EnergyCustomiseDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-energy-customise",
dialogImport: loadEnergyCustomiseDialog,
dialogParams: params,
});
};
+9 -43
View File
@@ -1,11 +1,5 @@
import "../../../layouts/hass-error-screen";
import {
mdiDownload,
mdiFire,
mdiLightningBolt,
mdiViewDashboardEdit,
mdiWater,
} from "@mdi/js";
import { mdiDownload, mdiFire, mdiLightningBolt, mdiWater } from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -37,9 +31,7 @@ import "./components/ha-energy-battery-settings";
import "./components/ha-energy-gas-settings";
import "./components/ha-energy-water-settings";
import { fileDownload } from "../../../util/file_download";
import { showToast } from "../../../util/toast";
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
import { showEnergyCustomiseDialog } from "./dialogs/show-dialog-energy-customise";
const INITIAL_CONFIG: EnergyPreferences = {
energy_sources: [],
@@ -132,22 +124,14 @@ class HaConfigEnergy extends LitElement {
.route=${this.route}
.tabs=${TABS}
>
<div slot="toolbar-icon" class="toolbar-icons">
<ha-icon-button
.path=${mdiViewDashboardEdit}
.label=${this.hass.localize(
"ui.panel.config.energy.customise.toolbar_action"
)}
@click=${this._customise}
></ha-icon-button>
<ha-icon-button
.path=${mdiDownload}
.label=${this.hass.localize(
"ui.panel.config.devices.download_diagnostics"
)}
@click=${this._downloadDiagnostics}
></ha-icon-button>
</div>
<ha-icon-button
slot="toolbar-icon"
.path=${mdiDownload}
.label=${this.hass.localize(
"ui.panel.config.devices.download_diagnostics"
)}
@click=${this._downloadDiagnostics}
></ha-icon-button>
<ha-alert>
${this.hass.localize("ui.panel.config.energy.new_device_info")}
</ha-alert>
@@ -270,20 +254,6 @@ class HaConfigEnergy extends LitElement {
this._statsMetadata = statsMetadata;
}
private _customise() {
if (!this._preferences) {
return;
}
showEnergyCustomiseDialog(this, {
preferences: this._preferences,
saveCallback: () => {
showToast(this, {
message: this.hass.localize("ui.panel.config.energy.customise.saved"),
});
},
});
}
private async _downloadDiagnostics() {
const data = {
version: this.hass.config.version,
@@ -315,10 +285,6 @@ class HaConfigEnergy extends LitElement {
return [
haStyle,
css`
.toolbar-icons {
display: flex;
align-items: center;
}
.content {
padding: 0 var(--ha-space-5);
max-width: 1040px;
@@ -1014,6 +1014,7 @@ export class HaConfigEntities extends LitElement {
@expanded-changed=${this._filterExpanded}
></ha-filter-domains>
<ha-filter-integrations
.hass=${this.hass}
.value=${this._filters["ha-filter-integrations"]}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
@@ -2,6 +2,7 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { dynamicElement } from "../../../../../common/dom/dynamic-element-directive";
import type { HASSDomEvent } from "../../../../../common/dom/fire_event";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { computeDomain } from "../../../../../common/entity/compute_domain";
import { computeDeviceName } from "../../../../../common/entity/compute_device_name";
@@ -10,6 +11,7 @@ import "../../../../../components/ha-dialog-footer";
import "../../../../../components/ha-icon-button-arrow-prev";
import "../../../../../components/ha-button";
import "../../../../../components/ha-dialog";
import type { MatterCommissionFinish } from "../../../../../external_app/external_messaging";
import {
commissionMatterDevice,
watchForNewMatterDevice,
@@ -82,6 +84,10 @@ class DialogMatterAddDevice extends LitElement {
@state() private _mainEntity?: ExtEntityRegistryEntry;
@state() private _proposedDeviceName?: string;
@state() private _commissioningFinished = false;
@state() private _deviceAddedState: {
name: string;
area: string | undefined;
@@ -104,15 +110,63 @@ class DialogMatterAddDevice extends LitElement {
// make sure a refresh of the page will navigate to the device page, old iOS apps will refresh the webview when commissioning is done
setRefreshUrl(`/config/devices/device/${device.id}`);
this._newDevice = device;
this._step = "device_added";
this._fetchMainEntity();
this._maybeShowDeviceAdded();
});
if (this._waitForCommissioningFinish) {
window.addEventListener(
"matter-commission-finish",
this._handleCommissionFinish
);
}
}
public closeDialog(): void {
this._open = false;
}
private get _waitForCommissioningFinish(): boolean {
// When the app supports reporting Matter commissioning status, defer
// advancing past the spinner until we receive matter/commission/finish.
return !!this.hass.auth.external?.config.hasMatterStatusReport;
}
private _maybeShowDeviceAdded(): void {
if (!this._newDevice) {
return;
}
if (this._waitForCommissioningFinish && !this._commissioningFinished) {
return;
}
this._step = "device_added";
}
private _handleCommissionFinish = (
ev: HASSDomEvent<MatterCommissionFinish>
) => {
const { name, success } = ev.detail;
if (!success) {
if (this._newDevice) {
// Device already showed up in the registry — ignore the failure signal
// and let the user finish the rename flow.
return;
}
showToast(this, {
message: this.hass.localize(
"ui.dialogs.matter-add-device.add_device_failed"
),
duration: 2000,
});
this.closeDialog();
return;
}
this._commissioningFinished = true;
if (name) {
this._proposedDeviceName = name;
}
this._maybeShowDeviceAdded();
};
protected updated(changedProps: Map<string, unknown>): void {
// Retry fetching main entity when hass updates (entities may not be available immediately)
if (
@@ -160,6 +214,8 @@ class DialogMatterAddDevice extends LitElement {
this._newDevice = undefined;
this._mainEntity = undefined;
this._mainEntityFetched = false;
this._proposedDeviceName = undefined;
this._commissioningFinished = false;
this._deviceAddedState = {
name: "",
area: undefined,
@@ -168,6 +224,10 @@ class DialogMatterAddDevice extends LitElement {
};
this._unsub?.();
this._unsub = undefined;
window.removeEventListener(
"matter-commission-finish",
this._handleCommissionFinish
);
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -211,6 +271,7 @@ class DialogMatterAddDevice extends LitElement {
hass: this.hass,
device: this._newDevice,
mainEntity: this._mainEntity,
proposedName: this._proposedDeviceName,
}
)}
</div>
@@ -36,6 +36,8 @@ class MatterAddDeviceDeviceAdded extends LitElement {
@property({ attribute: false }) public mainEntity?: ExtEntityRegistryEntry;
@property({ attribute: false }) public proposedName?: string;
@state() private _deviceName = "";
@state() private _area: string | undefined;
@@ -49,8 +51,18 @@ class MatterAddDeviceDeviceAdded extends LitElement {
protected willUpdate(changedProps: PropertyValues) {
if (!this._initialized && this.device) {
this._initialized = true;
this._deviceName = computeDeviceName(this.device) ?? "";
this._deviceName =
this.proposedName || (computeDeviceName(this.device) ?? "");
this._area = this.device.area_id ?? undefined;
} else if (
changedProps.has("proposedName") &&
this.proposedName &&
this.device &&
this._deviceName === (computeDeviceName(this.device) ?? "")
) {
// proposedName arrived after we initialized, and the user hasn't
// changed the name yet — adopt it
this._deviceName = this.proposedName;
}
if (
!this._deviceClassInitialized &&
@@ -158,7 +170,9 @@ class MatterAddDeviceDeviceAdded extends LitElement {
referrerpolicy="no-referrer"
/>
<div class="device-name">
<span>${computeDeviceName(this.device)}</span>
<span
>${this.proposedName || computeDeviceName(this.device)}</span
>
<span class="secondary">Matter</span>
</div>
</div>
+1 -40
View File
@@ -41,10 +41,7 @@ import "../../../components/ha-dropdown-item";
import "../../../components/ha-icon-button";
import "../../../components/ha-list";
import "../../../components/ha-svg-icon";
import {
fullEntitiesContext,
type RelatedContextItem,
} from "../../../data/context";
import { fullEntitiesContext } from "../../../data/context";
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
import { updateEntityRegistryEntry } from "../../../data/entity/entity_registry";
@@ -153,8 +150,6 @@ export class HaSceneEditor extends PreventUnsavedMixin(
private _entityRegistryUpdate?: EntityRegistryUpdate;
private _relatedContext?: RelatedContextItem;
private _newSceneId?: string;
private _entityRegCreated?: (
@@ -658,40 +653,6 @@ export class HaSceneEditor extends PreventUnsavedMixin(
);
}
}
if (
changedProps.has("sceneId") ||
changedProps.has("_scene") ||
changedProps.has("_registryEntry")
) {
this._setRelatedContext();
}
}
private _setRelatedContext(): void {
const context: RelatedContextItem | undefined = this.sceneId
? this._registryEntry?.area_id
? {
itemType: "area",
itemId: this._registryEntry.area_id,
}
: this._scene
? {
itemType: "scene",
itemId: this._scene.entity_id,
}
: undefined
: undefined;
if (
context?.itemType === this._relatedContext?.itemType &&
context?.itemId === this._relatedContext?.itemId
) {
return;
}
this._relatedContext = context;
fireEvent(this, "hass-related-context", context);
}
private _handleMenuAction(ev: HaDropdownSelectEvent) {
+1 -20
View File
@@ -6,8 +6,6 @@ import "../../components/ha-alert";
import "../../components/ha-icon-button-arrow-prev";
import "../../components/ha-menu-button";
import "../../components/ha-top-app-bar-fixed";
import type { EnergyFrontendSystemData } from "../../data/frontend";
import { fetchFrontendSystemData } from "../../data/frontend";
import type { LovelaceConfig } from "../../data/lovelace/config/types";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant, PanelInfo } from "../../types";
@@ -28,8 +26,6 @@ class PanelEnergy extends LitElement {
@state() private _lovelace?: Lovelace;
@state() private _config: EnergyFrontendSystemData = {};
@property({ attribute: false }) public route?: {
path: string;
prefix: string;
@@ -62,23 +58,10 @@ class PanelEnergy extends LitElement {
await Promise.all([
this.hass.loadFragmentTranslation("lovelace"),
this.hass.loadFragmentTranslation("energy"),
this._loadSystemData(),
]);
this._loadConfig();
}
private async _loadSystemData() {
try {
const data = await fetchFrontendSystemData(
this.hass.connection,
"energy"
);
this._config = data || {};
} catch (_err) {
this._config = {};
}
}
private async _loadConfig() {
try {
this._error = undefined;
@@ -111,7 +94,6 @@ class PanelEnergy extends LitElement {
this.route?.path === "/now"
? DEFAULT_POWER_COLLECTION_KEY
: undefined,
hidden_cards: this._config.hidden_cards,
},
},
this.hass
@@ -182,8 +164,7 @@ class PanelEnergy extends LitElement {
navigate(`/config/energy/${tab}?historyBack=1`);
}
private async _reloadConfig() {
await this._loadSystemData();
private _reloadConfig() {
this._loadConfig();
}
@@ -1,296 +0,0 @@
import type { LocalizeKeys } from "../../../common/translations/localize";
import type {
EnergyPreferences,
GridSourceTypeEnergyPreference,
} from "../../../data/energy";
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
/** Strategy config shared by the per-view energy strategies. */
export interface EnergyViewStrategyConfig extends LovelaceStrategyConfig {
collection_key?: string;
hidden_cards?: string[];
}
export type EnergyViewPath =
| "overview"
| "electricity"
| "gas"
| "water"
| "now";
// --- Applicability helpers -------------------------------------------------
// These mirror, one-to-one, the conditions the individual view strategies use
// to decide whether to emit a card. The catalog and the strategies must agree
// on what "applicable" means, so the conditions live here and are reused.
export const hasGridSource = (prefs: EnergyPreferences): boolean =>
prefs.energy_sources.some(
(source): source is GridSourceTypeEnergyPreference =>
source.type === "grid" &&
(!!source.stat_energy_from || !!source.stat_energy_to)
);
export const hasReturn = (prefs: EnergyPreferences): boolean =>
prefs.energy_sources.some(
(source) => source.type === "grid" && !!source.stat_energy_to
);
export const hasSolar = (prefs: EnergyPreferences): boolean =>
prefs.energy_sources.some((source) => source.type === "solar");
export const hasBattery = (prefs: EnergyPreferences): boolean =>
prefs.energy_sources.some((source) => source.type === "battery");
export const hasGasSource = (prefs: EnergyPreferences): boolean =>
prefs.energy_sources.some((source) => source.type === "gas");
export const hasWaterSource = (prefs: EnergyPreferences): boolean =>
prefs.energy_sources.some((source) => source.type === "water");
export const hasWaterDevices = (prefs: EnergyPreferences): boolean =>
(prefs.device_consumption_water?.length ?? 0) > 0;
export const hasDeviceConsumption = (prefs: EnergyPreferences): boolean =>
prefs.device_consumption.length > 0;
export const hasPowerSources = (prefs: EnergyPreferences): boolean =>
prefs.energy_sources.some((source) => {
if (source.type === "solar" && source.stat_rate) return true;
if (source.type === "battery" && source.stat_rate) return true;
if (source.type === "grid") {
return !!source.stat_rate || !!source.power_config;
}
return false;
});
export const hasPowerDevices = (prefs: EnergyPreferences): boolean =>
prefs.device_consumption.some((device) => device.stat_rate);
export const hasPowerWaterDevices = (prefs: EnergyPreferences): boolean =>
(prefs.device_consumption_water ?? []).some((device) => device.stat_rate);
// --- Card catalog ----------------------------------------------------------
export interface EnergyCardCatalogEntry {
/** Stable identifier and storage token: `<view>.<cardType>`. */
key: string;
view: EnergyViewPath;
/** Localize key for the label shown in the customise dialog. */
labelKey: LocalizeKeys;
/** Whether this card is emitted for the given preferences. */
isApplicable: (prefs: EnergyPreferences) => boolean;
}
export const energyCardKey = (view: EnergyViewPath, cardType: string): string =>
`${view}.${cardType}`;
const entry = (
view: EnergyViewPath,
cardType: string,
labelKey: LocalizeKeys,
isApplicable: (prefs: EnergyPreferences) => boolean
): EnergyCardCatalogEntry => ({
key: energyCardKey(view, cardType),
view,
labelKey,
isApplicable,
});
export const ENERGY_CARD_CATALOG: readonly EnergyCardCatalogEntry[] = [
// --- Overview ---
entry(
"overview",
"energy-distribution",
"ui.panel.energy.cards.energy_distribution_title",
(p) => hasGridSource(p) || hasBattery(p) || hasSolar(p)
),
entry(
"overview",
"energy-sources-table",
"ui.panel.energy.cards.energy_sources_table_title",
(p) => p.energy_sources.length > 0
),
entry(
"overview",
"power-sources-graph",
"ui.panel.energy.cards.power_sources_graph_title",
(p) => hasPowerSources(p)
),
entry(
"overview",
"energy-usage-graph",
"ui.panel.energy.cards.energy_usage_graph_title",
(p) => hasGridSource(p) || hasBattery(p)
),
entry(
"overview",
"energy-gas-graph",
"ui.panel.energy.cards.energy_gas_graph_title",
(p) => hasGasSource(p)
),
// One toggle gates the water row, which renders energy-water-graph (sources)
// or, with only water devices, water-sankey.
entry(
"overview",
"energy-water-graph",
"ui.panel.energy.cards.energy_water_graph_title",
(p) => hasWaterSource(p) || hasWaterDevices(p)
),
// --- Electricity ---
entry(
"electricity",
"energy-distribution",
"ui.panel.energy.cards.energy_distribution_title",
(p) => hasGridSource(p) || hasBattery(p) || hasSolar(p)
),
entry(
"electricity",
"energy-grid-balance",
"ui.panel.energy.cards.energy_grid_balance_title",
(p) => hasGridSource(p) && hasReturn(p)
),
entry(
"electricity",
"energy-grid-neutrality-gauge",
"ui.panel.energy.cards.energy_grid_neutrality_gauge_title",
(p) => hasReturn(p)
),
entry(
"electricity",
"energy-solar-consumed-gauge",
"ui.panel.energy.cards.energy_solar_consumed_gauge_title",
(p) => hasSolar(p) && hasReturn(p)
),
entry(
"electricity",
"energy-self-sufficiency-gauge",
"ui.panel.energy.cards.energy_self_sufficiency_gauge_title",
(p) => hasSolar(p) && hasGridSource(p)
),
entry(
"electricity",
"energy-carbon-consumed-gauge",
"ui.panel.energy.cards.energy_carbon_consumed_gauge_title",
(p) => hasGridSource(p)
),
entry(
"electricity",
"energy-usage-graph",
"ui.panel.energy.cards.energy_usage_graph_title",
(p) => hasGridSource(p) || hasBattery(p)
),
entry(
"electricity",
"energy-solar-graph",
"ui.panel.energy.cards.energy_solar_graph_title",
(p) => hasSolar(p)
),
entry(
"electricity",
"energy-sources-table",
"ui.panel.energy.cards.energy_sources_table_title",
(p) => hasGridSource(p) || hasSolar(p) || hasBattery(p)
),
entry(
"electricity",
"energy-devices-detail-graph",
"ui.panel.energy.cards.energy_devices_detail_graph_title",
(p) => hasDeviceConsumption(p)
),
entry(
"electricity",
"energy-devices-graph",
"ui.panel.energy.cards.energy_devices_graph_title",
(p) => hasDeviceConsumption(p)
),
entry(
"electricity",
"energy-sankey",
"ui.panel.energy.cards.energy_sankey_title",
(p) => hasDeviceConsumption(p)
),
// --- Gas ---
entry(
"gas",
"energy-gas-graph",
"ui.panel.energy.cards.energy_gas_graph_title",
(p) => hasGasSource(p)
),
entry(
"gas",
"energy-sources-table",
"ui.panel.energy.cards.energy_sources_table_title",
(p) => hasGasSource(p)
),
// --- Water ---
entry(
"water",
"energy-water-graph",
"ui.panel.energy.cards.energy_water_graph_title",
(p) => hasWaterSource(p)
),
entry(
"water",
"energy-sources-table",
"ui.panel.energy.cards.energy_sources_table_title",
(p) => hasWaterSource(p)
),
entry(
"water",
"water-sankey",
"ui.panel.energy.cards.water_sankey_title",
(p) => hasWaterDevices(p)
),
// --- Now (power) ---
entry(
"now",
"power-sources-graph",
"ui.panel.energy.cards.power_sources_graph_title",
(p) => hasPowerSources(p)
),
entry(
"now",
"power-sankey",
"ui.panel.energy.cards.power_sankey_title",
(p) => hasPowerDevices(p)
),
entry(
"now",
"water-flow-sankey",
"ui.panel.energy.cards.water_flow_sankey_title",
(p) => hasPowerWaterDevices(p)
),
];
// --- Lookup helpers --------------------------------------------------------
export const isEnergyCardHidden = (
view: EnergyViewPath,
cardType: string,
hidden: string[] | undefined
): boolean => !!hidden?.includes(energyCardKey(view, cardType));
/** Keys of all catalog cards that apply to the given preferences for a view. */
export const applicableEnergyCardKeys = (
view: EnergyViewPath,
prefs: EnergyPreferences
): string[] =>
ENERGY_CARD_CATALOG.filter(
(c) => c.view === view && c.isApplicable(prefs)
).map((c) => c.key);
/** True when a view has applicable cards but every one of them is hidden. */
export const isEnergyViewEmpty = (
view: EnergyViewPath,
prefs: EnergyPreferences,
hidden: string[] | undefined
): boolean => {
const applicable = applicableEnergyCardKeys(view, prefs);
return (
applicable.length > 0 && applicable.every((key) => hidden?.includes(key))
);
};
@@ -7,7 +7,7 @@ import {
import type { EnergyPreferences } from "../../../data/energy";
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
import type { LovelaceConfig } from "../../../data/lovelace/config/types";
import type { LovelaceStrategyViewConfig } from "../../../data/lovelace/config/view";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { LocalizeKeys } from "../../../common/translations/localize";
import type { HomeAssistant } from "../../../types";
import type { LovelaceStrategyDependency } from "../../lovelace/strategies/types";
@@ -15,8 +15,6 @@ import {
DEFAULT_ENERGY_COLLECTION_KEY,
DEFAULT_POWER_COLLECTION_KEY,
} from "../constants";
import type { EnergyViewPath } from "./energy-cards";
import { isEnergyViewEmpty } from "./energy-cards";
const OVERVIEW_VIEW = {
path: "overview",
@@ -24,7 +22,7 @@ const OVERVIEW_VIEW = {
type: "energy-overview",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
},
} as LovelaceStrategyViewConfig;
} as LovelaceViewConfig;
const ENERGY_VIEW = {
path: "electricity",
@@ -32,7 +30,7 @@ const ENERGY_VIEW = {
type: "energy",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
},
} as LovelaceStrategyViewConfig;
} as LovelaceViewConfig;
const WATER_VIEW = {
path: "water",
@@ -40,7 +38,7 @@ const WATER_VIEW = {
type: "water",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
},
} as LovelaceStrategyViewConfig;
} as LovelaceViewConfig;
const GAS_VIEW = {
path: "gas",
@@ -48,7 +46,7 @@ const GAS_VIEW = {
type: "gas",
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
},
} as LovelaceStrategyViewConfig;
} as LovelaceViewConfig;
const POWER_VIEW = {
path: "now",
@@ -56,7 +54,7 @@ const POWER_VIEW = {
type: "power",
collection_key: DEFAULT_POWER_COLLECTION_KEY,
},
} as LovelaceStrategyViewConfig;
} as LovelaceViewConfig;
const WIZARD_VIEW = {
type: "panel",
@@ -67,7 +65,6 @@ const WIZARD_VIEW = {
export interface EnergyDashboardStrategyConfig extends LovelaceStrategyConfig {
type: "energy";
default_collection?: string;
hidden_cards?: string[];
}
@customElement("energy-dashboard-strategy")
@@ -118,42 +115,28 @@ export class EnergyDashboardStrategy extends ReactiveElement {
const hasDeviceConsumption = prefs.device_consumption.length > 0;
const hidden = _config.hidden_cards;
const candidateViews: LovelaceStrategyViewConfig[] = [];
const views: LovelaceViewConfig[] = [];
if (hasEnergy || hasDeviceConsumption) {
candidateViews.push(ENERGY_VIEW);
views.push(ENERGY_VIEW);
}
if (hasGas) {
candidateViews.push(GAS_VIEW);
views.push(GAS_VIEW);
}
if (hasWater) {
candidateViews.push(WATER_VIEW);
views.push(WATER_VIEW);
}
if (hasPower) {
candidateViews.push(POWER_VIEW);
views.push(POWER_VIEW);
}
if (
hasPowerSource ||
[hasEnergy, hasGas, hasWater].filter(Boolean).length > 1
) {
candidateViews.unshift(OVERVIEW_VIEW);
views.unshift(OVERVIEW_VIEW);
}
// Drop a view (tab) when every card it would render has been hidden, so we
// don't show an empty tab. Keep at least one view so the dashboard never
// renders blank and the customize entry stays reachable.
let views = candidateViews.filter(
(view) => !isEnergyViewEmpty(view.path as EnergyViewPath, prefs, hidden)
);
if (views.length === 0) {
views = candidateViews;
}
return {
views: views.map((view) => ({
...view,
strategy: { ...view.strategy, hidden_cards: hidden },
title:
view.title ||
hass.localize(`ui.panel.energy.title.${view.path}` as LocalizeKeys),
@@ -4,22 +4,20 @@ import type { GridSourceTypeEnergyPreference } from "../../../data/energy";
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 type { LovelaceStrategyDependency } from "../../lovelace/strategies/types";
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../constants";
import type { EnergyViewStrategyConfig } from "./energy-cards";
import { isEnergyCardHidden } from "./energy-cards";
@customElement("energy-overview-view-strategy")
export class EnergyOverviewViewStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
static async generate(
_config: EnergyViewStrategyConfig,
_config: LovelaceStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const collectionKey =
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
const hidden = _config.hidden_cards;
const view: LovelaceViewConfig = {
type: "sections",
@@ -78,10 +76,7 @@ export class EnergyOverviewViewStrategy extends ReactiveElement {
return false;
});
if (
(hasGrid || hasBattery || hasSolar) &&
!isEnergyCardHidden("overview", "energy-distribution", hidden)
) {
if (hasGrid || hasBattery || hasSolar) {
view.sections!.push({
type: "grid",
cards: [
@@ -96,10 +91,7 @@ export class EnergyOverviewViewStrategy extends ReactiveElement {
});
}
if (
prefs.energy_sources.length &&
!isEnergyCardHidden("overview", "energy-sources-table", hidden)
) {
if (prefs.energy_sources.length) {
view.sections!.push({
type: "grid",
cards: [
@@ -115,10 +107,7 @@ export class EnergyOverviewViewStrategy extends ReactiveElement {
});
}
if (
hasPowerSources &&
!isEnergyCardHidden("overview", "power-sources-graph", hidden)
) {
if (hasPowerSources) {
view.sections!.push({
type: "grid",
cards: [
@@ -134,10 +123,7 @@ export class EnergyOverviewViewStrategy extends ReactiveElement {
});
}
if (
(hasGrid || hasBattery) &&
!isEnergyCardHidden("overview", "energy-usage-graph", hidden)
) {
if (hasGrid || hasBattery) {
view.sections!.push({
type: "grid",
cards: [
@@ -152,7 +138,7 @@ export class EnergyOverviewViewStrategy extends ReactiveElement {
});
}
if (hasGas && !isEnergyCardHidden("overview", "energy-gas-graph", hidden)) {
if (hasGas) {
view.sections!.push({
type: "grid",
cards: [
@@ -167,10 +153,7 @@ export class EnergyOverviewViewStrategy extends ReactiveElement {
});
}
if (
(hasWaterSources || hasWaterDevices) &&
!isEnergyCardHidden("overview", "energy-water-graph", hidden)
) {
if (hasWaterSources || hasWaterDevices) {
view.sections!.push({
type: "grid",
cards: [
@@ -3,11 +3,10 @@ import { customElement } from "lit/decorators";
import type { GridSourceTypeEnergyPreference } from "../../../data/energy";
import { getEnergyDataCollection } from "../../../data/energy";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../constants";
import type { EnergyViewStrategyConfig } from "./energy-cards";
import { isEnergyCardHidden } from "./energy-cards";
import { shouldShowFloorsAndAreas } from "./show-floors-and-areas";
import {
LARGE_SCREEN_CONDITION,
@@ -20,12 +19,11 @@ export class EnergyViewStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
static async generate(
_config: EnergyViewStrategyConfig,
_config: LovelaceStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const collectionKey =
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
const hidden = _config.hidden_cards;
const view: LovelaceViewConfig = {
type: "sections",
@@ -80,10 +78,7 @@ export class EnergyViewStrategy extends ReactiveElement {
const gaugeCards: LovelaceCardConfig[] = [];
const sidebarSection = view.sidebar!.sections![0];
if (
(hasGrid || hasBattery || hasSolar) &&
!isEnergyCardHidden("electricity", "energy-distribution", hidden)
) {
if (hasGrid || hasBattery || hasSolar) {
const distributionCard = {
title: hass.localize("ui.panel.energy.cards.energy_distribution_title"),
type: "energy-distribution",
@@ -99,11 +94,7 @@ export class EnergyViewStrategy extends ReactiveElement {
}
// Only include if we have both grid import and export configured
if (
hasGrid &&
hasReturn &&
!isEnergyCardHidden("electricity", "energy-grid-balance", hidden)
) {
if (hasGrid && hasReturn) {
const gridResultCard = {
type: "energy-grid-balance",
collection_key: collectionKey,
@@ -118,10 +109,7 @@ export class EnergyViewStrategy extends ReactiveElement {
}
// Only include if we have a grid source & return.
if (
hasReturn &&
!isEnergyCardHidden("electricity", "energy-grid-neutrality-gauge", hidden)
) {
if (hasReturn) {
const card = {
type: "energy-grid-neutrality-gauge",
collection_key: collectionKey,
@@ -131,28 +119,14 @@ export class EnergyViewStrategy extends ReactiveElement {
// Only include if we have a solar source.
if (hasSolar) {
if (
hasReturn &&
!isEnergyCardHidden(
"electricity",
"energy-solar-consumed-gauge",
hidden
)
) {
if (hasReturn) {
const card = {
type: "energy-solar-consumed-gauge",
collection_key: collectionKey,
};
gaugeCards.push(card);
}
if (
hasGrid &&
!isEnergyCardHidden(
"electricity",
"energy-self-sufficiency-gauge",
hidden
)
) {
if (hasGrid) {
const card = {
type: "energy-self-sufficiency-gauge",
collection_key: collectionKey,
@@ -162,10 +136,7 @@ export class EnergyViewStrategy extends ReactiveElement {
}
// Only include if we have a grid
if (
hasGrid &&
!isEnergyCardHidden("electricity", "energy-carbon-consumed-gauge", hidden)
) {
if (hasGrid) {
const card = {
type: "energy-carbon-consumed-gauge",
collection_key: collectionKey,
@@ -200,10 +171,7 @@ export class EnergyViewStrategy extends ReactiveElement {
});
// Only include if we have a grid or battery.
if (
(hasGrid || hasBattery) &&
!isEnergyCardHidden("electricity", "energy-usage-graph", hidden)
) {
if (hasGrid || hasBattery) {
mainCards.push({
title: hass.localize("ui.panel.energy.cards.energy_usage_graph_title"),
type: "energy-usage-graph",
@@ -213,10 +181,7 @@ export class EnergyViewStrategy extends ReactiveElement {
}
// Only include if we have a solar source.
if (
hasSolar &&
!isEnergyCardHidden("electricity", "energy-solar-graph", hidden)
) {
if (hasSolar) {
mainCards.push({
title: hass.localize("ui.panel.energy.cards.energy_solar_graph_title"),
type: "energy-solar-graph",
@@ -225,10 +190,7 @@ export class EnergyViewStrategy extends ReactiveElement {
});
}
if (
(hasGrid || hasSolar || hasBattery) &&
!isEnergyCardHidden("electricity", "energy-sources-table", hidden)
) {
if (hasGrid || hasSolar || hasBattery) {
mainCards.push({
title: hass.localize(
"ui.panel.energy.cards.energy_sources_table_title"
@@ -242,47 +204,35 @@ export class EnergyViewStrategy extends ReactiveElement {
// Only include if we have at least 1 device in the config.
if (prefs.device_consumption.length) {
if (
!isEnergyCardHidden(
"electricity",
"energy-devices-detail-graph",
hidden
)
) {
mainCards.push({
title: hass.localize(
"ui.panel.energy.cards.energy_devices_detail_graph_title"
),
type: "energy-devices-detail-graph",
collection_key: collectionKey,
grid_options: { columns: 36 },
});
}
if (!isEnergyCardHidden("electricity", "energy-devices-graph", hidden)) {
mainCards.push({
title: hass.localize(
"ui.panel.energy.cards.energy_devices_graph_title"
),
type: "energy-devices-graph",
collection_key: collectionKey,
grid_options: { columns: 36 },
});
}
if (!isEnergyCardHidden("electricity", "energy-sankey", hidden)) {
const showFloorsAndAreas = shouldShowFloorsAndAreas(
prefs.device_consumption,
hass,
(d) => d.stat_consumption
);
mainCards.push({
title: hass.localize("ui.panel.energy.cards.energy_sankey_title"),
type: "energy-sankey",
collection_key: collectionKey,
group_by_floor: showFloorsAndAreas,
group_by_area: showFloorsAndAreas,
grid_options: { columns: 36 },
});
}
const showFloorsAndAreas = shouldShowFloorsAndAreas(
prefs.device_consumption,
hass,
(d) => d.stat_consumption
);
mainCards.push({
title: hass.localize(
"ui.panel.energy.cards.energy_devices_detail_graph_title"
),
type: "energy-devices-detail-graph",
collection_key: collectionKey,
grid_options: { columns: 36 },
});
mainCards.push({
title: hass.localize(
"ui.panel.energy.cards.energy_devices_graph_title"
),
type: "energy-devices-graph",
collection_key: collectionKey,
grid_options: { columns: 36 },
});
mainCards.push({
title: hass.localize("ui.panel.energy.cards.energy_sankey_title"),
type: "energy-sankey",
collection_key: collectionKey,
group_by_floor: showFloorsAndAreas,
group_by_area: showFloorsAndAreas,
grid_options: { columns: 36 },
});
}
view.sections!.push({
@@ -3,9 +3,8 @@ 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 "../constants";
import type { EnergyViewStrategyConfig } from "./energy-cards";
import { isEnergyCardHidden } from "./energy-cards";
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
import type { LovelaceStrategyDependency } from "../../lovelace/strategies/types";
@@ -14,12 +13,11 @@ export class GasViewStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
static async generate(
_config: EnergyViewStrategyConfig,
_config: LovelaceStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const collectionKey =
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
const hidden = _config.hidden_cards;
const view: LovelaceViewConfig = {
type: "sections",
@@ -62,30 +60,24 @@ export class GasViewStrategy extends ReactiveElement {
},
});
if (!isEnergyCardHidden("gas", "energy-gas-graph", hidden)) {
section.cards!.push({
title: hass.localize("ui.panel.energy.cards.energy_gas_graph_title"),
type: "energy-gas-graph",
collection_key: collectionKey,
grid_options: {
columns: 24,
},
});
}
section.cards!.push({
title: hass.localize("ui.panel.energy.cards.energy_gas_graph_title"),
type: "energy-gas-graph",
collection_key: collectionKey,
grid_options: {
columns: 24,
},
});
if (!isEnergyCardHidden("gas", "energy-sources-table", hidden)) {
section.cards!.push({
title: hass.localize(
"ui.panel.energy.cards.energy_sources_table_title"
),
type: "energy-sources-table",
collection_key: collectionKey,
types: ["gas"],
grid_options: {
columns: 12,
},
});
}
section.cards!.push({
title: hass.localize("ui.panel.energy.cards.energy_sources_table_title"),
type: "energy-sources-table",
collection_key: collectionKey,
types: ["gas"],
grid_options: {
columns: 12,
},
});
return view;
}
@@ -1,11 +1,10 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import { getEnergyDataCollection } from "../../../data/energy";
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../constants";
import type { EnergyViewStrategyConfig } from "./energy-cards";
import { isEnergyCardHidden } from "./energy-cards";
import { shouldShowFloorsAndAreas } from "./show-floors-and-areas";
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
@@ -16,12 +15,11 @@ export class PowerViewStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
static async generate(
_config: EnergyViewStrategyConfig,
_config: LovelaceStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const collectionKey =
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
const hidden = _config.hidden_cards;
const energyCollection = getEnergyDataCollection(hass, {
key: collectionKey,
@@ -81,18 +79,14 @@ export class PowerViewStrategy extends ReactiveElement {
collection_key: collectionKey,
});
if (!isEnergyCardHidden("now", "power-sources-graph", hidden)) {
chartsSection.cards!.push({
title: hass.localize(
"ui.panel.energy.cards.power_sources_graph_title"
),
type: "power-sources-graph",
collection_key: collectionKey,
grid_options: {
columns: 36,
},
});
}
chartsSection.cards!.push({
title: hass.localize("ui.panel.energy.cards.power_sources_graph_title"),
type: "power-sources-graph",
collection_key: collectionKey,
grid_options: {
columns: 36,
},
});
}
if (hasGasSources) {
@@ -118,7 +112,7 @@ export class PowerViewStrategy extends ReactiveElement {
}
});
if (hasPowerDevices && !isEnergyCardHidden("now", "power-sankey", hidden)) {
if (hasPowerDevices) {
const showFloorsAndAreas = shouldShowFloorsAndAreas(
prefs.device_consumption,
hass,
@@ -136,10 +130,7 @@ export class PowerViewStrategy extends ReactiveElement {
});
}
if (
hasWaterDevices &&
!isEnergyCardHidden("now", "water-flow-sankey", hidden)
) {
if (hasWaterDevices) {
const showFloorsAndAreas = shouldShowFloorsAndAreas(
prefs.device_consumption_water,
hass,
@@ -2,12 +2,11 @@ import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import { getEnergyDataCollection } from "../../../data/energy";
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
import type { LovelaceStrategyDependency } from "../../lovelace/strategies/types";
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../constants";
import type { EnergyViewStrategyConfig } from "./energy-cards";
import { isEnergyCardHidden } from "./energy-cards";
import { shouldShowFloorsAndAreas } from "./show-floors-and-areas";
@customElement("water-view-strategy")
@@ -15,12 +14,11 @@ export class WaterViewStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
static async generate(
_config: EnergyViewStrategyConfig,
_config: LovelaceStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const collectionKey =
_config.collection_key || DEFAULT_ENERGY_COLLECTION_KEY;
const hidden = _config.hidden_cards;
const view: LovelaceViewConfig = {
type: "sections",
@@ -65,38 +63,29 @@ export class WaterViewStrategy extends ReactiveElement {
});
if (hasWaterSources) {
if (!isEnergyCardHidden("water", "energy-water-graph", hidden)) {
section.cards!.push({
title: hass.localize(
"ui.panel.energy.cards.energy_water_graph_title"
),
type: "energy-water-graph",
collection_key: collectionKey,
grid_options: {
columns: 24,
},
});
}
if (!isEnergyCardHidden("water", "energy-sources-table", hidden)) {
section.cards!.push({
title: hass.localize(
"ui.panel.energy.cards.energy_sources_table_title"
),
type: "energy-sources-table",
collection_key: collectionKey,
types: ["water"],
grid_options: {
columns: 12,
},
});
}
section.cards!.push({
title: hass.localize("ui.panel.energy.cards.energy_water_graph_title"),
type: "energy-water-graph",
collection_key: collectionKey,
grid_options: {
columns: 24,
},
});
section.cards!.push({
title: hass.localize(
"ui.panel.energy.cards.energy_sources_table_title"
),
type: "energy-sources-table",
collection_key: collectionKey,
types: ["water"],
grid_options: {
columns: 12,
},
});
}
// Only include if we have at least 1 water device in the config.
if (
hasWaterDevices &&
!isEnergyCardHidden("water", "water-sankey", hidden)
) {
if (hasWaterDevices) {
const showFloorsAndAreas = shouldShowFloorsAndAreas(
prefs.device_consumption_water,
hass,
+4 -2
View File
@@ -15,7 +15,6 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { getEntityLocation } from "../../../common/entity/get_entity_location";
import { deepEqual } from "../../../common/util/deep-equal";
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
import "../../../components/ha-alert";
@@ -91,7 +90,10 @@ class HuiMapCard extends LitElement implements LovelaceCard {
const personSources = new Set<string>();
const locationEntities: string[] = [];
Object.values(hass.states).forEach((entity) => {
if (!getEntityLocation(entity, hass.states)) {
if (
!("latitude" in entity.attributes) ||
!("longitude" in entity.attributes)
) {
return;
}
locationEntities.push(entity.entity_id);
@@ -16,11 +16,10 @@ import {
union,
} from "superstruct";
import { ContextProvider } from "@lit/context";
import type { HassEntity } from "home-assistant-js-websocket";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeDomain } from "../../../../common/entity/compute_domain";
import { getEntityLocation } from "../../../../common/entity/get_entity_location";
import { hasLocation } from "../../../../common/entity/has_location";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import { orderProperties } from "../../../../common/util/order-properties";
import "../../../../components/ha-form/ha-form";
@@ -276,13 +275,10 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
this._locationEntities = !this.hass
? []
: Object.keys(this.hass!.states).filter((entity_id) =>
getEntityLocation(this.hass!.states[entity_id], this.hass!.states)
hasLocation(this.hass!.states[entity_id])
);
}
private _entityHasLocation = (stateObj: HassEntity) =>
!!getEntityLocation(stateObj, this.hass!.states);
protected render() {
if (!this.hass || !this._config) {
return nothing;
@@ -324,7 +320,7 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
<hui-entity-editor
.hass=${this.hass}
.entities=${configEntities}
.entityFilter=${this._entityHasLocation}
.entityFilter=${hasLocation}
can-edit
.required=${!this._config.show_all}
@entities-changed=${this._entitiesValueChanged}
@@ -11,11 +11,6 @@ import type {
MediaPlayerSoundModeCardFeatureConfig,
} from "../../card-features/types";
import type { LovelaceCardFeatureEditor } from "../../types";
import {
customizableListData,
customizableListSchema,
processCustomizableListValue,
} from "./customizable-list-feature";
@customElement("hui-media-player-sound-mode-card-feature-editor")
export class HuiMediaPlayerSoundModeCardFeatureEditor
@@ -33,20 +28,28 @@ export class HuiMediaPlayerSoundModeCardFeatureEditor
}
private _schema = memoizeOne(
(stateObj: MediaPlayerEntity | undefined, customize: boolean) =>
customizableListSchema({
field: "sound_modes",
customize,
options:
stateObj?.attributes.sound_mode_list?.map((mode) => ({
value: mode,
label: this.hass!.formatEntityAttributeValue(
stateObj,
"sound_mode",
mode
),
})) ?? [],
})
(hass: HomeAssistant, stateObj?: MediaPlayerEntity) =>
[
{
name: "sound_modes",
selector: {
select: {
multiple: true,
mode: "list" as const,
reorder: true,
options:
stateObj?.attributes.sound_mode_list?.map((mode) => ({
value: mode,
label: hass.formatEntityAttributeValue(
stateObj,
"sound_mode",
mode
),
})) ?? [],
},
},
},
] as const
);
protected render() {
@@ -60,13 +63,12 @@ export class HuiMediaPlayerSoundModeCardFeatureEditor
| undefined)
: undefined;
const data = customizableListData(this._config, "sound_modes");
const schema = this._schema(stateObj, data.customize);
const schema = this._schema(this.hass!, stateObj);
return html`
<ha-form
.hass=${this.hass}
.data=${data}
.data=${this._config}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
@@ -77,27 +79,21 @@ export class HuiMediaPlayerSoundModeCardFeatureEditor
private _valueChanged(
ev: ValueChangedEvent<MediaPlayerSoundModeCardFeatureConfig>
): void {
const stateObj = this.context?.entity_id
? (this.hass!.states[this.context.entity_id] as
| MediaPlayerEntity
| undefined)
: undefined;
const defaults = stateObj?.attributes.sound_mode_list ?? [];
const config =
processCustomizableListValue<MediaPlayerSoundModeCardFeatureConfig>(
ev.detail.value,
"sound_modes",
defaults
);
fireEvent(this, "config-changed", { config });
fireEvent(this, "config-changed", { config: ev.detail.value });
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) =>
this.hass!.localize(
`ui.panel.lovelace.editor.features.types.media-player-sound-mode.${schema.name}`
);
) => {
switch (schema.name) {
case "sound_modes":
return this.hass?.localize(
`ui.panel.lovelace.editor.features.types.media-player-sound-mode.${schema.name}`
);
default:
return "";
}
};
}
declare global {
@@ -11,11 +11,6 @@ import type {
MediaPlayerSourceCardFeatureConfig,
} from "../../card-features/types";
import type { LovelaceCardFeatureEditor } from "../../types";
import {
customizableListData,
customizableListSchema,
processCustomizableListValue,
} from "./customizable-list-feature";
@customElement("hui-media-player-source-card-feature-editor")
export class HuiMediaPlayerSourceCardFeatureEditor
@@ -33,20 +28,24 @@ export class HuiMediaPlayerSourceCardFeatureEditor
}
private _schema = memoizeOne(
(stateObj: MediaPlayerEntity | undefined, customize: boolean) =>
customizableListSchema({
field: "sources",
customize,
options:
stateObj?.attributes.source_list?.map((source) => ({
value: source,
label: this.hass!.formatEntityAttributeValue(
stateObj,
"source",
source
),
})) ?? [],
})
(stateObj?: MediaPlayerEntity) =>
[
{
name: "sources",
selector: {
select: {
multiple: true,
mode: "list" as const,
reorder: true,
options:
stateObj?.attributes.source_list?.map((source) => ({
value: source,
label: source,
})) ?? [],
},
},
},
] as const
);
protected render() {
@@ -60,13 +59,12 @@ export class HuiMediaPlayerSourceCardFeatureEditor
| undefined)
: undefined;
const data = customizableListData(this._config, "sources");
const schema = this._schema(stateObj, data.customize);
const schema = this._schema(stateObj);
return html`
<ha-form
.hass=${this.hass}
.data=${data}
.data=${this._config}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
@@ -77,27 +75,21 @@ export class HuiMediaPlayerSourceCardFeatureEditor
private _valueChanged(
ev: ValueChangedEvent<MediaPlayerSourceCardFeatureConfig>
): void {
const stateObj = this.context?.entity_id
? (this.hass!.states[this.context.entity_id] as
| MediaPlayerEntity
| undefined)
: undefined;
const defaults = stateObj?.attributes.source_list ?? [];
const config =
processCustomizableListValue<MediaPlayerSourceCardFeatureConfig>(
ev.detail.value,
"sources",
defaults
);
fireEvent(this, "config-changed", { config });
fireEvent(this, "config-changed", { config: ev.detail.value });
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) =>
this.hass!.localize(
`ui.panel.lovelace.editor.features.types.media-player-source.${schema.name}`
);
) => {
switch (schema.name) {
case "sources":
return this.hass?.localize(
`ui.panel.lovelace.editor.features.types.media-player-source.${schema.name}`
);
default:
return "";
}
};
}
declare global {
+5 -5
View File
@@ -3,7 +3,6 @@ import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { getEntityLocation } from "../../common/entity/get_entity_location";
import { navigate } from "../../common/navigate";
import "../../components/ha-icon-button";
import "../../components/ha-menu-button";
@@ -65,10 +64,11 @@ class HaPanelMap extends LitElement {
const personSources = new Set<string>();
const locationEntities: string[] = [];
Object.values(this.hass!.states).forEach((entity) => {
if (entity.state === "home") {
return;
}
if (!getEntityLocation(entity, this.hass!.states)) {
if (
entity.state === "home" ||
!("latitude" in entity.attributes) ||
!("longitude" in entity.attributes)
) {
return;
}
locationEntities.push(entity.entity_id);
+3 -21
View File
@@ -4139,17 +4139,6 @@
"gas": "Gas",
"water": "Water"
},
"customise": {
"toolbar_action": "Customize cards",
"title": "Customize energy",
"saved": "Energy dashboard updated",
"save_failed": "Failed to save energy customization",
"unavailable": "This card isn't shown because the energy source or device it needs isn't configured.",
"groups": {
"overview": "Overview",
"now": "Now"
}
},
"delete_source": "Are you sure you want to remove this source?",
"delete_integration": "Are you sure you want to remove this integration? It will remove the entities it provides",
"grid": {
@@ -10160,13 +10149,11 @@
},
"media-player-sound-mode": {
"label": "Media player sound mode",
"sound_modes": "Sound modes",
"customize": "Customize sound modes"
"sound_modes": "Sound modes"
},
"media-player-source": {
"label": "Media player source",
"sources": "Sources",
"customize": "Customize sources"
"sources": "Sources"
},
"media-player-volume-buttons": {
"label": "Media player volume buttons",
@@ -11207,12 +11194,7 @@
"energy_top_consumers_title": "Top consumers",
"power_sankey_title": "Current power flow",
"water_flow_sankey_title": "Current water flow",
"power_sources_graph_title": "Power sources",
"energy_grid_balance_title": "Grid energy balance",
"energy_grid_neutrality_gauge_title": "Grid neutrality gauge",
"energy_solar_consumed_gauge_title": "Solar consumed gauge",
"energy_self_sufficiency_gauge_title": "Self-sufficiency gauge",
"energy_carbon_consumed_gauge_title": "Carbon consumed gauge"
"power_sources_graph_title": "Power sources"
}
},
"history": {
@@ -1,130 +0,0 @@
import { describe, expect, it } from "vitest";
import type {
EnergyPreferences,
EnergySource,
} from "../../../../src/data/energy";
import {
applicableEnergyCardKeys,
ENERGY_CARD_CATALOG,
energyCardKey,
isEnergyCardHidden,
isEnergyViewEmpty,
} from "../../../../src/panels/energy/strategies/energy-cards";
const source = (s: Partial<EnergySource> & { type: string }): EnergySource =>
s as unknown as EnergySource;
const makePrefs = (
prefs: Partial<EnergyPreferences> = {}
): EnergyPreferences => ({
energy_sources: [],
device_consumption: [],
device_consumption_water: [],
...prefs,
});
const GRID_RETURN = source({
type: "grid",
stat_energy_from: "sensor.grid_in",
stat_energy_to: "sensor.grid_out",
});
const SOLAR = source({ type: "solar", stat_energy_from: "sensor.solar" });
const GAS = source({ type: "gas", stat_energy_from: "sensor.gas" });
const WATER = source({ type: "water", stat_energy_from: "sensor.water" });
describe("energyCardKey", () => {
it("joins the view path and card type", () => {
expect(energyCardKey("electricity", "energy-solar-graph")).toBe(
"electricity.energy-solar-graph"
);
expect(energyCardKey("now", "power-sankey")).toBe("now.power-sankey");
});
});
describe("isEnergyCardHidden", () => {
it("returns true only when the composite key is in the hidden list", () => {
const hidden = ["electricity.energy-solar-graph"];
expect(
isEnergyCardHidden("electricity", "energy-solar-graph", hidden)
).toBe(true);
// Same card type in a different view is independent.
expect(isEnergyCardHidden("overview", "energy-solar-graph", hidden)).toBe(
false
);
expect(
isEnergyCardHidden("electricity", "energy-usage-graph", hidden)
).toBe(false);
});
it("treats undefined/empty hidden lists as nothing hidden", () => {
expect(
isEnergyCardHidden("electricity", "energy-solar-graph", undefined)
).toBe(false);
expect(isEnergyCardHidden("electricity", "energy-solar-graph", [])).toBe(
false
);
});
});
describe("catalog applicability", () => {
it("only lists cards relevant to the configured sources", () => {
const gasOnly = makePrefs({ energy_sources: [GAS] });
expect(applicableEnergyCardKeys("gas", gasOnly)).toEqual([
"gas.energy-gas-graph",
"gas.energy-sources-table",
]);
// No electricity sources -> no electricity cards apply.
expect(applicableEnergyCardKeys("electricity", gasOnly)).toEqual([]);
});
it("gates the solar graph and gauges on their sources", () => {
const solarGraph = ENERGY_CARD_CATALOG.find(
(c) => c.key === "electricity.energy-solar-graph"
)!;
expect(
solarGraph.isApplicable(makePrefs({ energy_sources: [SOLAR] }))
).toBe(true);
expect(
solarGraph.isApplicable(makePrefs({ energy_sources: [GRID_RETURN] }))
).toBe(false);
const neutralityGauge = ENERGY_CARD_CATALOG.find(
(c) => c.key === "electricity.energy-grid-neutrality-gauge"
)!;
// Needs grid export (return).
expect(
neutralityGauge.isApplicable(makePrefs({ energy_sources: [GRID_RETURN] }))
).toBe(true);
expect(
neutralityGauge.isApplicable(makePrefs({ energy_sources: [SOLAR] }))
).toBe(false);
});
});
describe("isEnergyViewEmpty", () => {
const prefs = makePrefs({ energy_sources: [WATER] });
it("is false when no cards in the view are hidden", () => {
expect(isEnergyViewEmpty("water", prefs, undefined)).toBe(false);
});
it("is false when only some applicable cards are hidden", () => {
expect(
isEnergyViewEmpty("water", prefs, ["water.energy-water-graph"])
).toBe(false);
});
it("is true when every applicable card is hidden", () => {
expect(
isEnergyViewEmpty("water", prefs, [
"water.energy-water-graph",
"water.energy-sources-table",
])
).toBe(true);
});
it("is false when the view has no applicable cards at all", () => {
// Water source configured, but the gas view has nothing applicable.
expect(isEnergyViewEmpty("gas", prefs, [])).toBe(false);
});
});
+195 -202
View File
@@ -4041,161 +4041,161 @@ __metadata:
languageName: node
linkType: hard
"@tsparticles/basic@npm:4.1.2":
version: 4.1.2
resolution: "@tsparticles/basic@npm:4.1.2"
"@tsparticles/basic@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/basic@npm:4.1.1"
dependencies:
"@tsparticles/engine": "npm:4.1.2"
"@tsparticles/plugin-blend": "npm:4.1.2"
"@tsparticles/plugin-hex-color": "npm:4.1.2"
"@tsparticles/plugin-hsl-color": "npm:4.1.2"
"@tsparticles/plugin-move": "npm:4.1.2"
"@tsparticles/plugin-rgb-color": "npm:4.1.2"
"@tsparticles/shape-circle": "npm:4.1.2"
"@tsparticles/updater-opacity": "npm:4.1.2"
"@tsparticles/updater-out-modes": "npm:4.1.2"
"@tsparticles/updater-paint": "npm:4.1.2"
"@tsparticles/updater-size": "npm:4.1.2"
checksum: 10/1c145d25373562cd3b45f20664610226c050a0a6867396c2d138a76761d3e7a5796cf107d8bdcbb8eb8cca19a3a1e4192eb356d37387d6547bf6b1ff796b71b2
"@tsparticles/engine": "npm:4.1.1"
"@tsparticles/plugin-blend": "npm:4.1.1"
"@tsparticles/plugin-hex-color": "npm:4.1.1"
"@tsparticles/plugin-hsl-color": "npm:4.1.1"
"@tsparticles/plugin-move": "npm:4.1.1"
"@tsparticles/plugin-rgb-color": "npm:4.1.1"
"@tsparticles/shape-circle": "npm:4.1.1"
"@tsparticles/updater-opacity": "npm:4.1.1"
"@tsparticles/updater-out-modes": "npm:4.1.1"
"@tsparticles/updater-paint": "npm:4.1.1"
"@tsparticles/updater-size": "npm:4.1.1"
checksum: 10/99191aeee4b9a3856aa82a2cea21e54d2099b6d58b4af3bacf4bb133277bd71de16ac07fe632ebabe08cc2a06be1aa6c00d82d593027276bf588870afc9182f5
languageName: node
linkType: hard
"@tsparticles/canvas-utils@npm:4.1.2":
version: 4.1.2
resolution: "@tsparticles/canvas-utils@npm:4.1.2"
"@tsparticles/canvas-utils@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/canvas-utils@npm:4.1.1"
peerDependencies:
"@tsparticles/engine": 4.1.2
checksum: 10/ffedc8400b5ff758331bdf1ef2362aac713c3e41d5bcece00153acc04c725947c41d545202d73bd6ef290d2ec220353f1801fb4ad22e99870989b0c68d7540c4
"@tsparticles/engine": 4.1.1
checksum: 10/2b5d7e9a55aa8086007f2dff940800c650233751ebb708fdbf9f91be4b0cd9d975400da42cdf531bb74860974dff5ea3c76199520ccd3f089904fba0d1bc0722
languageName: node
linkType: hard
"@tsparticles/engine@npm:4.1.2":
version: 4.1.2
resolution: "@tsparticles/engine@npm:4.1.2"
checksum: 10/6fe6aa50bba564a8a8691945cb378267e695ec58323066a4157e8a036e7a3caa99e1ced08d05bffd173045bbc33c4d41523afc28624b1bea015393c1a88ef2bb
"@tsparticles/engine@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/engine@npm:4.1.1"
checksum: 10/74886de63046f8752515f097176cae2fa8d506fd9d2d6a84106d43a89ff688e047d99a5018e56e3fffc17d5d13a9061f63fafcb2b4ebe773f78379621d0d9855
languageName: node
linkType: hard
"@tsparticles/interaction-particles-links@npm:4.1.2":
version: 4.1.2
resolution: "@tsparticles/interaction-particles-links@npm:4.1.2"
"@tsparticles/interaction-particles-links@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/interaction-particles-links@npm:4.1.1"
dependencies:
"@tsparticles/canvas-utils": "npm:4.1.2"
"@tsparticles/canvas-utils": "npm:4.1.1"
peerDependencies:
"@tsparticles/engine": 4.1.2
"@tsparticles/plugin-interactivity": 4.1.2
checksum: 10/b9e1e85bdb3226a25d7a4486556abfe458c81c109855c04857623d2d3506ef325fcc2dc4fcee962414e216aa6346bc0849df1bf7199a3e4e75bed2fb7f069702
"@tsparticles/engine": 4.1.1
"@tsparticles/plugin-interactivity": 4.1.1
checksum: 10/c0c7ad3740f2168b75ca7ae09341fefcc1d8c8d117fbee7a6519a622843dfe5d57bb65113a4eba40f26906904f069cbac196840f45fc7be12864fddfa8106184
languageName: node
linkType: hard
"@tsparticles/plugin-blend@npm:4.1.2":
version: 4.1.2
resolution: "@tsparticles/plugin-blend@npm:4.1.2"
"@tsparticles/plugin-blend@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/plugin-blend@npm:4.1.1"
peerDependencies:
"@tsparticles/engine": 4.1.2
checksum: 10/1f51bcf76d6d749a0a18799fd3a814a82e62f060fd3c20d28f8bb170311815aeba8efc361e73cef7356c51151c7debafc75d34bff6ab2d4e58099b51774a28c6
"@tsparticles/engine": 4.1.1
checksum: 10/9a666dc1fe0ed9ff4743fc23ca3bf27bd5d66073cc0127203b566077d431321b61af440875e96c87950e18011b1e6b9b43d328e4200923dd4be9fd934c07a5cd
languageName: node
linkType: hard
"@tsparticles/plugin-hex-color@npm:4.1.2":
version: 4.1.2
resolution: "@tsparticles/plugin-hex-color@npm:4.1.2"
"@tsparticles/plugin-hex-color@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/plugin-hex-color@npm:4.1.1"
peerDependencies:
"@tsparticles/engine": 4.1.2
checksum: 10/d32a39bbd6732b9e630f84a7d226753735f02d65b70a1d5d11fa680421d5f1378aee75af1ecb5ada5af9875822bbaf2b5d1f213d1e0cbce2a50820743eb9c2fc
"@tsparticles/engine": 4.1.1
checksum: 10/33fde7c2763affb1dd7fa033431a2553646bde79c9ab52106be6226a9716ae973c90c8b4bf381d96f661ab75467934019cd456fe27ccac59dca11b5c989c3a75
languageName: node
linkType: hard
"@tsparticles/plugin-hsl-color@npm:4.1.2":
version: 4.1.2
resolution: "@tsparticles/plugin-hsl-color@npm:4.1.2"
"@tsparticles/plugin-hsl-color@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/plugin-hsl-color@npm:4.1.1"
peerDependencies:
"@tsparticles/engine": 4.1.2
checksum: 10/c130e80b0fd2750402fa0e309d0bb624b2138474069ae4ccb76610722c4f0966f278ad1a35c85fbcf5449914936f1c53322246f1cdd028106d83f779beee538c
"@tsparticles/engine": 4.1.1
checksum: 10/594f4a840f6f6f134a11d76c1a7fa80ed60a5d945534aad53e8ab5718341187f0fac73eb45d1c39246b98e982f1d60f9dbb95a083edcf8407e015048056b84e7
languageName: node
linkType: hard
"@tsparticles/plugin-interactivity@npm:4.1.2":
version: 4.1.2
resolution: "@tsparticles/plugin-interactivity@npm:4.1.2"
"@tsparticles/plugin-interactivity@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/plugin-interactivity@npm:4.1.1"
peerDependencies:
"@tsparticles/engine": 4.1.2
checksum: 10/78efd4ffe3a07752f26262fe27ee0a931b45fea6c81d62e156d8f965dc22a2141d64395eb99477624ca9aac9e152f9e2585fc8aced9c822c61cb22ecfad184a4
"@tsparticles/engine": 4.1.1
checksum: 10/448bd8f7c741ed0359ace49a6c58d58947d95660158a131c86c248427b9caa41645c6e8c9cea6a8f0f6a70cd25c0dd64017edba79c8f225b6db07f39b660eb4d
languageName: node
linkType: hard
"@tsparticles/plugin-move@npm:4.1.2":
version: 4.1.2
resolution: "@tsparticles/plugin-move@npm:4.1.2"
"@tsparticles/plugin-move@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/plugin-move@npm:4.1.1"
peerDependencies:
"@tsparticles/engine": 4.1.2
checksum: 10/fac5b90904d3fa62b310a1ce19647c5ae4a592111b5eeccfc6e946c6d17d677b0f3931ba3c114f04045827a92c1a0ed66791e99b352cb75d6c50f3749f389bfc
"@tsparticles/engine": 4.1.1
checksum: 10/b854cb6e2dcea2971f1abaca75c6e8cde40611178f13513559f60cc45da568e8b61c2f027619041d94e11c60af08a76eb4957d344a910b7a6255265937199094
languageName: node
linkType: hard
"@tsparticles/plugin-rgb-color@npm:4.1.2":
version: 4.1.2
resolution: "@tsparticles/plugin-rgb-color@npm:4.1.2"
"@tsparticles/plugin-rgb-color@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/plugin-rgb-color@npm:4.1.1"
peerDependencies:
"@tsparticles/engine": 4.1.2
checksum: 10/7e3e83171f74f7e9e3f68ce8d0aeb4685b277ecac3c4874a951e86228d3db78978d6197ef9e04b17190471294323c561c18c92c9d9ae8e8a115e6238cbecdb43
"@tsparticles/engine": 4.1.1
checksum: 10/2372dfe5ceec163b49c02b3b0169e6032fae34e4ade3c29d42bd26af23fac76b3416d70eed9cac9a0f2b470c763ee903d12d0e9156b7693b3c8908e25714cf76
languageName: node
linkType: hard
"@tsparticles/preset-links@npm:4.1.2":
version: 4.1.2
resolution: "@tsparticles/preset-links@npm:4.1.2"
"@tsparticles/preset-links@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/preset-links@npm:4.1.1"
dependencies:
"@tsparticles/basic": "npm:4.1.2"
"@tsparticles/engine": "npm:4.1.2"
"@tsparticles/interaction-particles-links": "npm:4.1.2"
"@tsparticles/plugin-interactivity": "npm:4.1.2"
checksum: 10/d907884c4e9fd023aa8e0e1ff2388ef24d06a446433cc4ffbd620a4d22801f52ce97fe5f441b9fdea854f36d6f421d7571648925e5ea5c0976c121194156a2b1
"@tsparticles/basic": "npm:4.1.1"
"@tsparticles/engine": "npm:4.1.1"
"@tsparticles/interaction-particles-links": "npm:4.1.1"
"@tsparticles/plugin-interactivity": "npm:4.1.1"
checksum: 10/26dd1dcd4ede3bae32cae3585a7f929e0089a75e389f83cb5b00213e977b647c3e7e743b5e83d3a5400beffcff9343bde7538412579708bad761d2e933708638
languageName: node
linkType: hard
"@tsparticles/shape-circle@npm:4.1.2":
version: 4.1.2
resolution: "@tsparticles/shape-circle@npm:4.1.2"
"@tsparticles/shape-circle@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/shape-circle@npm:4.1.1"
peerDependencies:
"@tsparticles/engine": 4.1.2
checksum: 10/60a87755d4e598c278c1850b58b07bcfbef603de242de3979702cd8120cf24fd95d985b2092c7a8a2cccd43eb6a1939ab215f74a93898ea1e2d425be5deb405c
"@tsparticles/engine": 4.1.1
checksum: 10/401e8a267cf9301dae8386e9bb1c5ff3a02dfb5b5f136187c73ed6b89e33f176f929fdc53acaa60b13f3ee1ed7f8c6f35f3f0e26ff6930a33d1949a0f23e4c1f
languageName: node
linkType: hard
"@tsparticles/updater-opacity@npm:4.1.2":
version: 4.1.2
resolution: "@tsparticles/updater-opacity@npm:4.1.2"
"@tsparticles/updater-opacity@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/updater-opacity@npm:4.1.1"
peerDependencies:
"@tsparticles/engine": 4.1.2
checksum: 10/4dc426175bb3e4c5d3bc35e7d83ace7130c4f8817c7f88ee6bf1b4b1a52b28511d99b1fc7ded3c549a5baae401f7c22cc701d83919694387154aef86ea6974c2
"@tsparticles/engine": 4.1.1
checksum: 10/3fe157203e02dfec1ef9ac693d517cd9abcad8c28acaac8d4ca923b8301a99ae73129ed4559ea507c0d7d6ad626bdbbdb03f25868d7d28f852e2d8ffc7d13790
languageName: node
linkType: hard
"@tsparticles/updater-out-modes@npm:4.1.2":
version: 4.1.2
resolution: "@tsparticles/updater-out-modes@npm:4.1.2"
"@tsparticles/updater-out-modes@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/updater-out-modes@npm:4.1.1"
peerDependencies:
"@tsparticles/engine": 4.1.2
checksum: 10/901be7b7dfa97d3d4ff857715a6b61dd4bc2ca09f23fee38d86cd6827351134c5cd4c3fa702d2aec8601544b1f6935f2b4034c4567a00128f9cf36cbaf379722
"@tsparticles/engine": 4.1.1
checksum: 10/bc5f074661c42acc20a87ee3ff093fe63d2fd87a7f7d3156c2fb23dc1d24881ccc9e9cb82313ff7265317fcdc447a5b17b2204930faf8ba627278e2dc0fab2c3
languageName: node
linkType: hard
"@tsparticles/updater-paint@npm:4.1.2":
version: 4.1.2
resolution: "@tsparticles/updater-paint@npm:4.1.2"
"@tsparticles/updater-paint@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/updater-paint@npm:4.1.1"
peerDependencies:
"@tsparticles/engine": 4.1.2
checksum: 10/5a0b4bc0a4c6061e1eb93a6af7a97b472e5d552677dafe8c888b0f9a71b68ed10ef255d86f2cf8d8466ecd1d33ba8f5e52920947237fe1218cbcd002c50dff47
"@tsparticles/engine": 4.1.1
checksum: 10/77459f6337b869d654573696dbe2bc1d238ddf3410857d8b91df606f7402301acae34f4a334727faa552f9816f518353b7a98e0bda4854ea5419cb34e8d447d2
languageName: node
linkType: hard
"@tsparticles/updater-size@npm:4.1.2":
version: 4.1.2
resolution: "@tsparticles/updater-size@npm:4.1.2"
"@tsparticles/updater-size@npm:4.1.1":
version: 4.1.1
resolution: "@tsparticles/updater-size@npm:4.1.1"
peerDependencies:
"@tsparticles/engine": 4.1.2
checksum: 10/d5c4f79d5c1d3d977dd5ebac56c9679df134ddba919b8e2ee1473820f2c551f5b162356b49b22e6e54134b87a27c51cb5a6c2e2441ba2569b0dab62974e91a47
"@tsparticles/engine": 4.1.1
checksum: 10/33ae9d51394e299459478a9dedd369de1bd266ad9c2e2236ec2c20bb455aad9118efa13afc7d359b6d7d010a34bba5b60cbf2e3d8b853e124c0b53c2aae8db18
languageName: node
linkType: hard
@@ -4548,105 +4548,105 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/eslint-plugin@npm:8.60.1":
version: 8.60.1
resolution: "@typescript-eslint/eslint-plugin@npm:8.60.1"
"@typescript-eslint/eslint-plugin@npm:8.60.0":
version: 8.60.0
resolution: "@typescript-eslint/eslint-plugin@npm:8.60.0"
dependencies:
"@eslint-community/regexpp": "npm:^4.12.2"
"@typescript-eslint/scope-manager": "npm:8.60.1"
"@typescript-eslint/type-utils": "npm:8.60.1"
"@typescript-eslint/utils": "npm:8.60.1"
"@typescript-eslint/visitor-keys": "npm:8.60.1"
"@typescript-eslint/scope-manager": "npm:8.60.0"
"@typescript-eslint/type-utils": "npm:8.60.0"
"@typescript-eslint/utils": "npm:8.60.0"
"@typescript-eslint/visitor-keys": "npm:8.60.0"
ignore: "npm:^7.0.5"
natural-compare: "npm:^1.4.0"
ts-api-utils: "npm:^2.5.0"
peerDependencies:
"@typescript-eslint/parser": ^8.60.1
"@typescript-eslint/parser": ^8.60.0
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.1.0"
checksum: 10/f3633bb2700bc32299578baeaf6650418656229be256147ba9d1ab09b34ef2b7fed83804ef4d2439e9189dbdcb89399d67bc8fea55262be6caa32114be048538
checksum: 10/aec6f08be04ad0014c80e5cf3bd8ec83d59c44244c9ca357c4cf182b6f0debdd690e64daa88215e937183e97c4bdee6749dbf4162191c5851ae9c648439c8a96
languageName: node
linkType: hard
"@typescript-eslint/parser@npm:8.60.1":
version: 8.60.1
resolution: "@typescript-eslint/parser@npm:8.60.1"
"@typescript-eslint/parser@npm:8.60.0":
version: 8.60.0
resolution: "@typescript-eslint/parser@npm:8.60.0"
dependencies:
"@typescript-eslint/scope-manager": "npm:8.60.1"
"@typescript-eslint/types": "npm:8.60.1"
"@typescript-eslint/typescript-estree": "npm:8.60.1"
"@typescript-eslint/visitor-keys": "npm:8.60.1"
"@typescript-eslint/scope-manager": "npm:8.60.0"
"@typescript-eslint/types": "npm:8.60.0"
"@typescript-eslint/typescript-estree": "npm:8.60.0"
"@typescript-eslint/visitor-keys": "npm:8.60.0"
debug: "npm:^4.4.3"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.1.0"
checksum: 10/f9c484c4a3897015328f328a1c6ee778d113dd134201f635c0421cb72efe6e63f3a68524aff0df6e19e76ff93daf5cabd946e67f12f10dddcf19bda534aa68dc
checksum: 10/f55fa3547e3d0a0ec88bcb886b9bf6cef9b425c016dfa47e2ad7fbcbaa854640ba3f501cc0115824b58f33be4bf8bdf544505847988688906d11c154b600c54d
languageName: node
linkType: hard
"@typescript-eslint/project-service@npm:8.60.1":
version: 8.60.1
resolution: "@typescript-eslint/project-service@npm:8.60.1"
"@typescript-eslint/project-service@npm:8.60.0":
version: 8.60.0
resolution: "@typescript-eslint/project-service@npm:8.60.0"
dependencies:
"@typescript-eslint/tsconfig-utils": "npm:^8.60.1"
"@typescript-eslint/types": "npm:^8.60.1"
"@typescript-eslint/tsconfig-utils": "npm:^8.60.0"
"@typescript-eslint/types": "npm:^8.60.0"
debug: "npm:^4.4.3"
peerDependencies:
typescript: ">=4.8.4 <6.1.0"
checksum: 10/fec693dd79c3a1e6a24091127a37af4eb9d9cee8192cf2a434adae48543eadff834bc0623b5b563c8b592b250bc080570f9e7b42807252ea898442c525beeee9
checksum: 10/21e233d1292775753861aad32b30448f9fb5508f53d5a12c8ce7e75613df236757377fa877c738cc858ac863f2f8259a1f63bfb15a32ee9c5476fe9b2d12fbb0
languageName: node
linkType: hard
"@typescript-eslint/scope-manager@npm:8.60.1":
version: 8.60.1
resolution: "@typescript-eslint/scope-manager@npm:8.60.1"
"@typescript-eslint/scope-manager@npm:8.60.0":
version: 8.60.0
resolution: "@typescript-eslint/scope-manager@npm:8.60.0"
dependencies:
"@typescript-eslint/types": "npm:8.60.1"
"@typescript-eslint/visitor-keys": "npm:8.60.1"
checksum: 10/7228c110410ff8cfc01e96d8f17c986f8b4dd447fe3a3291baaab8fe946026ccdf0291865f788f18cf538ab49bfc067fe797708b6b8590104a65f7e69f921cc5
"@typescript-eslint/types": "npm:8.60.0"
"@typescript-eslint/visitor-keys": "npm:8.60.0"
checksum: 10/c08274fdb38be51d2d655ee32bed271cfedf5f5775709da98b3d6cf5f7eb419e98228fb087b48f4a591f4dd71ebcb27a8bd716fa831442c7cad708288625e454
languageName: node
linkType: hard
"@typescript-eslint/tsconfig-utils@npm:8.60.1, @typescript-eslint/tsconfig-utils@npm:^8.60.1":
version: 8.60.1
resolution: "@typescript-eslint/tsconfig-utils@npm:8.60.1"
"@typescript-eslint/tsconfig-utils@npm:8.60.0, @typescript-eslint/tsconfig-utils@npm:^8.60.0":
version: 8.60.0
resolution: "@typescript-eslint/tsconfig-utils@npm:8.60.0"
peerDependencies:
typescript: ">=4.8.4 <6.1.0"
checksum: 10/afc78b19b856a71dc4e493f931ae44e1a91dc6441a14cb92e4063db880892f3874768f9d347d4b2f45362f2090e4455407c70f42027d77ddc85d6cba95cdb76c
checksum: 10/d82cac7dec0366c0e680d002b4d20bc2564a198a2d9a80099f4fa7ee2b2f394dd2d47df03f1c4b276c4de6c7b8684a50e7bad0ddd5b33907188e90cc203a9593
languageName: node
linkType: hard
"@typescript-eslint/type-utils@npm:8.60.1":
version: 8.60.1
resolution: "@typescript-eslint/type-utils@npm:8.60.1"
"@typescript-eslint/type-utils@npm:8.60.0":
version: 8.60.0
resolution: "@typescript-eslint/type-utils@npm:8.60.0"
dependencies:
"@typescript-eslint/types": "npm:8.60.1"
"@typescript-eslint/typescript-estree": "npm:8.60.1"
"@typescript-eslint/utils": "npm:8.60.1"
"@typescript-eslint/types": "npm:8.60.0"
"@typescript-eslint/typescript-estree": "npm:8.60.0"
"@typescript-eslint/utils": "npm:8.60.0"
debug: "npm:^4.4.3"
ts-api-utils: "npm:^2.5.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.1.0"
checksum: 10/6f426263be597063831bf308e52328e8d387af5db955a09cb85fde1c72f5b1b36a365133b9c9a74330e5e948e59bf9a9b82605f4c9c4e3bf9b6cb7f4c37e4b18
checksum: 10/4b29dcc1ee7a006b2df8a50700b43701bedd4f8380e94311a8988102d98fdd89244c233a8063a800cbdee86278bdc98874bfa6a8a3c71f1b278be1be6698961b
languageName: node
linkType: hard
"@typescript-eslint/types@npm:8.60.1, @typescript-eslint/types@npm:^8.56.0, @typescript-eslint/types@npm:^8.60.1":
version: 8.60.1
resolution: "@typescript-eslint/types@npm:8.60.1"
checksum: 10/c603417e621b5b1263c2f60fad9e202d560fd07fce7f40e9a356c0530e5eaf0ff1a9af865237bf93aa18a5a4e2f034ee0cce0fe6c070f08df33e35a099bdea47
"@typescript-eslint/types@npm:8.60.0, @typescript-eslint/types@npm:^8.56.0, @typescript-eslint/types@npm:^8.60.0":
version: 8.60.0
resolution: "@typescript-eslint/types@npm:8.60.0"
checksum: 10/8c6967503b3a370af10fea7bfec9adc7a4152e0e8aaa72ee790f105f08721683f6e8829acf610de82bfcdeb56bdf07f6795ccec394edbdac222fd3a1d76fe9cd
languageName: node
linkType: hard
"@typescript-eslint/typescript-estree@npm:8.60.1":
version: 8.60.1
resolution: "@typescript-eslint/typescript-estree@npm:8.60.1"
"@typescript-eslint/typescript-estree@npm:8.60.0":
version: 8.60.0
resolution: "@typescript-eslint/typescript-estree@npm:8.60.0"
dependencies:
"@typescript-eslint/project-service": "npm:8.60.1"
"@typescript-eslint/tsconfig-utils": "npm:8.60.1"
"@typescript-eslint/types": "npm:8.60.1"
"@typescript-eslint/visitor-keys": "npm:8.60.1"
"@typescript-eslint/project-service": "npm:8.60.0"
"@typescript-eslint/tsconfig-utils": "npm:8.60.0"
"@typescript-eslint/types": "npm:8.60.0"
"@typescript-eslint/visitor-keys": "npm:8.60.0"
debug: "npm:^4.4.3"
minimatch: "npm:^10.2.2"
semver: "npm:^7.7.3"
@@ -4654,32 +4654,32 @@ __metadata:
ts-api-utils: "npm:^2.5.0"
peerDependencies:
typescript: ">=4.8.4 <6.1.0"
checksum: 10/9c3a56266aadf589bc6e770cd04cb3f55b1ee1507dcacda61866408c656ae4462aa7e11baf39eb939bc4d1e3b843cf58e60f3ebdeb3e75f042ff0f6fb39c311b
checksum: 10/ad02384fd48152a7d9bb5db1aa5d6cbda1cfa9e549a2d529d801ec1401d1d7d011c5e071f5b4d99c5ed656c95e5e97c46a783b45dcc7c016df7fee37ab5bdc0a
languageName: node
linkType: hard
"@typescript-eslint/utils@npm:8.60.1":
version: 8.60.1
resolution: "@typescript-eslint/utils@npm:8.60.1"
"@typescript-eslint/utils@npm:8.60.0":
version: 8.60.0
resolution: "@typescript-eslint/utils@npm:8.60.0"
dependencies:
"@eslint-community/eslint-utils": "npm:^4.9.1"
"@typescript-eslint/scope-manager": "npm:8.60.1"
"@typescript-eslint/types": "npm:8.60.1"
"@typescript-eslint/typescript-estree": "npm:8.60.1"
"@typescript-eslint/scope-manager": "npm:8.60.0"
"@typescript-eslint/types": "npm:8.60.0"
"@typescript-eslint/typescript-estree": "npm:8.60.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.1.0"
checksum: 10/a75f8714995b6280b4c15ca957bbc6634862453461111e4a2a07b8bc72b51a504484a9b957fc5b7a646c4bf09f1e414a0c52cd3b6798c42fb8c4de83b1b5a364
checksum: 10/9fc8bc7a62deacd3823d957de8e8ca2012ffa90715734cd89d0e3a62c2c9e2775d3ba9da80e419339893a44af8674d690488cb195c981e8de9fd9dfa4948956d
languageName: node
linkType: hard
"@typescript-eslint/visitor-keys@npm:8.60.1":
version: 8.60.1
resolution: "@typescript-eslint/visitor-keys@npm:8.60.1"
"@typescript-eslint/visitor-keys@npm:8.60.0":
version: 8.60.0
resolution: "@typescript-eslint/visitor-keys@npm:8.60.0"
dependencies:
"@typescript-eslint/types": "npm:8.60.1"
"@typescript-eslint/types": "npm:8.60.0"
eslint-visitor-keys: "npm:^5.0.0"
checksum: 10/6d120b4a790477ae0291e69f6457686c71b929cc40519148f6b6c7fbc09604b15821ae8cf1005aa23acec5105b4016db256a68d68f30eda8d6c24d4fdb0ede86
checksum: 10/4854d08416e2c97837cc1ecf8dacb50b3337ebb34bd6d703ad40b6585fdf78243074e56994ddc90650586146cebd6ad7390b6fa3ddda4e3532be4b872dd8f541
languageName: node
linkType: hard
@@ -5641,12 +5641,12 @@ __metadata:
languageName: node
linkType: hard
"barcode-detector@npm:3.2.0":
version: 3.2.0
resolution: "barcode-detector@npm:3.2.0"
"barcode-detector@npm:3.1.3":
version: 3.1.3
resolution: "barcode-detector@npm:3.1.3"
dependencies:
zxing-wasm: "npm:3.1.0"
checksum: 10/f52eb18ddae2af3d4c9c76b47e7b639d0834cd32f558901d8a23cc00349047e53a6da5c3958653fa524dcb912ed9178f3e0d37939b9be00f9607772a84d90ccf
zxing-wasm: "npm:3.0.3"
checksum: 10/3e33b00bdc4b6f6bae67ca2a2fbe7def8861060591bf5a46ece6a8f30eadc9c66ce32776633107bb704ad0910302845a6bd520045e169410ec7ad8d4346633a7
languageName: node
linkType: hard
@@ -7957,10 +7957,10 @@ __metadata:
languageName: node
linkType: hard
"fuse.js@npm:7.4.1":
version: 7.4.1
resolution: "fuse.js@npm:7.4.1"
checksum: 10/581941d5015968ee624feb10a56d9b49d5d954672b2c9ec189d4ca513da6f8a3dea2d5f6637386d8298ffc5846f6d83435210d40a47c58e14d11dc5707544c75
"fuse.js@npm:7.4.0":
version: 7.4.0
resolution: "fuse.js@npm:7.4.0"
checksum: 10/dba0ef239be1f28ba5daefb3a17371c73291f4d0db3d1733b625848a7311e05aa58a795cd5b2fd9626c09857608a74e8c9620f5d1ce2d3d0b2d40155ae15e21e
languageName: node
linkType: hard
@@ -8487,8 +8487,8 @@ __metadata:
"@rspack/dev-server": "npm:2.0.3"
"@swc/helpers": "npm:0.5.23"
"@thomasloven/round-slider": "npm:0.6.0"
"@tsparticles/engine": "npm:4.1.2"
"@tsparticles/preset-links": "npm:4.1.2"
"@tsparticles/engine": "npm:4.1.1"
"@tsparticles/preset-links": "npm:4.1.1"
"@types/chromecast-caf-receiver": "npm:6.0.26"
"@types/chromecast-caf-sender": "npm:1.0.11"
"@types/color-name": "npm:2.0.0"
@@ -8509,7 +8509,7 @@ __metadata:
"@webcomponents/webcomponentsjs": "npm:2.8.0"
babel-loader: "npm:10.1.1"
babel-plugin-template-html-minifier: "npm:4.1.0"
barcode-detector: "npm:3.2.0"
barcode-detector: "npm:3.1.3"
browserslist-useragent-regexp: "npm:4.1.4"
cally: "npm:0.9.2"
color-name: "npm:2.1.0"
@@ -8534,7 +8534,7 @@ __metadata:
eslint-plugin-wc: "npm:3.1.0"
fancy-log: "npm:2.0.0"
fs-extra: "npm:11.3.5"
fuse.js: "npm:7.4.1"
fuse.js: "npm:7.4.0"
generate-license-file: "npm:4.2.1"
glob: "npm:13.0.6"
globals: "npm:17.6.0"
@@ -8582,12 +8582,12 @@ __metadata:
sortablejs: "patch:sortablejs@npm%3A1.15.6#~/.yarn/patches/sortablejs-npm-1.15.6-3235a8f83b.patch"
stacktrace-js: "npm:2.0.2"
superstruct: "npm:2.0.2"
tar: "npm:7.5.16"
tar: "npm:7.5.15"
terser-webpack-plugin: "npm:5.6.1"
tinykeys: "patch:tinykeys@npm%3A4.0.0#~/.yarn/patches/tinykeys-npm-4.0.0-a6ca3fd771.patch"
tinykeys: "npm:4.0.0"
ts-lit-plugin: "npm:2.0.2"
typescript: "npm:6.0.3"
typescript-eslint: "npm:8.60.1"
typescript-eslint: "npm:8.60.0"
vite-tsconfig-paths: "npm:6.1.1"
vitest: "npm:4.1.8"
webpack-stats-plugin: "npm:1.1.3"
@@ -12999,16 +12999,16 @@ __metadata:
languageName: node
linkType: hard
"tar@npm:*, tar@npm:7.5.16, tar@npm:^7.4.3, tar@npm:^7.5.4":
version: 7.5.16
resolution: "tar@npm:7.5.16"
"tar@npm:*, tar@npm:7.5.15, tar@npm:^7.4.3, tar@npm:^7.5.4":
version: 7.5.15
resolution: "tar@npm:7.5.15"
dependencies:
"@isaacs/fs-minipass": "npm:^4.0.0"
chownr: "npm:^3.0.0"
minipass: "npm:^7.1.2"
minizlib: "npm:^3.1.0"
yallist: "npm:^5.0.0"
checksum: 10/fafa22efceb9f056bf29ddc47d9bd90bb82fe3ce57b8d1242fc45771251741964cebba69d4e14a24fd1643f3c7f68478e945a19def534703cf370c2d9dca2e09
checksum: 10/b4cb6acd822159867f81ebda8d765c6941ec8292f1cf2f870d3713f4933c14bf0ed7bf4a92338143c31e8815ca0a1fdd62aa03ddb48a42ae187f7ef696583ffe
languageName: node
linkType: hard
@@ -13196,13 +13196,6 @@ __metadata:
languageName: node
linkType: hard
"tinykeys@patch:tinykeys@npm%3A4.0.0#~/.yarn/patches/tinykeys-npm-4.0.0-a6ca3fd771.patch":
version: 4.0.0
resolution: "tinykeys@patch:tinykeys@npm%3A4.0.0#~/.yarn/patches/tinykeys-npm-4.0.0-a6ca3fd771.patch::version=4.0.0&hash=267e16"
checksum: 10/f1ca5d1eef7fc628ba55c53da218869db0d6efc6ba28ebbc4974476ff6f2a29ec34cd584008eabc6f03453cfcd4c7fc59b51d8fbca355fd30a8e784eb3c9bc8d
languageName: node
linkType: hard
"tinyrainbow@npm:^3.1.0":
version: 3.1.0
resolution: "tinyrainbow@npm:3.1.0"
@@ -13399,12 +13392,12 @@ __metadata:
languageName: node
linkType: hard
"type-fest@npm:^5.7.0":
version: 5.7.0
resolution: "type-fest@npm:5.7.0"
"type-fest@npm:^5.6.0":
version: 5.6.0
resolution: "type-fest@npm:5.6.0"
dependencies:
tagged-tag: "npm:^1.0.0"
checksum: 10/4867626aa489968df98e09ecdefbc45dfbb191ae5fb8924b3bd45da9cd940879b387086226366dce028570983a3fbe80adc53ad105a169bbbd27621c496bd6f0
checksum: 10/2cc7a510f46a538af7a365ed50cee10e51a69bd353ca66c2a432e172b90ff12efff9ba59c7ba493d6361d07fd298c84945b9e4481ab63f85a29860c0b0430233
languageName: node
linkType: hard
@@ -13461,18 +13454,18 @@ __metadata:
languageName: node
linkType: hard
"typescript-eslint@npm:8.60.1":
version: 8.60.1
resolution: "typescript-eslint@npm:8.60.1"
"typescript-eslint@npm:8.60.0":
version: 8.60.0
resolution: "typescript-eslint@npm:8.60.0"
dependencies:
"@typescript-eslint/eslint-plugin": "npm:8.60.1"
"@typescript-eslint/parser": "npm:8.60.1"
"@typescript-eslint/typescript-estree": "npm:8.60.1"
"@typescript-eslint/utils": "npm:8.60.1"
"@typescript-eslint/eslint-plugin": "npm:8.60.0"
"@typescript-eslint/parser": "npm:8.60.0"
"@typescript-eslint/typescript-estree": "npm:8.60.0"
"@typescript-eslint/utils": "npm:8.60.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
typescript: ">=4.8.4 <6.1.0"
checksum: 10/e12091ab2540b817c76b0ec6aad92e341f810310bec2b24bc95780aee106049c05363998f6ea52ed066130c8afc41dca1627f56e4c1df1dd519f4d4ca0ce4910
checksum: 10/625e49e6d06e32adcfe903087d1fb2adc3be925adafe1f4e57f690bb196b35e2aac01760a3d5e17a53ea2feb6fef3a13da4b8faa214f628ec56e64f99f20e4ad
languageName: node
linkType: hard
@@ -14867,14 +14860,14 @@ __metadata:
languageName: node
linkType: hard
"zxing-wasm@npm:3.1.0":
version: 3.1.0
resolution: "zxing-wasm@npm:3.1.0"
"zxing-wasm@npm:3.0.3":
version: 3.0.3
resolution: "zxing-wasm@npm:3.0.3"
dependencies:
"@types/emscripten": "npm:^1.41.5"
type-fest: "npm:^5.7.0"
type-fest: "npm:^5.6.0"
peerDependencies:
"@types/emscripten": ">=1.39.6"
checksum: 10/ea68d0cfbe31d8dabcd9b942dcfdb703866c1f76ee0d804fb75f1e49f092a26771575705dd48d69927bc6525f146fd9e35030a5a72341222716d7f62e5f6c788
checksum: 10/0c0636fab96a67d7f8540dede8d98b5cbb53c95313cfbe21114e2fd15262d6b860c84467d627d6e768bcdc684d75c7b5b8ef6b3d53d8d14004436e809a1d4b32
languageName: node
linkType: hard