Compare commits

..

4 Commits

Author SHA1 Message Date
Simon Lamon
f22f01e513 Merge branch 'dev' into sec_pypi_publishing 2025-10-06 20:28:38 +02:00
Simon Lamon
3f86f144b5 Merge branch 'dev' into sec_pypi_publishing 2025-10-04 17:25:20 +02:00
Simon Lamon
4efef5ed16 Update release.yaml 2025-09-24 07:04:06 +02:00
Simon Lamon
cac7ae2a40 Remove twine and introduce trusted publishing 2025-09-20 21:23:04 +02:00
92 changed files with 1320 additions and 4409 deletions

View File

@@ -36,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
uses: github/codeql-action/init@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6
with:
languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
uses: github/codeql-action/autobuild@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -57,4 +57,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
uses: github/codeql-action/analyze@64d10c13136e1c5bce3e5fbde8d4906eeaafc885 # v3.30.6

View File

@@ -19,8 +19,11 @@ jobs:
release:
name: Release
runs-on: ubuntu-latest
environment: pypi
permissions:
contents: write # Required to upload release assets
id-token: write # For "Trusted Publisher" to PyPi
if: github.repository_owner == 'home-assistant'
steps:
- name: Checkout the repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -46,16 +49,20 @@ jobs:
run: ./script/translations_download
env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
- name: Build and release package
run: |
python3 -m pip install twine build
export TWINE_USERNAME="__token__"
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
python3 -m pip install build
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
script/release
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
skip-existing: true
- name: Upload release assets
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4
with:
files: |
dist/*.whl
@@ -108,7 +115,7 @@ jobs:
- name: Tar folder
run: tar -czf landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz -C landing-page/dist .
- name: Upload release asset
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4
with:
files: landing-page/home_assistant_frontend_landingpage-${{ github.event.release.tag_name }}.tar.gz
@@ -137,6 +144,6 @@ jobs:
- name: Tar folder
run: tar -czf hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz -C hassio/build .
- name: Upload release asset
uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2.4.1
uses: softprops/action-gh-release@62c96d0c4e8a889135c1f3a25910db8dbe0e85f7 # v2.3.4
with:
files: hassio/home_assistant_frontend_supervisor-${{ github.event.release.tag_name }}.tar.gz

View File

@@ -1,3 +0,0 @@
---
title: Dialog (ha-wa-dialog)
---

View File

@@ -1,523 +0,0 @@
import { css, html, LitElement } from "lit";
import { customElement, state } from "lit/decorators";
import { mdiCog, mdiHelp } from "@mdi/js";
import "../../../../src/components/ha-button";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-dialog-footer";
import "../../../../src/components/ha-form/ha-form";
import "../../../../src/components/ha-icon-button";
import "../../../../src/components/ha-wa-dialog";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
const SCHEMA: HaFormSchema[] = [
{ type: "string", name: "Name", default: "", autofocus: true },
{ type: "string", name: "Email", default: "" },
];
type DialogType =
| false
| "basic"
| "basic-subtitle-below"
| "basic-subtitle-above"
| "form"
| "actions";
@customElement("demo-components-ha-wa-dialog")
export class DemoHaWaDialog extends LitElement {
@state() private _openDialog: DialogType = false;
protected render() {
return html`
<div class="content">
<h1>Dialog <code>&lt;ha-wa-dialog&gt;</code></h1>
<p class="subtitle">Dialog component built with WebAwesome.</p>
<h2>Demos</h2>
<div class="buttons">
<ha-button @click=${this._handleOpenDialog("basic")}
>Basic dialog</ha-button
>
<ha-button @click=${this._handleOpenDialog("basic-subtitle-below")}
>Basic dialog with subtitle below</ha-button
>
<ha-button @click=${this._handleOpenDialog("basic-subtitle-above")}
>Basic dialog with subtitle above</ha-button
>
<ha-button @click=${this._handleOpenDialog("form")}
>Dialog with form</ha-button
>
<ha-button @click=${this._handleOpenDialog("actions")}
>Dialog with actions</ha-button
>
</div>
<ha-wa-dialog
.open=${this._openDialog === "basic"}
header-title="Basic dialog"
@closed=${this._handleClosed}
>
<div>Dialog content</div>
</ha-wa-dialog>
<ha-wa-dialog
.open=${this._openDialog === "basic-subtitle-below"}
header-title="Basic dialog with subtitle"
header-subtitle="This is a basic dialog with a subtitle below"
@closed=${this._handleClosed}
>
<div>Dialog content</div>
</ha-wa-dialog>
<ha-wa-dialog
.open=${this._openDialog === "basic-subtitle-above"}
header-title="Dialog with subtitle above"
header-subtitle="This is a basic dialog with a subtitle above"
header-subtitle-position="above"
@closed=${this._handleClosed}
>
<div>Dialog content</div>
</ha-wa-dialog>
<ha-wa-dialog
.open=${this._openDialog === "form"}
header-title="Dialog with form"
header-subtitle="This is a dialog with a form and a footer"
prevent-scrim-close
@closed=${this._handleClosed}
>
<ha-form autofocus .schema=${SCHEMA}></ha-form>
<ha-dialog-footer slot="footer">
<ha-button
data-dialog="close"
slot="secondaryAction"
variant="plain"
>Cancel</ha-button
>
<ha-button data-dialog="close" slot="primaryAction" variant="accent"
>Submit</ha-button
>
</ha-dialog-footer>
</ha-wa-dialog>
<ha-wa-dialog
.open=${this._openDialog === "actions"}
header-title="Dialog with actions"
header-subtitle="This is a dialog with header actions"
@closed=${this._handleClosed}
>
<div slot="headerActionItems">
<ha-icon-button label="Settings" path=${mdiCog}></ha-icon-button>
<ha-icon-button label="Help" path=${mdiHelp}></ha-icon-button>
</div>
<div>Dialog content</div>
</ha-wa-dialog>
<h2>Design</h2>
<h3>Width</h3>
<p>There are multiple widths available for the dialog.</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>small</code></td>
<td><code>min(320px, var(--full-width))</code></td>
</tr>
<tr>
<td><code>medium</code></td>
<td><code>min(580px, var(--full-width))</code></td>
</tr>
<tr>
<td><code>large</code></td>
<td><code>min(720px, var(--full-width))</code></td>
</tr>
<tr>
<td><code>full</code></td>
<td><code>var(--full-width)</code></td>
</tr>
</tbody>
</table>
<p>
<code>--full-width</code> is calculated based on the available width
of the screen. 95vw is the maximum width of the dialog on a large
screen, while on a small screen it is 100vw minus the safe area
insets.
</p>
<p>Dialogs have a default width of <code>medium</code>.</p>
<h3>Prevent scrim close</h3>
<p>
You can prevent the dialog from being closed by clicking the
scrim/overlay. This is allowed by default.
</p>
<h3>Header</h3>
<p>The header contains a title, a subtitle and action items.</p>
<table>
<thead>
<tr>
<th>Slot</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>header</code></td>
<td>The entire header area.</td>
</tr>
<tr>
<td><code>headerTitle</code></td>
<td>The header title text.</td>
</tr>
<tr>
<td><code>headerSubtitle</code></td>
<td>The header subtitle text.</td>
</tr>
<tr>
<td><code>headerActionItems</code></td>
<td>The header action items.</td>
</tr>
</tbody>
</table>
<h4>Header title</h4>
<p>The header title is a text string.</p>
<h4>Header subtitle</h4>
<p>The header subtitle is a text string.</p>
<h4>Header action items</h4>
<p>
The header action items usually containing icon buttons and/or menu
buttons.
</p>
<h3>Body</h3>
<p>The body is the content of the dialog.</p>
<h3>Footer</h3>
<p>The footer is the footer of the dialog.</p>
<p>
It is recommended to use the <code>ha-dialog-footer</code> component
for the footer and to style the buttons inside the footer as so:
</p>
<table>
<thead>
<tr>
<th>Slot</th>
<th>Description</th>
<th>Variant to use</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>secondaryAction</code></td>
<td>The secondary action button(s).</td>
<td><code>plain</code></td>
</tr>
<tr>
<td><code>primaryAction</code></td>
<td>The primary action button(s).</td>
<td><code>accent</code></td>
</tr>
</tbody>
</table>
<h2>Implementation</h2>
<h3>Example Usage</h3>
<pre><code>&lt;ha-wa-dialog
open
header-title="Dialog title"
header-subtitle="Dialog subtitle"
prevent-scrim-close
&gt;
&lt;div slot="headerActionItems"&gt;
&lt;ha-icon-button label="Settings" path="mdiCog"&gt;&lt;/ha-icon-button&gt;
&lt;ha-icon-button label="Help" path="mdiHelp"&gt;&lt;/ha-icon-button&gt;
&lt;/div&gt;
&lt;div&gt;Dialog content&lt;/div&gt;
&lt;ha-dialog-footer slot="footer"&gt;
&lt;ha-button data-dialog="close" slot="secondaryAction" variant="plain"
&gt;Cancel&lt;/ha-button
&gt;
&lt;ha-button slot="primaryAction" variant="accent"&gt;Submit&lt;/ha-button&gt;
&lt;/ha-dialog-footer&gt;
&lt;/ha-wa-dialog&gt;</code></pre>
<h3>API</h3>
<p>
This component is based on the webawesome dialog component. Check the
<a
href="https://webawesome.com/docs/components/dialog/"
target="_blank"
rel="noopener noreferrer"
>webawesome documentation</a
>
for more details.
</p>
<h4>Attributes</h4>
<table>
<thead>
<tr>
<th>Attribute</th>
<th>Description</th>
<th>Default</th>
<th>Options</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>open</code></td>
<td>Controls the dialog open state.</td>
<td><code>false</code></td>
<td><code>false</code>, <code>true</code></td>
</tr>
<tr>
<td><code>width</code></td>
<td>Preferred dialog width preset.</td>
<td><code>medium</code></td>
<td>
<code>small</code>, <code>medium</code>, <code>large</code>,
<code>full</code>
</td>
</tr>
<tr>
<td><code>prevent-scrim-close</code></td>
<td>
Prevents closing the dialog by clicking the scrim/overlay.
</td>
<td><code>false</code></td>
<td><code>true</code></td>
</tr>
<tr>
<td><code>header-title</code></td>
<td>Header title text when no custom title slot is provided.</td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>header-subtitle</code></td>
<td>
Header subtitle text when no custom subtitle slot is provided.
</td>
<td></td>
<td></td>
</tr>
<tr>
<td><code>header-subtitle-position</code></td>
<td>Position of the subtitle relative to the title.</td>
<td><code>below</code></td>
<td><code>above</code>, <code>below</code></td>
</tr>
<tr>
<td><code>flexcontent</code></td>
<td>
Makes the dialog body a flex container for flexible layouts.
</td>
<td><code>false</code></td>
<td><code>false</code>, <code>true</code></td>
</tr>
</tbody>
</table>
<h4>CSS Custom Properties</h4>
<table>
<thead>
<tr>
<th>CSS Property</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>--dialog-content-padding</code></td>
<td>Padding for dialog content sections.</td>
</tr>
<tr>
<td><code>--ha-dialog-show-duration</code></td>
<td>Show animation duration.</td>
</tr>
<tr>
<td><code>--ha-dialog-hide-duration</code></td>
<td>Hide animation duration.</td>
</tr>
<tr>
<td><code>--ha-dialog-surface-background</code></td>
<td>Dialog background color.</td>
</tr>
<tr>
<td><code>--ha-dialog-border-radius</code></td>
<td>Border radius of the dialog surface.</td>
</tr>
<tr>
<td><code>--dialog-z-index</code></td>
<td>Z-index for the dialog.</td>
</tr>
<tr>
<td><code>--dialog-surface-position</code></td>
<td>CSS position of the dialog surface.</td>
</tr>
<tr>
<td><code>--dialog-surface-margin-top</code></td>
<td>Top margin for the dialog surface.</td>
</tr>
</tbody>
</table>
<h4>Events</h4>
<table>
<thead>
<tr>
<th>Event</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>opened</code></td>
<td>Fired when the dialog is shown.</td>
</tr>
<tr>
<td><code>closed</code></td>
<td>Fired after the dialog is hidden.</td>
</tr>
</tbody>
</table>
</div>
`;
}
private _handleOpenDialog = (dialog: DialogType) => () => {
this._openDialog = dialog;
};
private _handleClosed = () => {
this._openDialog = false;
};
static styles = [
css`
:host {
display: block;
padding: var(--ha-space-4);
}
.content {
max-width: 1000px;
margin: 0 auto;
}
h1 {
margin-top: 0;
margin-bottom: var(--ha-space-2);
}
h2 {
margin-top: var(--ha-space-6);
margin-bottom: var(--ha-space-3);
}
h3,
h4 {
margin-top: var(--ha-space-4);
margin-bottom: var(--ha-space-2);
}
p {
margin: var(--ha-space-2) 0;
line-height: 1.6;
}
.subtitle {
color: var(--secondary-text-color);
font-size: 1.1em;
margin-bottom: var(--ha-space-4);
}
table {
width: 100%;
border-collapse: collapse;
margin: var(--ha-space-3) 0;
}
th,
td {
text-align: left;
padding: var(--ha-space-2);
border-bottom: 1px solid var(--divider-color);
}
th {
font-weight: 500;
}
code {
background-color: var(--secondary-background-color);
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
font-size: 0.9em;
}
pre {
background-color: var(--secondary-background-color);
padding: var(--ha-space-3);
border-radius: 8px;
overflow-x: auto;
margin: var(--ha-space-3) 0;
}
pre code {
background-color: transparent;
padding: 0;
}
.buttons {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: var(--ha-space-2);
margin: var(--ha-space-4) 0;
}
a {
color: var(--primary-color);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"demo-components-ha-wa-dialog": DemoHaWaDialog;
}
}

View File

@@ -34,25 +34,25 @@
"@codemirror/legacy-modes": "6.5.2",
"@codemirror/search": "6.5.11",
"@codemirror/state": "6.5.2",
"@codemirror/view": "6.38.5",
"@codemirror/view": "6.38.4",
"@date-fns/tz": "1.4.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "6.18.2",
"@formatjs/intl-displaynames": "6.8.13",
"@formatjs/intl-durationformat": "0.7.6",
"@formatjs/intl-getcanonicallocales": "2.5.6",
"@formatjs/intl-listformat": "7.7.13",
"@formatjs/intl-locale": "4.2.13",
"@formatjs/intl-numberformat": "8.15.6",
"@formatjs/intl-pluralrules": "5.4.6",
"@formatjs/intl-relativetimeformat": "11.4.13",
"@formatjs/intl-datetimeformat": "6.18.0",
"@formatjs/intl-displaynames": "6.8.11",
"@formatjs/intl-durationformat": "0.7.4",
"@formatjs/intl-getcanonicallocales": "2.5.5",
"@formatjs/intl-listformat": "7.7.11",
"@formatjs/intl-locale": "4.2.11",
"@formatjs/intl-numberformat": "8.15.4",
"@formatjs/intl-pluralrules": "5.4.4",
"@formatjs/intl-relativetimeformat": "11.4.11",
"@fullcalendar/core": "6.1.19",
"@fullcalendar/daygrid": "6.1.19",
"@fullcalendar/interaction": "6.1.19",
"@fullcalendar/list": "6.1.19",
"@fullcalendar/luxon3": "6.1.19",
"@fullcalendar/timegrid": "6.1.19",
"@home-assistant/webawesome": "3.0.0-beta.6.ha.4",
"@home-assistant/webawesome": "3.0.0-beta.6.ha.1",
"@lezer/highlight": "1.2.1",
"@lit-labs/motion": "1.0.9",
"@lit-labs/observers": "2.0.6",
@@ -99,7 +99,7 @@
"barcode-detector": "3.0.6",
"color-name": "2.0.2",
"comlink": "4.4.2",
"core-js": "3.46.0",
"core-js": "3.45.1",
"cropperjs": "1.6.2",
"culori": "4.0.2",
"date-fns": "4.1.0",
@@ -114,7 +114,7 @@
"hls.js": "1.6.13",
"home-assistant-js-websocket": "9.5.0",
"idb-keyval": "6.2.2",
"intl-messageformat": "10.7.18",
"intl-messageformat": "10.7.16",
"js-yaml": "4.1.0",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
@@ -122,7 +122,7 @@
"lit": "3.3.1",
"lit-html": "3.3.1",
"luxon": "3.7.2",
"marked": "16.4.0",
"marked": "16.3.0",
"memoize-one": "6.0.0",
"node-vibrant": "4.0.3",
"object-hash": "3.0.0",
@@ -135,7 +135,7 @@
"stacktrace-js": "2.0.2",
"superstruct": "2.0.2",
"tinykeys": "3.0.0",
"ua-parser-js": "2.0.6",
"ua-parser-js": "2.0.5",
"vue": "2.7.16",
"vue2-daterange-picker": "0.6.8",
"weekstart": "2.0.0",
@@ -152,7 +152,7 @@
"@babel/helper-define-polyfill-provider": "0.6.5",
"@babel/plugin-transform-runtime": "7.28.3",
"@babel/preset-env": "7.28.3",
"@bundle-stats/plugin-webpack-filter": "4.21.5",
"@bundle-stats/plugin-webpack-filter": "4.21.4",
"@lokalise/node-api": "15.3.0",
"@octokit/auth-oauth-device": "8.0.2",
"@octokit/plugin-retry": "8.0.2",
@@ -183,7 +183,7 @@
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
"del": "8.0.1",
"eslint": "9.37.0",
"eslint": "9.36.0",
"eslint-config-airbnb-base": "15.0.0",
"eslint-config-prettier": "10.1.8",
"eslint-import-resolver-webpack": "0.13.10",
@@ -217,7 +217,7 @@
"terser-webpack-plugin": "5.3.14",
"ts-lit-plugin": "2.0.2",
"typescript": "5.9.3",
"typescript-eslint": "8.46.0",
"typescript-eslint": "8.45.0",
"vite-tsconfig-paths": "5.1.4",
"vitest": "3.2.4",
"webpack-stats-plugin": "1.1.3",

View File

@@ -9,7 +9,7 @@
":semanticCommitsDisabled",
"group:monorepos",
"group:recommended",
"security:minimumReleaseAgeNpm"
"npm:unpublishSafe"
],
"enabledManagers": ["npm", "nvm"],
"postUpdateOptions": ["yarnDedupeHighest"],

View File

@@ -1,5 +1,4 @@
#!/bin/sh
# Pushes a new version to PyPi.
# Stop on errors
set -e
@@ -12,5 +11,4 @@ yarn install
script/build_frontend
rm -rf dist home_assistant_frontend.egg-info
python3 -m build
python3 -m twine upload dist/*.whl --skip-existing
python3 -m build -q

View File

@@ -61,9 +61,3 @@ export const computeEntityEntryName = (
return name;
};
export const entityUseDeviceName = (
stateObj: HassEntity,
entities: HomeAssistant["entities"],
devices: HomeAssistant["devices"]
): boolean => !computeEntityName(stateObj, entities, devices);

View File

@@ -1,104 +0,0 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../../types";
import { ensureArray } from "../array/ensure-array";
import { computeAreaName } from "./compute_area_name";
import { computeDeviceName } from "./compute_device_name";
import { computeEntityName, entityUseDeviceName } from "./compute_entity_name";
import { computeFloorName } from "./compute_floor_name";
import { getEntityContext } from "./context/get_entity_context";
const DEFAULT_SEPARATOR = " ";
export type EntityNameItem =
| {
type: "entity" | "device" | "area" | "floor";
}
| {
type: "text";
text: string;
};
export interface EntityNameOptions {
separator?: string;
}
export const computeEntityNameDisplay = (
stateObj: HassEntity,
name: EntityNameItem | EntityNameItem[],
entities: HomeAssistant["entities"],
devices: HomeAssistant["devices"],
areas: HomeAssistant["areas"],
floors: HomeAssistant["floors"],
options?: EntityNameOptions
) => {
let items = ensureArray(name);
const separator = options?.separator ?? DEFAULT_SEPARATOR;
// If all items are text, just join them
if (items.every((n) => n.type === "text")) {
return items.map((item) => item.text).join(separator);
}
const useDeviceName = entityUseDeviceName(stateObj, entities, devices);
// If entity uses device name, and device is not already included, replace it with device name
if (useDeviceName) {
const hasDevice = items.some((n) => n.type === "device");
if (!hasDevice) {
items = items.map((n) => (n.type === "entity" ? { type: "device" } : n));
}
}
const names = computeEntityNameList(
stateObj,
items,
entities,
devices,
areas,
floors
);
// If after processing there is only one name, return that
if (names.length === 1) {
return names[0] || "";
}
return names.filter((n) => n).join(separator);
};
export const computeEntityNameList = (
stateObj: HassEntity,
name: EntityNameItem[],
entities: HomeAssistant["entities"],
devices: HomeAssistant["devices"],
areas: HomeAssistant["areas"],
floors: HomeAssistant["floors"]
): (string | undefined)[] => {
const { device, area, floor } = getEntityContext(
stateObj,
entities,
devices,
areas,
floors
);
const names = name.map((item) => {
switch (item.type) {
case "entity":
return computeEntityName(stateObj, entities, devices);
case "device":
return device ? computeDeviceName(device) : undefined;
case "area":
return area ? computeAreaName(area) : undefined;
case "floor":
return floor ? computeFloorName(floor) : undefined;
case "text":
return item.text;
default:
return "";
}
});
return names;
};

View File

@@ -1,12 +1,13 @@
import type { HassConfig, HassEntity } from "home-assistant-js-websocket";
import type { FrontendLocaleData } from "../../data/translation";
import type { HomeAssistant } from "../../types";
import {
computeEntityNameDisplay,
type EntityNameItem,
type EntityNameOptions,
} from "../entity/compute_entity_name_display";
import type { LocalizeFunc } from "./localize";
import { computeEntityName } from "../entity/compute_entity_name";
import { computeDeviceName } from "../entity/compute_device_name";
import { getEntityContext } from "../entity/context/get_entity_context";
import { computeAreaName } from "../entity/compute_area_name";
import { computeFloorName } from "../entity/compute_floor_name";
import { ensureArray } from "../array/ensure-array";
export type FormatEntityStateFunc = (
stateObj: HassEntity,
@@ -26,8 +27,8 @@ export type EntityNameType = "entity" | "device" | "area" | "floor";
export type FormatEntityNameFunc = (
stateObj: HassEntity,
name: EntityNameItem | EntityNameItem[],
options?: EntityNameOptions
type: EntityNameType | EntityNameType[],
separator?: string
) => string;
export const computeFormatFunctions = async (
@@ -74,15 +75,45 @@ export const computeFormatFunctions = async (
),
formatEntityAttributeName: (stateObj, attribute) =>
computeAttributeNameDisplay(localize, stateObj, entities, attribute),
formatEntityName: (stateObj, name, options) =>
computeEntityNameDisplay(
formatEntityName: (stateObj, type, separator = " ") => {
const types = ensureArray(type);
const namesList: (string | undefined)[] = [];
const { device, area, floor } = getEntityContext(
stateObj,
name,
entities,
devices,
areas,
floors,
options
),
floors
);
for (const t of types) {
switch (t) {
case "entity": {
namesList.push(computeEntityName(stateObj, entities, devices));
break;
}
case "device": {
if (device) {
namesList.push(computeDeviceName(device));
}
break;
}
case "area": {
if (area) {
namesList.push(computeAreaName(area));
}
break;
}
case "floor": {
if (floor) {
namesList.push(computeFloorName(floor));
}
break;
}
}
}
return namesList.filter((name) => name !== undefined).join(separator);
},
};
};

View File

@@ -1,8 +0,0 @@
import xss from "xss";
export const filterXSS = (html: string) =>
xss(html, {
whiteList: {},
stripIgnoreTag: true,
stripIgnoreTagBody: true,
});

View File

@@ -27,7 +27,6 @@ import type { HomeAssistant } from "../../types";
import { isMac } from "../../util/is_mac";
import "../chips/ha-assist-chip";
import "../ha-icon-button";
import { filterXSS } from "../../common/util/xss";
import { formatTimeLabel } from "./axis-label";
import { downSampleLineData } from "./down-sample";
@@ -812,8 +811,7 @@ export class HaChartBase extends LitElement {
};
}
}
const name = filterXSS(String(s.name ?? s.id ?? ""));
return { ...s, name, data };
return { ...s, data };
});
return series as ECOption["series"];
}

View File

@@ -9,7 +9,6 @@ import { ResizeController } from "@lit-labs/observers/resize-controller";
import type { HomeAssistant } from "../../types";
import type { ECOption } from "../../resources/echarts";
import { measureTextWidth } from "../../util/text";
import { filterXSS } from "../../common/util/xss";
import "./ha-chart-base";
import { NODE_SIZE } from "../trace/hat-graph-const";
import "../ha-alert";
@@ -93,12 +92,12 @@ export class HaSankeyChart extends LitElement {
: data.value;
if (data.id) {
const node = this.data.nodes.find((n) => n.id === data.id);
return `${params.marker} ${filterXSS(node?.label ?? data.id)}<br>${value}`;
return `${params.marker} ${node?.label ?? data.id}<br>${value}`;
}
if (data.source && data.target) {
const source = this.data.nodes.find((n) => n.id === data.source);
const target = this.data.nodes.find((n) => n.id === data.target);
return `${filterXSS(source?.label ?? data.source)}${filterXSS(target?.label ?? data.target)}<br>${value}`;
return `${source?.label ?? data.source}${target?.label ?? data.target}<br>${value}`;
}
return null;
};

View File

@@ -1,9 +1,6 @@
import { expose } from "comlink";
import Fuse from "fuse.js";
import memoizeOne from "memoize-one";
import { ipCompare, stringCompare } from "../../common/string/compare";
import { stringCompare, ipCompare } from "../../common/string/compare";
import { stripDiacritics } from "../../common/string/strip-diacritics";
import { HaFuse } from "../../resources/fuse";
import type {
ClonedDataTableColumnData,
DataTableRowData,
@@ -11,48 +8,29 @@ import type {
SortingDirection,
} from "./ha-data-table";
const fuseIndex = memoizeOne(
(data: DataTableRowData[], columns: SortableColumnContainer) => {
const searchKeys = new Set<string>();
Object.entries(columns).forEach(([key, column]) => {
if (column.filterable) {
searchKeys.add(
column.filterKey
? `${column.valueColumn || key}.${column.filterKey}`
: key
);
}
});
return Fuse.createIndex([...searchKeys], data);
}
);
const filterData = (
data: DataTableRowData[],
columns: SortableColumnContainer,
filter: string
) => {
filter = stripDiacritics(filter.toLowerCase());
return data.filter((row) =>
Object.entries(columns).some((columnEntry) => {
const [key, column] = columnEntry;
if (column.filterable) {
const value = String(
column.filterKey
? row[column.valueColumn || key][column.filterKey]
: row[column.valueColumn || key]
);
if (filter === "") {
return data;
}
const index = fuseIndex(data, columns);
const fuse = new HaFuse(
data,
{ shouldSort: false, minMatchCharLength: 1 },
index
if (stripDiacritics(value).toLowerCase().includes(filter)) {
return true;
}
}
return false;
})
);
const searchResults = fuse.multiTermsSearch(filter);
if (searchResults) {
return searchResults.map((result) => result.item);
}
return data;
};
const sortData = (

View File

@@ -1,493 +0,0 @@
import "@material/mwc-menu/mwc-menu-surface";
import { mdiDrag, mdiPlus } from "@mdi/js";
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import type { IFuseOptions } from "fuse.js";
import Fuse from "fuse.js";
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 { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import type { EntityNameItem } from "../../common/entity/compute_entity_name_display";
import { getEntityContext } from "../../common/entity/context/get_entity_context";
import type { EntityNameType } from "../../common/translations/entity-state";
import type { LocalizeKeys } from "../../common/translations/localize";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../chips/ha-assist-chip";
import "../chips/ha-chip-set";
import "../chips/ha-input-chip";
import "../ha-combo-box";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-sortable";
interface EntityNameOption {
primary: string;
secondary?: string;
value: string;
}
const rowRenderer: ComboBoxLitRenderer<EntityNameOption> = (item) => html`
<ha-combo-box-item type="button">
<span slot="headline">${item.primary}</span>
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
</ha-combo-box-item>
`;
const KNOWN_TYPES = new Set(["entity", "device", "area", "floor"]);
const UNIQUE_TYPES = new Set(["entity", "device", "area", "floor"]);
@customElement("ha-entity-name-picker")
export class HaEntityNamePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entityId?: string;
@property({ attribute: false }) public value?:
| string
| EntityNameItem
| EntityNameItem[];
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public required = false;
@property({ type: Boolean, reflect: true }) public disabled = false;
@query(".container", true) private _container?: HTMLDivElement;
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
@state() private _opened = false;
private _editIndex?: number;
private _validOptions = memoizeOne((entityId?: string) => {
const options = new Set<string>();
if (!entityId) {
return options;
}
const stateObj = this.hass.states[entityId];
if (!stateObj) {
return options;
}
options.add("entity");
const context = getEntityContext(
stateObj,
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
if (context.device) options.add("device");
if (context.area) options.add("area");
if (context.floor) options.add("floor");
return options;
});
private _getOptions = memoizeOne((entityId?: string) => {
if (!entityId) {
return [];
}
const options = this._validOptions(entityId);
const items = (
["entity", "device", "area", "floor"] as const
).map<EntityNameOption>((name) => {
const stateObj = this.hass.states[entityId];
const isValid = options.has(name);
const primary = this.hass.localize(
`ui.components.entity.entity-name-picker.types.${name}`
);
const secondary =
stateObj && isValid
? this.hass.formatEntityName(stateObj, { type: name })
: this.hass.localize(
`ui.components.entity.entity-name-picker.types.${name}_missing` as LocalizeKeys
) || "-";
return {
primary,
secondary,
value: name,
};
});
return items;
});
private _formatItem = (item: EntityNameItem) => {
if (item.type === "text") {
return `"${item.text}"`;
}
if (KNOWN_TYPES.has(item.type)) {
return this.hass.localize(
`ui.components.entity.entity-name-picker.types.${item.type as EntityNameType}`
);
}
return item.type;
};
protected render() {
const value = this._value;
const options = this._getOptions(this.entityId);
const validOptions = this._validOptions(this.entityId);
return html`
${this.label ? html`<label>${this.label}</label>` : nothing}
<div class="container">
<ha-sortable
no-style
@item-moved=${this._moveItem}
.disabled=${this.disabled}
handle-selector="button.primary.action"
filter=".add"
>
<ha-chip-set>
${repeat(
this._value,
(item) => item,
(item: EntityNameItem, idx) => {
const label = this._formatItem(item);
const isValid =
item.type === "text" || validOptions.has(item.type);
return html`
<ha-input-chip
data-idx=${idx}
@remove=${this._removeItem}
@click=${this._editItem}
.label=${label}
.selected=${!this.disabled}
.disabled=${this.disabled}
class=${!isValid ? "invalid" : ""}
>
<ha-svg-icon slot="icon" .path=${mdiDrag}></ha-svg-icon>
<span>${label}</span>
</ha-input-chip>
`;
}
)}
${this.disabled
? nothing
: html`
<ha-assist-chip
@click=${this._addItem}
.disabled=${this.disabled}
label=${this.hass.localize(
"ui.components.entity.entity-name-picker.add"
)}
class="add"
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-assist-chip>
`}
</ha-chip-set>
</ha-sortable>
<mwc-menu-surface
.open=${this._opened}
@closed=${this._onClosed}
@opened=${this._onOpened}
@input=${stopPropagation}
.anchor=${this._container}
>
<ha-combo-box
.hass=${this.hass}
.value=${""}
.autofocus=${this.autofocus}
.disabled=${this.disabled || !this.entityId}
.required=${this.required && !value.length}
.helper=${this.helper}
.items=${options}
allow-custom-value
item-id-path="value"
item-value-path="value"
item-label-path="primary"
.renderer=${rowRenderer}
@opened-changed=${this._openedChanged}
@value-changed=${this._comboBoxValueChanged}
@filter-changed=${this._filterChanged}
>
</ha-combo-box>
</mwc-menu-surface>
</div>
`;
}
private _onClosed(ev) {
ev.stopPropagation();
this._opened = false;
this._editIndex = undefined;
}
private async _onOpened(ev) {
if (!this._opened) {
return;
}
ev.stopPropagation();
this._opened = true;
await this._comboBox?.focus();
await this._comboBox?.open();
}
private async _addItem(ev) {
ev.stopPropagation();
this._opened = true;
}
private async _editItem(ev) {
ev.stopPropagation();
const idx = parseInt(ev.currentTarget.dataset.idx, 10);
this._editIndex = idx;
this._opened = true;
}
private get _value(): EntityNameItem[] {
return this._toItems(this.value);
}
private _toItems = memoizeOne((value?: typeof this.value) => {
if (typeof value === "string") {
return [{ type: "text", text: value } as const];
}
return value ? ensureArray(value) : [];
});
private _toValue = memoizeOne(
(items: EntityNameItem[]): typeof this.value => {
if (items.length === 0) {
return [];
}
if (items.length === 1) {
const item = items[0];
return item.type === "text" ? item.text : item;
}
return items;
}
);
private _openedChanged(ev: ValueChangedEvent<boolean>) {
const open = ev.detail.value;
if (open) {
const options = this._comboBox.items || [];
const initialItem =
this._editIndex != null ? this._value[this._editIndex] : undefined;
const initialValue = initialItem
? initialItem.type === "text"
? initialItem.text
: initialItem.type
: "";
const filteredItems = this._filterSelectedOptions(options, initialValue);
this._comboBox.filteredItems = filteredItems;
this._comboBox.setInputValue(initialValue);
} else {
this._opened = false;
}
}
private _filterSelectedOptions = (
options: EntityNameOption[],
current?: string
) => {
const value = this._value;
const types = value.map((item) => item.type) as string[];
const filteredOptions = options.filter(
(option) =>
!UNIQUE_TYPES.has(option.value) ||
!types.includes(option.value) ||
option.value === current
);
return filteredOptions;
};
private _filterChanged(ev: ValueChangedEvent<string>) {
const input = ev.detail.value;
const filter = input?.toLowerCase() || "";
const options = this._comboBox.items || [];
const currentItem =
this._editIndex != null ? this._value[this._editIndex] : undefined;
const currentValue = currentItem
? currentItem.type === "text"
? currentItem.text
: currentItem.type
: "";
this._comboBox.filteredItems = this._filterSelectedOptions(
options,
currentValue
);
if (!filter) {
return;
}
const fuseOptions: IFuseOptions<EntityNameOption> = {
keys: ["primary", "secondary", "value"],
isCaseSensitive: false,
minMatchCharLength: Math.min(filter.length, 2),
threshold: 0.2,
ignoreDiacritics: true,
};
const fuse = new Fuse(this._comboBox.filteredItems, fuseOptions);
const filteredItems = fuse.search(filter).map((result) => result.item);
this._comboBox.filteredItems = filteredItems;
}
private async _moveItem(ev: CustomEvent) {
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;
const value = this._value;
const newValue = value.concat();
const element = newValue.splice(oldIndex, 1)[0];
newValue.splice(newIndex, 0, element);
this._setValue(newValue);
await this.updateComplete;
this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
}
private async _removeItem(ev) {
ev.stopPropagation();
const value = [...this._value];
const idx = parseInt(ev.target.dataset.idx, 10);
value.splice(idx, 1);
this._setValue(value);
await this.updateComplete;
this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
}
private _comboBoxValueChanged(ev: ValueChangedEvent<string>): void {
ev.stopPropagation();
const value = ev.detail.value;
if (this.disabled || value === "") {
return;
}
const item: EntityNameItem = KNOWN_TYPES.has(value as any)
? { type: value as EntityNameType }
: { type: "text", text: value };
const newValue = [...this._value];
if (this._editIndex != null) {
newValue[this._editIndex] = item;
} else {
newValue.push(item);
}
this._setValue(newValue);
}
private _setValue(value: EntityNameItem[]) {
const newValue = this._toValue(value);
this.value = newValue;
fireEvent(this, "value-changed", {
value: newValue,
});
}
static styles = css`
:host {
position: relative;
width: 100%;
}
.container {
position: relative;
background-color: var(--mdc-text-field-fill-color, whitesmoke);
border-radius: var(--ha-border-radius-sm);
border-end-end-radius: var(--ha-border-radius-square);
border-end-start-radius: var(--ha-border-radius-square);
}
.container:after {
display: block;
content: "";
position: absolute;
pointer-events: none;
bottom: 0;
left: 0;
right: 0;
height: 1px;
width: 100%;
background-color: var(
--mdc-text-field-idle-line-color,
rgba(0, 0, 0, 0.42)
);
transform:
height 180ms ease-in-out,
background-color 180ms ease-in-out;
}
:host([disabled]) .container:after {
background-color: var(
--mdc-text-field-disabled-line-color,
rgba(0, 0, 0, 0.42)
);
}
.container:focus-within:after {
height: 2px;
background-color: var(--mdc-theme-primary);
}
label {
display: block;
margin: 0 0 var(--ha-space-2);
}
.add {
order: 1;
}
mwc-menu-surface {
--mdc-menu-min-width: 100%;
}
ha-chip-set {
padding: var(--ha-space-2) var(--ha-space-2);
}
.invalid {
text-decoration: line-through;
}
.sortable-fallback {
display: none;
opacity: 0;
}
.sortable-ghost {
opacity: 0.4;
}
.sortable-drag {
cursor: grabbing;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-entity-name-picker": HaEntityNamePicker;
}
}

View File

@@ -6,7 +6,6 @@ import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
import { computeStateName } from "../../common/entity/compute_state_name";
import { isValidEntityId } from "../../common/entity/valid_entity_id";
import { computeRTL } from "../../common/util/compute_rtl";
@@ -145,14 +144,9 @@ export class HaEntityPicker extends LitElement {
`;
}
const [entityName, deviceName, areaName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
const entityName = this.hass.formatEntityName(stateObj, "entity");
const deviceName = this.hass.formatEntityName(stateObj, "device");
const areaName = this.hass.formatEntityName(stateObj, "area");
const isRTL = computeRTL(this.hass);
@@ -306,24 +300,21 @@ export class HaEntityPicker extends LitElement {
);
}
const isRTL = computeRTL(hass);
const isRTL = computeRTL(this.hass);
items = entityIds.map<EntityComboBoxItem>((entityId) => {
const stateObj = hass.states[entityId];
const stateObj = hass!.states[entityId];
const friendlyName = computeStateName(stateObj); // Keep this for search
const entityName = this.hass.formatEntityName(stateObj, "entity");
const deviceName = this.hass.formatEntityName(stateObj, "device");
const areaName = this.hass.formatEntityName(stateObj, "area");
const [entityName, deviceName, areaName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
hass.entities,
hass.devices,
hass.areas,
hass.floors
const domainName = domainToName(
this.hass.localize,
computeDomain(entityId)
);
const domainName = domainToName(hass.localize, computeDomain(entityId));
const primary = entityName || deviceName || entityId;
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)

View File

@@ -6,7 +6,6 @@ import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
import { computeStateName } from "../../common/entity/compute_state_name";
import { computeRTL } from "../../common/util/compute_rtl";
import { domainToName } from "../../data/integration";
@@ -21,6 +20,7 @@ import "../ha-combo-box-item";
import "../ha-generic-picker";
import type { HaGenericPicker } from "../ha-generic-picker";
import "../ha-icon-button";
import "../ha-input-helper-text";
import type {
PickerComboBoxItem,
PickerComboBoxSearchFn,
@@ -199,7 +199,7 @@ export class HaStatisticPicker extends LitElement {
});
}
const isRTL = computeRTL(hass);
const isRTL = computeRTL(this.hass);
const output: StatisticComboBoxItem[] = [];
@@ -256,15 +256,9 @@ export class HaStatisticPicker extends LitElement {
const id = meta.statistic_id;
const friendlyName = computeStateName(stateObj); // Keep this for search
const [entityName, deviceName, areaName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
hass.entities,
hass.devices,
hass.areas,
hass.floors
);
const entityName = hass.formatEntityName(stateObj, "entity");
const deviceName = hass.formatEntityName(stateObj, "device");
const areaName = hass.formatEntityName(stateObj, "area");
const primary = entityName || deviceName || id;
const secondary = [areaName, entityName ? deviceName : undefined]
@@ -337,14 +331,9 @@ export class HaStatisticPicker extends LitElement {
const stateObj = this.hass.states[statisticId];
if (stateObj) {
const [entityName, deviceName, areaName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
const entityName = this.hass.formatEntityName(stateObj, "entity");
const deviceName = this.hass.formatEntityName(stateObj, "device");
const areaName = this.hass.formatEntityName(stateObj, "area");
const isRTL = computeRTL(this.hass);
@@ -476,7 +465,6 @@ export class HaStatisticPicker extends LitElement {
.hideClearIcon=${this.hideClearIcon}
.searchFn=${this._searchFn}
.valueRenderer=${this._valueRenderer}
.helper=${this.helper}
@value-changed=${this._valueChanged}
>
</ha-generic-picker>

View File

@@ -41,7 +41,8 @@ export class HaButton extends Button {
return [
Button.styles,
css`
:host {
.button {
/* set theme vars */
--wa-form-control-padding-inline: 16px;
--wa-font-weight-action: var(--ha-font-weight-medium);
--wa-form-control-border-radius: var(
@@ -53,8 +54,7 @@ export class HaButton extends Button {
--ha-button-height,
var(--button-height, 40px)
);
}
.button {
font-size: var(--ha-font-size-m);
line-height: 1;
@@ -223,12 +223,6 @@ export class HaButton extends Button {
.button.has-end {
padding-inline-end: 8px;
}
.label {
overflow: hidden;
text-overflow: ellipsis;
padding: var(--ha-space-1) 0;
}
`,
];
}

View File

@@ -1,52 +0,0 @@
import { css, html, LitElement } from "lit";
import { customElement } from "lit/decorators";
/**
* Home Assistant dialog footer component
*
* @element ha-dialog-footer
* @extends {LitElement}
*
* @summary
* A simple footer container for dialog actions,
* typically used as the `footer` slot in `ha-wa-dialog`.
*
* @slot primaryAction - Primary action button(s), aligned to the end.
* @slot secondaryAction - Secondary action button(s), placed before the primary action.
*
* @remarks
* **Button Styling Guidance:**
* - `primaryAction` slot: Use `variant="accent"`
* - `secondaryAction` slot: Use `variant="plain"`
*/
@customElement("ha-dialog-footer")
export class HaDialogFooter extends LitElement {
protected render() {
return html`
<footer>
<slot name="secondaryAction"></slot>
<slot name="primaryAction"></slot>
</footer>
`;
}
static get styles() {
return [
css`
footer {
display: flex;
gap: var(--ha-space-3);
justify-content: flex-end;
align-items: center;
width: 100%;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-dialog-footer": HaDialogFooter;
}
}

View File

@@ -1,20 +1,9 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement } from "lit/decorators";
@customElement("ha-dialog-header")
export class HaDialogHeader extends LitElement {
@property({ type: String, attribute: "subtitle-position" })
public subtitlePosition: "above" | "below" = "below";
protected render() {
const titleSlot = html`<div class="header-title">
<slot name="title"></slot>
</div>`;
const subtitleSlot = html`<div class="header-subtitle">
<slot name="subtitle"></slot>
</div>`;
return html`
<header class="header">
<div class="header-bar">
@@ -22,9 +11,12 @@ export class HaDialogHeader extends LitElement {
<slot name="navigationIcon"></slot>
</section>
<section class="header-content">
${this.subtitlePosition === "above"
? html`${subtitleSlot}${titleSlot}`
: html`${titleSlot}${subtitleSlot}`}
<div class="header-title">
<slot name="title"></slot>
</div>
<div class="header-subtitle">
<slot name="subtitle"></slot>
</div>
</section>
<section class="header-action-items">
<slot name="actionItems"></slot>
@@ -48,7 +40,7 @@ export class HaDialogHeader extends LitElement {
.header-bar {
display: flex;
flex-direction: row;
align-items: center;
align-items: flex-start;
padding: 4px;
box-sizing: border-box;
}
@@ -61,17 +53,13 @@ export class HaDialogHeader extends LitElement {
white-space: nowrap;
}
.header-title {
height: var(
--ha-dialog-header-title-height,
calc(var(--ha-font-size-xl) + 4px)
);
font-size: var(--ha-font-size-xl);
line-height: var(--ha-line-height-condensed);
font-weight: var(--ha-font-weight-medium);
font-weight: var(--ha-font-weight-normal);
}
.header-subtitle {
font-size: var(--ha-font-size-m);
line-height: var(--ha-line-height-normal);
line-height: 20px;
color: var(--secondary-text-color);
}
@media all and (min-width: 450px) and (min-height: 500px) {

View File

@@ -121,7 +121,7 @@ export class HaDialog extends DialogBase {
position: var(--dialog-surface-position, relative);
top: var(--dialog-surface-top);
margin-top: var(--dialog-surface-margin-top);
min-width: var(--mdc-dialog-min-width, auto);
min-width: var(--mdc-dialog-min-width, 100vw);
min-height: var(--mdc-dialog-min-height, auto);
border-radius: var(
--ha-dialog-border-radius,
@@ -133,13 +133,25 @@ export class HaDialog extends DialogBase {
--ha-dialog-surface-background,
var(--mdc-theme-surface, #fff)
);
padding: var(--dialog-surface-padding);
}
:host([flexContent]) .mdc-dialog .mdc-dialog__content {
display: flex;
flex-direction: column;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
.mdc-dialog .mdc-dialog__surface {
min-height: 100vh;
min-height: 100svh;
max-height: 100vh;
max-height: 100svh;
padding-top: var(--safe-area-inset-top);
padding-bottom: var(--safe-area-inset-bottom);
padding-left: var(--safe-area-inset-left);
padding-right: var(--safe-area-inset-right);
}
}
.header_title {
display: flex;
align-items: center;

View File

@@ -61,7 +61,6 @@ export class HaFormString extends LitElement implements HaFormElement {
.required=${this.schema.required}
.autoValidate=${this.schema.required}
.name=${this.schema.name}
.autofocus=${this.schema.autofocus}
.autocomplete=${this.schema.autocomplete}
.suffix=${this.isPassword
? // reserve some space for the icon.

View File

@@ -105,11 +105,6 @@ export class HaForm extends LitElement implements HaFormElement {
}
}
static shadowRootOptions: ShadowRootInit = {
mode: "open",
delegatesFocus: true,
};
protected render(): TemplateResult {
return html`
<div class="root" part="root">

View File

@@ -39,15 +39,6 @@ export class HaPictureUpload extends LitElement {
@property({ type: Boolean, attribute: "select-media" }) public selectMedia =
false;
// This property is set when this component is used inside a media selector.
// When set, it returns selected media or uploaded files as MediaSelectorValue
// When unset, it only allows selecting images from image-upload, and returns
// selected or uploaded images as a string starting with /api/...
@property({ type: Boolean, attribute: "full-media" }) public fullMedia =
false;
@property({ attribute: false }) public contentIdHelper?: string;
@property({ attribute: false }) public cropOptions?: CropOptions;
@property({ type: Boolean }) public original = false;
@@ -173,33 +164,12 @@ export class HaPictureUpload extends LitElement {
this._uploading = true;
try {
const media = await createImage(this.hass, file);
if (this.fullMedia) {
const item = {
media_content_id: `${MEDIA_PREFIX}/${media.id}`,
media_content_type: media.content_type,
title: media.name,
media_class: "image" as const,
can_play: true,
can_expand: false,
can_search: false,
thumbnail: generateImageThumbnailUrl(media.id, 256),
} as const;
const navigateIds = [
{},
{ media_content_type: "app", media_content_id: MEDIA_PREFIX },
];
fireEvent(this, "media-picked", {
item,
navigateIds,
});
} else {
this.value = generateImageThumbnailUrl(
media.id,
this.size,
this.original
);
fireEvent(this, "change");
}
this.value = generateImageThumbnailUrl(
media.id,
this.size,
this.original
);
fireEvent(this, "change");
} catch (err: any) {
showAlertDialog(this, {
text: err.toString(),
@@ -213,24 +183,15 @@ export class HaPictureUpload extends LitElement {
showMediaBrowserDialog(this, {
action: "pick",
entityId: "browser",
accept: ["image/*"],
navigateIds: this.fullMedia
? undefined
: [
{ media_content_id: undefined, media_content_type: undefined },
{
media_content_id: MEDIA_PREFIX,
media_content_type: "app",
},
],
minimumNavigateLevel: this.fullMedia ? undefined : 2,
hideContentType: true,
contentIdHelper: this.contentIdHelper,
navigateIds: [
{ media_content_id: undefined, media_content_type: undefined },
{
media_content_id: MEDIA_PREFIX,
media_content_type: "app",
},
],
minimumNavigateLevel: 2,
mediaPickedCallback: async (pickedMedia: MediaPickedEvent) => {
if (this.fullMedia) {
fireEvent(this, "media-picked", pickedMedia);
return;
}
const mediaId = getIdFromUrl(pickedMedia.item.media_content_id);
if (mediaId) {
if (this.crop) {

View File

@@ -220,7 +220,7 @@ export class HaResizableBottomSheet extends LitElement {
min-height: var(--min-height, 30dvh);
background-color: var(
--ha-bottom-sheet-surface-background,
var(--ha-color-surface-default)
var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff)),
);
display: flex;
flex-direction: column;

View File

@@ -1,50 +0,0 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { EntityNameSelector } from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../types";
import "../entity/ha-entity-name-picker";
@customElement("ha-selector-entity_name")
export class HaSelectorEntityName extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: EntityNameSelector;
@property() public value?: string | string[];
@property() public label?: string;
@property() public helper?: string;
@property({ type: Boolean }) public disabled = false;
@property({ type: Boolean }) public required = true;
@property({ attribute: false }) public context?: {
entity?: string;
};
protected render() {
const value = this.value ?? this.selector.entity_name?.default_name;
return html`
<ha-entity-name-picker
.hass=${this.hass}
.entityId=${this.selector.entity_name?.entity_id ||
this.context?.entity}
.value=${value}
.label=${this.label}
.helper=${this.helper}
.disabled=${this.disabled}
.required=${this.required}
></ha-entity-name-picker>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-entity_name": HaSelectorEntityName;
}
}

View File

@@ -19,7 +19,6 @@ import "../ha-form/ha-form";
import type { SchemaUnion } from "../ha-form/types";
import { showMediaBrowserDialog } from "../media-player/show-media-browser-dialog";
import { ensureArray } from "../../common/array/ensure-array";
import "../ha-picture-upload";
const MANUAL_SCHEMA = [
{ name: "media_content_id", required: false, selector: { text: {} } },
@@ -106,17 +105,6 @@ export class HaMediaSelector extends LitElement {
(stateObj &&
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA));
if (this.selector.media?.image_upload && !this.value) {
return html`<ha-picture-upload
.hass=${this.hass}
.value=${null}
.contentIdHelper=${this.selector.media?.content_id_helper}
select-media
full-media
@media-picked=${this._pictureUploadMediaPicked}
></ha-picture-upload>`;
}
return html`
${this._hasAccept ||
(this._contextEntities && this._contextEntities.length <= 1)
@@ -154,7 +142,8 @@ export class HaMediaSelector extends LitElement {
.computeHelper=${this._computeHelperCallback}
></ha-form>
`
: html`<ha-card
: html`
<ha-card
outlined
tabindex="0"
role="button"
@@ -214,20 +203,7 @@ export class HaMediaSelector extends LitElement {
</div>
</div>
</ha-card>
${this.selector.media?.clearable
? html`<div>
<ha-button
appearance="plain"
size="small"
variant="danger"
@click=${this._clearValue}
>
${this.hass.localize(
"ui.components.picture-upload.clear_picture"
)}
</ha-button>
</div>`
: nothing}`}
`}
`;
}
@@ -272,8 +248,6 @@ export class HaMediaSelector extends LitElement {
accept: this.selector.media?.accept,
defaultId: this.value?.media_content_id,
defaultType: this.value?.media_content_type,
hideContentType: this.selector.media?.hide_content_type,
contentIdHelper: this.selector.media?.content_id_helper,
mediaPickedCallback: (pickedMedia: MediaPickedEvent) => {
fireEvent(this, "value-changed", {
value: {
@@ -315,31 +289,6 @@ export class HaMediaSelector extends LitElement {
}
}
private _pictureUploadMediaPicked(ev) {
const pickedMedia = ev.detail as MediaPickedEvent;
fireEvent(this, "value-changed", {
value: {
...this.value,
media_content_id: pickedMedia.item.media_content_id,
media_content_type: pickedMedia.item.media_content_type,
metadata: {
title: pickedMedia.item.title,
thumbnail: pickedMedia.item.thumbnail,
media_class: pickedMedia.item.media_class,
children_media_class: pickedMedia.item.children_media_class,
navigateIds: pickedMedia.navigateIds?.map((id) => ({
media_content_type: id.media_content_type,
media_content_id: id.media_content_id,
})),
},
},
});
}
private _clearValue() {
fireEvent(this, "value-changed", { value: undefined });
}
static styles = css`
ha-entity-picker {
display: block;

View File

@@ -29,7 +29,6 @@ const LOAD_ELEMENTS = {
device: () => import("./ha-selector-device"),
duration: () => import("./ha-selector-duration"),
entity: () => import("./ha-selector-entity"),
entity_name: () => import("./ha-selector-entity-name"),
statistic: () => import("./ha-selector-statistic"),
file: () => import("./ha-selector-file"),
floor: () => import("./ha-selector-floor"),

View File

@@ -857,7 +857,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
justify-content: center;
align-items: center;
min-width: 8px;
border-radius: var(--ha-border-radius-xl);
border-radius: var(--ha-border-radius-md);
font-weight: var(--ha-font-weight-normal);
line-height: normal;
background-color: var(--accent-color);

View File

@@ -17,7 +17,7 @@ export class HaTooltip extends Tooltip {
css`
:host {
--wa-tooltip-background-color: var(--secondary-background-color);
--wa-tooltip-content-color: var(--primary-text-color);
--wa-tooltip-color: var(--primary-text-color);
--wa-tooltip-font-family: var(
--ha-tooltip-font-family,
var(--ha-font-family-body)

View File

@@ -1,320 +0,0 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "@home-assistant/webawesome/dist/components/dialog/dialog";
import { mdiClose } from "@mdi/js";
import "./ha-dialog-header";
import "./ha-icon-button";
import type { HomeAssistant } from "../types";
import { fireEvent } from "../common/dom/fire_event";
import { haStyleScrollbar } from "../resources/styles";
export type DialogWidth = "small" | "medium" | "large" | "full";
/**
* Home Assistant dialog component
*
* @element ha-wa-dialog
* @extends {LitElement}
*
* @summary
* A stylable dialog built using the `wa-dialog` component, providing a standardized header (ha-dialog-header),
* body, and footer (preferably using `ha-dialog-footer`) with slots
*
* You can open and close the dialog declaratively by using the `data-dialog="close"` attribute.
* @see https://webawesome.com/docs/components/dialog/#opening-and-closing-dialogs-declaratively
*
* @slot header - Replace the entire header area.
* @slot headerNavigationIcon - Leading header action (e.g. close/back button).
* @slot headerActionItems - Trailing header actions (e.g. buttons, menus).
* @slot - Dialog content body.
* @slot footer - Dialog footer content.
*
* @csspart dialog - The dialog surface.
* @csspart header - The header container.
* @csspart body - The scrollable body container.
* @csspart footer - The footer container.
*
* @cssprop --dialog-content-padding - Padding for the dialog content sections.
* @cssprop --ha-dialog-show-duration - Show animation duration.
* @cssprop --ha-dialog-hide-duration - Hide animation duration.
* @cssprop --ha-dialog-surface-background - Dialog background color.
* @cssprop --ha-dialog-border-radius - Border radius of the dialog surface.
* @cssprop --dialog-z-index - Z-index for the dialog.
* @cssprop --dialog-surface-position - CSS position of the dialog surface.
* @cssprop --dialog-surface-margin-top - Top margin for the dialog surface.
*
* @attr {boolean} open - Controls the dialog open state.
* @attr {("small"|"medium"|"large"|"full")} width - Preferred dialog width preset. Defaults to "medium".
* @attr {boolean} prevent-scrim-close - Prevents closing the dialog by clicking the scrim/overlay. Defaults to false.
* @attr {string} header-title - Header title text when no custom title slot is provided.
* @attr {string} header-subtitle - Header subtitle text when no custom subtitle slot is provided.
* @attr {("above"|"below")} header-subtitle-position - Position of the subtitle relative to the title. Defaults to "below".
* @attr {boolean} flexcontent - Makes the dialog body a flex container for flexible layouts.
*
* @event opened - Fired when the dialog is shown.
* @event closed - Fired after the dialog is hidden.
*
* @remarks
* **Focus Management:**
* To automatically focus an element when the dialog opens, add the `autofocus` attribute to it.
* Components with `delegatesFocus: true` (like `ha-form`) will forward focus to their first focusable child.
* Example: `<ha-form autofocus .schema=${schema}></ha-form>`
*
* @see https://github.com/home-assistant/frontend/issues/27143
*/
@customElement("ha-wa-dialog")
export class HaWaDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true })
public open = false;
@property({ type: String, reflect: true, attribute: "width" })
public width: DialogWidth = "medium";
@property({ type: Boolean, reflect: true, attribute: "prevent-scrim-close" })
public preventScrimClose = false;
@property({ type: String, attribute: "header-title" })
public headerTitle = "";
@property({ type: String, attribute: "header-subtitle" })
public headerSubtitle = "";
@property({ type: String, attribute: "header-subtitle-position" })
public headerSubtitlePosition: "above" | "below" = "below";
@property({ type: Boolean, reflect: true, attribute: "flexcontent" })
public flexContent = false;
@state()
private _open = false;
protected updated(
changedProperties: Map<string | number | symbol, unknown>
): void {
super.updated(changedProperties);
if (changedProperties.has("open")) {
this._open = this.open;
}
}
protected render() {
return html`
<wa-dialog
.open=${this._open}
.lightDismiss=${!this.preventScrimClose}
without-header
@wa-show=${this._handleShow}
@wa-after-hide=${this._handleAfterHide}
>
<slot name="header">
<ha-dialog-header .subtitlePosition=${this.headerSubtitlePosition}>
<slot name="headerNavigationIcon" slot="navigationIcon">
<ha-icon-button
data-dialog="close"
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
.path=${mdiClose}
></ha-icon-button>
</slot>
${this.headerTitle
? html`<span slot="title" class="title">
${this.headerTitle}
</span>`
: nothing}
${this.headerSubtitle
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
: nothing}
<slot name="headerActionItems" slot="actionItems"></slot>
</ha-dialog-header>
</slot>
<div class="body ha-scrollbar">
<slot></slot>
</div>
<slot name="footer" slot="footer"></slot>
</wa-dialog>
`;
}
private _handleShow = async () => {
this._open = true;
fireEvent(this, "opened");
await this.updateComplete;
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
};
private _handleAfterHide = () => {
this._open = false;
fireEvent(this, "closed");
};
public disconnectedCallback(): void {
super.disconnectedCallback();
this._open = false;
}
static styles = [
haStyleScrollbar,
css`
wa-dialog {
--full-width: var(
--ha-dialog-width-full,
min(
95vw,
calc(
100vw - var(--safe-area-inset-left, var(--ha-space-0)) - var(
--safe-area-inset-right,
var(--ha-space-0)
)
)
)
);
--width: var(--ha-dialog-width-md, min(580px, var(--full-width)));
--spacing: var(--dialog-content-padding, var(--ha-space-6));
--show-duration: var(--ha-dialog-show-duration, 200ms);
--hide-duration: var(--ha-dialog-hide-duration, 200ms);
--ha-dialog-surface-background: var(
--card-background-color,
var(--ha-color-surface-default)
);
--wa-color-surface-raised: var(
--ha-dialog-surface-background,
var(--card-background-color, var(--ha-color-surface-default))
);
--wa-panel-border-radius: var(
--ha-dialog-border-radius,
var(--ha-border-radius-3xl)
);
max-width: var(--ha-dialog-max-width, 100vw);
max-width: var(--ha-dialog-max-width, 100svw);
}
:host([width="small"]) wa-dialog {
--width: var(--ha-dialog-width-sm, min(320px, var(--full-width)));
}
:host([width="large"]) wa-dialog {
--width: var(--ha-dialog-width-lg, min(720px, var(--full-width)));
}
:host([width="full"]) wa-dialog {
--width: var(--full-width);
}
wa-dialog::part(dialog) {
min-width: var(--width, var(--full-width));
max-width: var(--width, var(--full-width));
max-height: var(
--ha-dialog-max-height,
calc(100% - var(--ha-space-20))
);
position: var(--dialog-surface-position, relative);
margin-top: var(--dialog-surface-margin-top, auto);
display: flex;
flex-direction: column;
overflow: hidden;
}
@media all and (max-width: 450px), all and (max-height: 500px) {
:host {
--ha-dialog-border-radius: var(--ha-space-0);
}
wa-dialog {
--full-width: var(--ha-dialog-width-full, 100vw);
}
wa-dialog::part(dialog) {
min-height: var(--ha-dialog-min-height, 100vh);
min-height: var(--ha-dialog-min-height, 100svh);
max-height: var(--ha-dialog-max-height, 100vh);
max-height: var(--ha-dialog-max-height, 100svh);
padding-top: var(--safe-area-inset-top, var(--ha-space-0));
padding-bottom: var(--safe-area-inset-bottom, var(--ha-space-0));
padding-left: var(--safe-area-inset-left, var(--ha-space-0));
padding-right: var(--safe-area-inset-right, var(--ha-space-0));
}
}
.header-title-container {
display: flex;
align-items: center;
}
.header-title {
margin: 0;
margin-bottom: 0;
color: var(
--ha-dialog-header-title-color,
var(--ha-color-on-surface-default, var(--primary-text-color))
);
font-size: var(
--ha-dialog-header-title-font-size,
var(--ha-font-size-2xl)
);
line-height: var(
--ha-dialog-header-title-line-height,
var(--ha-line-height-condensed)
);
font-weight: var(
--ha-dialog-header-title-font-weight,
var(--ha-font-weight-normal)
);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: var(--ha-space-3);
}
wa-dialog::part(body) {
padding: 0;
display: flex;
flex-direction: column;
max-width: 100%;
overflow: hidden;
}
.body {
position: var(--dialog-content-position, relative);
padding: 0 var(--dialog-content-padding, var(--ha-space-6))
var(--dialog-content-padding, var(--ha-space-6))
var(--dialog-content-padding, var(--ha-space-6));
overflow: auto;
flex-grow: 1;
}
:host([flexcontent]) .body {
max-width: 100%;
display: flex;
flex-direction: column;
}
wa-dialog::part(footer) {
padding: var(--ha-space-0);
}
::slotted([slot="footer"]) {
display: flex;
padding: var(--ha-space-3) var(--ha-space-4) var(--ha-space-4)
var(--ha-space-4);
gap: var(--ha-space-3);
justify-content: flex-end;
align-items: center;
width: 100%;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-wa-dialog": HaWaDialog;
}
interface HASSDomEvents {
opened: undefined;
closed: undefined;
}
}

View File

@@ -167,8 +167,6 @@ class DialogMediaPlayerBrowse extends LitElement {
.accept=${this._params.accept}
.defaultId=${this._params.defaultId}
.defaultType=${this._params.defaultType}
.hideContentType=${this._params.hideContentType}
.contentIdHelper=${this._params.contentIdHelper}
@close-dialog=${this.closeDialog}
@media-picked=${this._mediaPicked}
@media-browsed=${this._mediaBrowsed}

View File

@@ -19,12 +19,8 @@ class BrowseMediaManual extends LitElement {
@property({ attribute: false }) public item!: MediaPlayerItemId;
@property({ attribute: false }) public hideContentType = false;
@property({ attribute: false }) public contentIdHelper?: string;
private _schema = memoizeOne(
(hideContentType: boolean) =>
() =>
[
{
name: "media_content_id",
@@ -33,17 +29,13 @@ class BrowseMediaManual extends LitElement {
text: {},
},
},
...(hideContentType
? []
: [
{
name: "media_content_type",
required: false,
selector: {
text: {},
},
},
]),
{
name: "media_content_type",
required: false,
selector: {
text: {},
},
},
] as const
);
@@ -53,7 +45,7 @@ class BrowseMediaManual extends LitElement {
<div class="card-content">
<ha-form
.hass=${this.hass}
.schema=${this._schema(this.hideContentType)}
.schema=${this._schema()}
.data=${this.item}
.computeLabel=${this._computeLabel}
.computeHelper=${this._computeHelper}
@@ -77,35 +69,13 @@ class BrowseMediaManual extends LitElement {
private _computeLabel = (
entry: SchemaUnion<ReturnType<typeof this._schema>>
): string => {
switch (entry.name) {
case "media_content_id":
case "media_content_type":
return this.hass.localize(
`ui.components.selectors.media.${entry.name}`
);
}
return entry.name;
};
): string =>
this.hass.localize(`ui.components.selectors.media.${entry.name}`);
private _computeHelper = (
entry: SchemaUnion<ReturnType<typeof this._schema>>
): string => {
switch (entry.name) {
case "media_content_id":
return (
this.contentIdHelper ||
this.hass.localize(
`ui.components.selectors.media.${entry.name}_detail`
)
);
case "media_content_type":
return this.hass.localize(
`ui.components.selectors.media.${entry.name}_detail`
);
}
return "";
};
): string =>
this.hass.localize(`ui.components.selectors.media.${entry.name}_detail`);
private _mediaPicked() {
fireEvent(this, "manual-media-picked", {

View File

@@ -76,8 +76,8 @@ declare global {
}
export interface MediaPlayerItemId {
media_content_id?: string | undefined;
media_content_type?: string | undefined;
media_content_id: string | undefined;
media_content_type: string | undefined;
}
const MANUAL_ITEM: MediaPlayerItem = {
@@ -113,10 +113,6 @@ export class HaMediaPlayerBrowse extends LitElement {
@property({ attribute: false }) public defaultType?: string;
@property({ attribute: false }) public hideContentType = false;
@property({ attribute: false }) public contentIdHelper?: string;
// @todo Consider reworking to eliminate need for attribute since it is manipulated internally
@property({ type: Boolean, reflect: true }) public narrow = false;
@@ -525,8 +521,6 @@ export class HaMediaPlayerBrowse extends LitElement {
media_content_type: this.defaultType || "",
}}
.hass=${this.hass}
.hideContentType=${this.hideContentType}
.contentIdHelper=${this.contentIdHelper}
@manual-media-picked=${this._manualPicked}
></ha-browse-media-manual>`
: isTTSMediaSource(currentItem.media_content_id)

View File

@@ -14,8 +14,6 @@ export interface MediaPlayerBrowseDialogParams {
accept?: string[];
defaultId?: string;
defaultType?: string;
hideContentType?: boolean;
contentIdHelper?: string;
}
export const showMediaBrowserDialog = (

View File

@@ -102,7 +102,6 @@ export type EnergySolarForecasts = Record<string, EnergySolarForecast>;
export interface DeviceConsumptionEnergyPreference {
// This is an ever increasing value
stat_consumption: string;
stat_power?: string;
name?: string;
included_in_stat?: string;
}
@@ -131,17 +130,11 @@ export interface FlowToGridSourceEnergyPreference {
number_energy_price: number | null;
}
export interface GridPowerSourceEnergyPreference {
// W meter
stat_power: string;
}
export interface GridSourceTypeEnergyPreference {
type: "grid";
flow_from: FlowFromGridSourceEnergyPreference[];
flow_to: FlowToGridSourceEnergyPreference[];
power?: GridPowerSourceEnergyPreference[];
cost_adjustment_day: number;
}
@@ -150,7 +143,6 @@ export interface SolarSourceTypeEnergyPreference {
type: "solar";
stat_energy_from: string;
stat_power?: string;
config_entry_solar_forecast: string[] | null;
}
@@ -158,7 +150,6 @@ export interface BatterySourceTypeEnergyPreference {
type: "battery";
stat_energy_from: string;
stat_energy_to: string;
stat_power?: string;
}
export interface GasSourceTypeEnergyPreference {
type: "gas";
@@ -360,35 +351,6 @@ export const getReferencedStatisticIds = (
return statIDs;
};
export const getReferencedStatisticIdsPower = (
prefs: EnergyPreferences
): string[] => {
const statIDs: (string | undefined)[] = [];
for (const source of prefs.energy_sources) {
if (source.type === "gas" || source.type === "water") {
continue;
}
if (source.type === "solar") {
statIDs.push(source.stat_power);
continue;
}
if (source.type === "battery") {
statIDs.push(source.stat_power);
continue;
}
if (source.power) {
statIDs.push(...source.power.map((p) => p.stat_power));
}
}
statIDs.push(...prefs.device_consumption.map((d) => d.stat_power));
return statIDs.filter(Boolean) as string[];
};
export const enum CompareMode {
NONE = "",
PREVIOUS = "previous",
@@ -436,10 +398,9 @@ const getEnergyData = async (
"gas",
"device",
]);
const powerStatIds = getReferencedStatisticIdsPower(prefs);
const waterStatIds = getReferencedStatisticIds(prefs, info, ["water"]);
const allStatIDs = [...energyStatIds, ...waterStatIds, ...powerStatIds];
const allStatIDs = [...energyStatIds, ...waterStatIds];
const dayDifference = differenceInDays(end || new Date(), start);
const period =
@@ -450,8 +411,6 @@ const getEnergyData = async (
: dayDifference > 2
? "day"
: "hour";
const finePeriod =
dayDifference > 64 ? "day" : dayDifference > 8 ? "hour" : "5minute";
const statsMetadata: Record<string, StatisticsMetaData> = {};
const statsMetadataArray = allStatIDs.length
@@ -473,9 +432,6 @@ const getEnergyData = async (
? (gasUnit as (typeof VOLUME_UNITS)[number])
: undefined,
};
const powerUnits: StatisticsUnitConfiguration = {
power: "kW",
};
const waterUnit = getEnergyWaterUnit(hass, prefs, statsMetadata);
const waterUnits: StatisticsUnitConfiguration = {
volume: waterUnit,
@@ -486,12 +442,6 @@ const getEnergyData = async (
"change",
])
: {};
const _powerStats: Statistics | Promise<Statistics> = powerStatIds.length
? fetchStatistics(hass!, start, end, powerStatIds, finePeriod, powerUnits, [
"mean",
])
: {};
const _waterStats: Statistics | Promise<Statistics> = waterStatIds.length
? fetchStatistics(hass!, start, end, waterStatIds, period, waterUnits, [
"change",
@@ -598,7 +548,6 @@ const getEnergyData = async (
const [
energyStats,
powerStats,
waterStats,
energyStatsCompare,
waterStatsCompare,
@@ -606,14 +555,13 @@ const getEnergyData = async (
fossilEnergyConsumptionCompare,
] = await Promise.all([
_energyStats,
_powerStats,
_waterStats,
_energyStatsCompare,
_waterStatsCompare,
_fossilEnergyConsumption,
_fossilEnergyConsumptionCompare,
]);
const stats = { ...energyStats, ...waterStats, ...powerStats };
const stats = { ...energyStats, ...waterStats };
if (compare) {
statsCompare = { ...energyStatsCompare, ...waterStatsCompare };
}

View File

@@ -39,6 +39,8 @@ import type { HomeAssistant, TranslationDict } from "../types";
import { isUnavailableState } from "./entity";
import { isTTSMediaSource } from "./tts";
import { generateEntityFilter } from "../common/entity/entity_filter";
interface MediaPlayerEntityAttributes extends HassEntityAttributeBase {
media_content_id?: string;
media_content_type?: string;
@@ -522,3 +524,33 @@ export const mediaPlayerJoin = (
export const mediaPlayerUnjoin = (hass: HomeAssistant, entity_id: string) =>
hass.callService("media_player", "unjoin", {}, { entity_id });
/**
* Compute active media player states in a specific area.
* @param hass Home Assistant object
* @param areaId Area ID to filter media players by
* @returns Array of playing media player entities
*/
export const computeActiveAreaMediaStates = (
hass: HomeAssistant,
areaId: string
): MediaPlayerEntity[] => {
const area = hass.areas[areaId];
if (!area) {
return [];
}
// Get all media_player entities in this area
const mediaFilter = generateEntityFilter(hass, {
area: areaId,
domain: "media_player",
});
const mediaEntities = Object.keys(hass.entities).filter(mediaFilter);
return mediaEntities
.map((entityId) => hass.states[entityId] as MediaPlayerEntity | undefined)
.filter(
(stateObj): stateObj is MediaPlayerEntity => stateObj?.state === "playing"
);
};

View File

@@ -95,9 +95,7 @@ export interface StatisticsValidationResultUnitsChanged {
data: {
statistic_id: string;
state_unit: string;
state_unit_class: string | null;
metadata_unit: string;
metadata_unit_class: string | null;
supported_unit: string;
};
}
@@ -233,14 +231,12 @@ export const validateStatistics = (hass: HomeAssistant) =>
export const updateStatisticsMetadata = (
hass: HomeAssistant,
statistic_id: string,
unit_of_measurement: string | null,
unit_class: string | null
unit_of_measurement: string | null
) =>
hass.callWS<undefined>({
type: "recorder/update_statistics_metadata",
statistic_id,
unit_of_measurement,
unit_class,
});
export const clearStatistics = (hass: HomeAssistant, statistic_ids: string[]) =>

View File

@@ -18,7 +18,6 @@ import type {
EntityRegistryEntry,
} from "./entity_registry";
import type { EntitySources } from "./entity_sources";
import type { EntityNameItem } from "../common/entity/compute_entity_name_display";
export type Selector =
| ActionSelector
@@ -42,7 +41,6 @@ export type Selector =
| LegacyDeviceSelector
| DurationSelector
| EntitySelector
| EntityNameSelector
| LegacyEntitySelector
| FileSelector
| IconSelector
@@ -312,10 +310,6 @@ export interface LocationSelectorValue {
export interface MediaSelector {
media: {
accept?: string[];
image_upload?: boolean;
clearable?: boolean;
hide_content_type?: boolean;
content_id_helper?: string;
} | null;
}
@@ -505,13 +499,6 @@ export interface UiStateContentSelector {
} | null;
}
export interface EntityNameSelector {
entity_name: {
entity_id?: string;
default_name?: EntityNameItem | EntityNameItem[] | string;
} | null;
}
export const expandLabelTarget = (
hass: HomeAssistant,
labelId: string,

View File

@@ -76,10 +76,7 @@ export const formatSelectorValue = (
if (!stateObj) {
return entityId;
}
const name = hass.formatEntityName(stateObj, [
{ type: "device" },
{ type: "entity" },
]);
const name = hass.formatEntityName(stateObj, ["device", "entity"], " ");
return name || entityId;
})
.join(", ");

View File

@@ -43,7 +43,7 @@ export type ModernForecastType = "hourly" | "daily" | "twice_daily";
export type ForecastType = ModernForecastType | "legacy";
export interface ForecastAttribute {
interface ForecastAttribute {
temperature: number;
datetime: string;
templow?: number;

View File

@@ -77,84 +77,80 @@ class MoreInfoMediaPlayer extends LitElement {
return nothing;
}
if (!stateActive(this.stateObj)) {
return nothing;
}
const supportsMute = supportsFeature(
this.stateObj,
MediaPlayerEntityFeature.VOLUME_MUTE
);
const supportsSet = supportsFeature(
const supportsSliding = supportsFeature(
this.stateObj,
MediaPlayerEntityFeature.VOLUME_SET
);
const supportsStep = supportsFeature(
this.stateObj,
MediaPlayerEntityFeature.VOLUME_STEP
);
if (!supportsMute && !supportsSet && !supportsStep) {
return nothing;
}
return html`
<div class="volume">
${supportsMute
? html`
<ha-icon-button
.path=${this.stateObj.attributes.is_volume_muted
? mdiVolumeOff
: mdiVolumeHigh}
.label=${this.hass.localize(
`ui.card.media_player.${
this.stateObj.attributes.is_volume_muted
? "media_volume_unmute"
: "media_volume_mute"
}`
)}
@click=${this._toggleMute}
></ha-icon-button>
`
: nothing}
${supportsStep
? html` <ha-icon-button
action="volume_down"
.path=${mdiVolumeMinus}
.label=${this.hass.localize(
"ui.card.media_player.media_volume_down"
)}
@click=${this._handleClick}
></ha-icon-button>`
: nothing}
${supportsSet
? html`
${!supportsMute && !supportsStep
? html`<ha-svg-icon .path=${mdiVolumeHigh}></ha-svg-icon>`
: nothing}
<ha-slider
labeled
id="input"
.value=${Number(this.stateObj.attributes.volume_level) * 100}
@change=${this._selectedValueChanged}
></ha-slider>
`
: nothing}
${supportsStep
? html`
<ha-icon-button
action="volume_up"
.path=${mdiVolumePlus}
.label=${this.hass.localize(
"ui.card.media_player.media_volume_up"
)}
@click=${this._handleClick}
></ha-icon-button>
`
: nothing}
</div>
`;
return html`${(supportsFeature(
this.stateObj!,
MediaPlayerEntityFeature.VOLUME_SET
) ||
supportsFeature(this.stateObj!, MediaPlayerEntityFeature.VOLUME_STEP)) &&
stateActive(this.stateObj!)
? html`
<div class="volume">
${supportsMute
? html`
<ha-icon-button
.path=${this.stateObj.attributes.is_volume_muted
? mdiVolumeOff
: mdiVolumeHigh}
.label=${this.hass.localize(
`ui.card.media_player.${
this.stateObj.attributes.is_volume_muted
? "media_volume_unmute"
: "media_volume_mute"
}`
)}
@click=${this._toggleMute}
></ha-icon-button>
`
: ""}
${supportsFeature(
this.stateObj,
MediaPlayerEntityFeature.VOLUME_STEP
) && !supportsSliding
? html`
<ha-icon-button
action="volume_down"
.path=${mdiVolumeMinus}
.label=${this.hass.localize(
"ui.card.media_player.media_volume_down"
)}
@click=${this._handleClick}
></ha-icon-button>
<ha-icon-button
action="volume_up"
.path=${mdiVolumePlus}
.label=${this.hass.localize(
"ui.card.media_player.media_volume_up"
)}
@click=${this._handleClick}
></ha-icon-button>
`
: nothing}
${supportsSliding
? html`
${!supportsMute
? html`<ha-svg-icon .path=${mdiVolumeHigh}></ha-svg-icon>`
: nothing}
<ha-slider
labeled
id="input"
.value=${Number(this.stateObj.attributes.volume_level) *
100}
@change=${this._selectedValueChanged}
></ha-slider>
`
: nothing}
</div>
`
: nothing}`;
}
protected _renderSourceControl() {
@@ -167,12 +163,15 @@ class MoreInfoMediaPlayer extends LitElement {
}
return html`<ha-md-button-menu positioning="popover">
<ha-icon-button
<ha-button
slot="trigger"
.title=${this.hass.localize(`ui.card.media_player.source`)}
.path=${mdiLoginVariant}
appearance="plain"
variant="neutral"
size="small"
title=${this.hass.localize(`ui.card.media_player.source`)}
>
</ha-icon-button>
<ha-svg-icon .path=${mdiLoginVariant}></ha-svg-icon>
</ha-button>
${this.stateObj.attributes.source_list!.map(
(source) =>
html`<ha-md-menu-item
@@ -200,12 +199,15 @@ class MoreInfoMediaPlayer extends LitElement {
}
return html`<ha-md-button-menu positioning="popover">
<ha-icon-button
<ha-button
slot="trigger"
.title=${this.hass.localize(`ui.card.media_player.sound_mode`)}
.path=${mdiMusicNoteEighth}
appearance="plain"
variant="neutral"
size="small"
title=${this.hass.localize(`ui.card.media_player.sound_mode`)}
>
</ha-icon-button>
<ha-svg-icon .path=${mdiMusicNoteEighth}></ha-svg-icon>
</ha-button>
${this.stateObj.attributes.sound_mode_list!.map(
(soundMode) =>
html`<ha-md-menu-item
@@ -231,17 +233,21 @@ class MoreInfoMediaPlayer extends LitElement {
const groupMembers = this.stateObj.attributes.group_members;
const hasMultipleMembers = groupMembers && groupMembers?.length > 1;
return html`<ha-icon-button
return html`<ha-button
class="grouping"
@click=${this._showGroupMediaPlayers}
.title=${this.hass.localize("ui.card.media_player.join")}
appearance="plain"
variant="neutral"
size="small"
title=${this.hass.localize("ui.card.media_player.join")}
>
<div class="grouping">
<div>
<ha-svg-icon .path=${mdiSpeakerMultiple}></ha-svg-icon>
${hasMultipleMembers
? html`<span class="badge">${groupMembers?.length || 4}</span>`
? html`<span class="badge"> ${groupMembers?.length || 4} </span>`
: nothing}
</div>
</ha-icon-button>`;
</ha-button>`;
}
protected _renderEmptyCover(title: string, icon?: string) {
@@ -404,39 +410,48 @@ class MoreInfoMediaPlayer extends LitElement {
${!isUnavailableState(stateObj.state) &&
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA)
? html`
<ha-icon-button
<ha-button
@click=${this._showBrowseMedia}
.title=${this.hass.localize(
appearance="plain"
variant="neutral"
size="small"
title=${this.hass.localize(
"ui.card.media_player.browse_media"
)}
.path=${mdiPlayBoxMultiple}
>
</ha-icon-button>
<ha-svg-icon .path=${mdiPlayBoxMultiple}></ha-svg-icon>
</ha-button>
`
: nothing}
${this._renderGrouping()} ${this._renderSourceControl()}
${this._renderSoundMode()}
${turnOn
? html`<ha-icon-button
? html`<ha-button
action=${turnOn.action}
@click=${this._handleClick}
.title=${this.hass.localize(
appearance="plain"
variant="neutral"
size="small"
title=${this.hass.localize(
`ui.card.media_player.${turnOn.action}`
)}
.path=${turnOn.icon}
>
</ha-icon-button>`
<ha-svg-icon .path=${turnOn.icon}></ha-svg-icon>
</ha-button>`
: nothing}
${turnOff
? html`<ha-icon-button
? html`<ha-button
action=${turnOff.action}
@click=${this._handleClick}
.title=${this.hass.localize(
appearance="plain"
variant="neutral"
size="small"
title=${this.hass.localize(
`ui.card.media_player.${turnOff.action}`
)}
.path=${turnOff.icon}
>
</ha-icon-button>`
<ha-svg-icon .path=${turnOff.icon}></ha-svg-icon>
</ha-button>`
: nothing}
</div>
</div>
@@ -560,7 +575,7 @@ class MoreInfoMediaPlayer extends LitElement {
font-size: var(--ha-font-size-xs);
background-color: var(--primary-color);
padding: 0 4px;
color: var(--text-primary-color);
color: var(--primary-text-color);
}
.position-bar {
@@ -597,15 +612,15 @@ class MoreInfoMediaPlayer extends LitElement {
justify-content: space-around;
}
.controls-row ha-icon-button {
color: var(--secondary-text-color);
.controls-row ha-button {
width: 32px;
}
.controls-row ha-svg-icon {
color: var(--ha-color-on-neutral-quiet);
}
.grouping {
.grouping::part(label) {
position: relative;
}

View File

@@ -16,7 +16,6 @@ import "../../../components/ha-tab-group";
import "../../../components/ha-tab-group-tab";
import "../../../components/ha-tooltip";
import type {
ForecastAttribute,
ForecastEvent,
ModernForecastType,
WeatherEntity,
@@ -132,24 +131,6 @@ class MoreInfoWeather extends LitElement {
getSupportedForecastTypes(stateObj)
);
private _groupForecastByDay = memoizeOne((forecast: ForecastAttribute[]) => {
if (!forecast) return [];
const grouped = new Map<string, NonNullable<typeof forecast>>();
forecast.forEach((item) => {
const date = new Date(item.datetime);
const dateKey = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`;
if (!grouped.has(dateKey)) {
grouped.set(dateKey, []);
}
grouped.get(dateKey)!.push(item);
});
return Array.from(grouped.values());
});
protected render() {
if (!this.hass || !this.stateObj) {
return nothing;
@@ -333,90 +314,78 @@ class MoreInfoWeather extends LitElement {
: nothing}
<div class="forecast">
${forecast?.length
? this._groupForecastByDay(forecast).map((dayForecast) => {
const showDayHeader = hourly || dayNight;
return html`
<div class="forecast-day">
${showDayHeader
? html`<div class="forecast-day-header">
${formatDateWeekdayShort(
new Date(dayForecast[0].datetime),
this.hass!.locale,
this.hass!.config
)}
</div>`
: nothing}
<div class="forecast-day-content">
${dayForecast.map((item) =>
this._showValue(item.templow) ||
this._showValue(item.temperature)
? forecast.map((item) =>
this._showValue(item.templow) || this._showValue(item.temperature)
? html`
<div>
<div>
${dayNight
? html`
${formatDateWeekdayShort(
new Date(item.datetime),
this.hass!.locale,
this.hass!.config
)}
<div class="daynight">
${item.is_daytime !== false
? this.hass!.localize("ui.card.weather.day")
: this.hass!.localize(
"ui.card.weather.night"
)}<br />
</div>
`
: hourly
? html`
${formatTime(
new Date(item.datetime),
this.hass!.locale,
this.hass!.config
)}
`
: html`
${formatDateWeekdayShort(
new Date(item.datetime),
this.hass!.locale,
this.hass!.config
)}
`}
</div>
${this._showValue(item.condition)
? html`
<div class="forecast-item">
<div
class="forecast-item-label ${showDayHeader
? ""
: "no-header"}"
>
${hourly
? formatTime(
new Date(item.datetime),
this.hass!.locale,
this.hass!.config
)
: dayNight
? html`<div class="daynight">
${item.is_daytime !== false
? this.hass!.localize(
"ui.card.weather.day"
)
: this.hass!.localize(
"ui.card.weather.night"
)}
</div>`
: formatDateWeekdayShort(
new Date(item.datetime),
this.hass!.locale,
this.hass!.config
)}
</div>
${this._showValue(item.condition)
? html`
<div class="forecast-image-icon">
${getWeatherStateIcon(
item.condition!,
this,
!(
item.is_daytime ||
item.is_daytime === undefined
)
)}
</div>
`
: nothing}
<div class="temp">
${this._showValue(item.temperature)
? html`${formatNumber(
item.temperature,
this.hass!.locale
)}°`
: "—"}
</div>
<div class="templow">
${this._showValue(item.templow)
? html`${formatNumber(
item.templow!,
this.hass!.locale
)}°`
: nothing}
</div>
<div class="forecast-image-icon">
${getWeatherStateIcon(
item.condition!,
this,
!(
item.is_daytime ||
item.is_daytime === undefined
)
)}
</div>
`
: nothing
)}
</div>
</div>
`;
})
: nothing}
<div class="temp">
${this._showValue(item.temperature)
? html`${formatNumber(
item.temperature,
this.hass!.locale
)}°`
: "—"}
</div>
<div class="templow">
${this._showValue(item.templow)
? html`${formatNumber(
item.templow!,
this.hass!.locale
)}°`
: hourly
? nothing
: "—"}
</div>
</div>
`
: nothing
)
: html`<ha-spinner size="medium"></ha-spinner>`}
</div>
@@ -587,46 +556,14 @@ class MoreInfoWeather extends LitElement {
user-select: none;
}
.forecast-day {
display: flex;
flex-direction: column;
}
.forecast-day-header {
position: sticky;
top: 0;
left: 0;
color: var(--primary-text-color);
z-index: 1;
padding: 0 var(--ha-space-3) var(--ha-space-1) var(--ha-space-3);
width: fit-content;
width: 40px;
.forecast > div {
text-align: center;
font-weight: var(--ha-font-weight-semi-bold);
}
.forecast-day-content {
display: flex;
flex-direction: row;
}
.forecast-item {
text-align: center;
padding: 0 var(--ha-space-3);
}
.forecast-item-label {
font-size: var(--ha-font-size-m);
color: var(--secondary-text-color);
}
.forecast-item-label.no-header {
color: var(--primary-text-color);
padding: 0 10px;
}
.forecast .icon,
.forecast .temp {
margin: var(--ha-space-1) 0;
margin: 4px 0;
}
.forecast .temp {

View File

@@ -23,14 +23,8 @@ import { stopPropagation } from "../../common/dom/stop_propagation";
import { computeAreaName } from "../../common/entity/compute_area_name";
import { computeDeviceName } from "../../common/entity/compute_device_name";
import { computeDomain } from "../../common/entity/compute_domain";
import {
computeEntityEntryName,
computeEntityName,
} from "../../common/entity/compute_entity_name";
import {
getEntityContext,
getEntityEntryContext,
} from "../../common/entity/context/get_entity_context";
import { computeEntityEntryName } from "../../common/entity/compute_entity_name";
import { getEntityEntryContext } from "../../common/entity/context/get_entity_context";
import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event";
import { navigate } from "../../common/navigate";
import "../../components/ha-button-menu";
@@ -327,34 +321,28 @@ export class MoreInfoDialog extends LitElement {
(isDefaultView && this._parentEntityIds.length === 0) ||
isSpecificInitialView;
const context = stateObj
? getEntityContext(
stateObj,
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
)
: this._entry
? getEntityEntryContext(
this._entry,
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
)
: undefined;
let entityName: string | undefined;
let deviceName: string | undefined;
let areaName: string | undefined;
const entityName = stateObj
? computeEntityName(stateObj, this.hass.entities, this.hass.devices)
: this._entry
? computeEntityEntryName(this._entry, this.hass.devices)
: entityId;
const deviceName = context?.device
? computeDeviceName(context.device)
: undefined;
const areaName = context?.area ? computeAreaName(context.area) : undefined;
if (stateObj) {
entityName = this.hass.formatEntityName(stateObj, "entity");
deviceName = this.hass.formatEntityName(stateObj, "device");
areaName = this.hass.formatEntityName(stateObj, "area");
} else if (this._entry) {
const { device, area } = getEntityEntryContext(
this._entry,
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
entityName = computeEntityEntryName(this._entry, this.hass.devices);
deviceName = device ? computeDeviceName(device) : undefined;
areaName = area ? computeAreaName(area) : undefined;
} else {
entityName = entityId;
}
const breadcrumb = [areaName, deviceName, entityName].filter(
(v): v is string => Boolean(v)

View File

@@ -23,7 +23,6 @@ import { fireEvent } from "../../common/dom/fire_event";
import { computeAreaName } from "../../common/entity/compute_area_name";
import { computeDeviceNameDisplay } from "../../common/entity/compute_device_name";
import { computeDomain } from "../../common/entity/compute_domain";
import { entityUseDeviceName } from "../../common/entity/compute_entity_name";
import { computeStateName } from "../../common/entity/compute_state_name";
import { getDeviceContext } from "../../common/entity/context/get_device_context";
import { navigate } from "../../common/navigate";
@@ -31,9 +30,9 @@ import { caseInsensitiveStringCompare } from "../../common/string/compare";
import type { ScorableTextItem } from "../../common/string/filter/sequence-matching";
import { computeRTL } from "../../common/util/compute_rtl";
import { debounce } from "../../common/util/debounce";
import "../../components/ha-button";
import "../../components/ha-icon-button";
import "../../components/ha-label";
import "../../components/ha-button";
import "../../components/ha-list";
import "../../components/ha-md-list-item";
import "../../components/ha-spinner";
@@ -632,29 +631,14 @@ export class QuickBar extends LitElement {
const stateObj = this.hass.states[entityId];
const friendlyName = computeStateName(stateObj); // Keep this for search
const entityName = this.hass.formatEntityName(stateObj, "entity");
const deviceName = this.hass.formatEntityName(stateObj, "device");
const areaName = this.hass.formatEntityName(stateObj, "area");
const useDeviceName = entityUseDeviceName(
stateObj,
this.hass.entities,
this.hass.devices
);
const name = this.hass.formatEntityName(
stateObj,
useDeviceName ? { type: "device" } : { type: "entity" }
);
const primary = name || entityId;
const secondary = this.hass.formatEntityName(
stateObj,
useDeviceName
? [{ type: "area" }]
: [{ type: "area" }, { type: "device" }],
{
separator: isRTL ? " ◂ " : " ▸ ",
}
);
const primary = entityName || deviceName || entityId;
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
const translatedDomain = domainToName(
this.hass.localize,

View File

@@ -1,6 +1,7 @@
import "@material/mwc-linear-progress/mwc-linear-progress";
import {
mdiAutoFix,
mdiClose,
mdiLifebuoy,
mdiPower,
mdiPowerCycle,
@@ -8,14 +9,16 @@ import {
} from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-alert";
import "../../components/ha-expansion-panel";
import "../../components/ha-fade-in";
import "../../components/ha-icon-button";
import "../../components/ha-icon-next";
import "../../components/ha-wa-dialog";
import "../../components/ha-md-dialog";
import type { HaMdDialog } from "../../components/ha-md-dialog";
import "../../components/ha-md-list";
import "../../components/ha-md-list-item";
import "../../components/ha-spinner";
@@ -55,14 +58,12 @@ class DialogRestart extends LitElement {
@state()
private _hostInfo?: HassioHostInfo;
@state()
private _dialogOpen = false;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public async showDialog(): Promise<void> {
const isHassioLoaded = isComponentLoaded(this.hass, "hassio");
this._open = true;
this._dialogOpen = true;
if (isHassioLoaded && !this._hostInfo) {
this._loadHostInfo();
@@ -91,13 +92,16 @@ class DialogRestart extends LitElement {
}
private _dialogClosed(): void {
this._dialogOpen = false;
this._open = false;
this._loadingHostInfo = false;
this._loadingBackupInfo = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
public closeDialog(): void {
this._dialog?.close();
}
protected render() {
if (!this._open) {
return nothing;
@@ -109,13 +113,17 @@ class DialogRestart extends LitElement {
const dialogTitle = this.hass.localize("ui.dialogs.restart.heading");
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._dialogOpen}
header-title=${dialogTitle}
@closed=${this._dialogClosed}
>
<div class="content">
<ha-md-dialog open @closed=${this._dialogClosed}>
<ha-dialog-header slot="headline">
<ha-icon-button
slot="navigationIcon"
.label=${this.hass.localize("ui.common.close") ?? "Close"}
.path=${mdiClose}
@click=${this.closeDialog}
></ha-icon-button>
<span slot="title" .title=${dialogTitle}> ${dialogTitle} </span>
</ha-dialog-header>
<div slot="content" class="content">
<div class="action-loader">
${this._loadingBackupInfo
? html`<ha-fade-in .delay=${250}>
@@ -257,12 +265,12 @@ class DialogRestart extends LitElement {
</ha-expansion-panel>
`}
</div>
</ha-wa-dialog>
</ha-md-dialog>
`;
}
private async _reload() {
this._dialogOpen = false;
this.closeDialog();
showToast(this, {
message: this.hass.localize("ui.dialogs.restart.reload.reloading"),
@@ -366,7 +374,7 @@ class DialogRestart extends LitElement {
return;
}
this._dialogOpen = false;
this.closeDialog();
let actionFunc;
@@ -405,9 +413,15 @@ class DialogRestart extends LitElement {
haStyle,
haStyleDialog,
css`
ha-wa-dialog {
ha-md-dialog {
--dialog-content-padding: 0;
}
@media all and (min-width: 550px) {
ha-md-dialog {
min-width: 500px;
max-width: 500px;
}
}
ha-expansion-panel {
border-top: 1px solid var(--divider-color);

View File

@@ -302,8 +302,6 @@ export default class HaAutomationSidebar extends LitElement {
--ha-bottom-sheet-border-style: solid;
--ha-bottom-sheet-border-color: var(--primary-color);
margin-top: var(--safe-area-inset-top);
--ha-bottom-sheet-surface-background: var(--card-background-color);
}
@media all and (max-width: 870px) {

View File

@@ -1,7 +1,7 @@
import { mdiClose } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-alert";
@@ -9,9 +9,9 @@ import "../../../../components/ha-button";
import "../../../../components/ha-dialog-header";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-icon-button-prev";
import "../../../../components/ha-wa-dialog";
import "../../../../components/ha-md-dialog";
import type { HaMdDialog } from "../../../../components/ha-md-dialog";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-md-select";
@@ -73,13 +73,12 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
@state() private _formData?: FormData;
@state() private _open = false;
@query("ha-md-dialog") private _dialog?: HaMdDialog;
public showDialog(_params: GenerateBackupDialogParams): void {
this._step = STEPS[0];
this._formData = INITIAL_DATA;
this._params = _params;
this._open = true;
this._fetchAgents();
this._fetchBackupConfig();
@@ -89,7 +88,6 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
if (this._params!.cancel) {
this._params!.cancel();
}
this._open = false;
this._step = undefined;
this._formData = undefined;
this._agents = [];
@@ -116,7 +114,7 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
}
public closeDialog() {
this._open = false;
this._dialog?.close();
return true;
}
@@ -181,19 +179,15 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
const selectedAgents = this._formData.agent_ids;
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
@closed=${this._dialogClosed}
>
<ha-dialog-header slot="header">
<ha-md-dialog open disable-cancel-action @closed=${this._dialogClosed}>
<ha-dialog-header slot="headline">
${isFirstStep
? html`
<ha-icon-button
slot="navigationIcon"
data-dialog="close"
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
@click=${this.closeDialog}
></ha-icon-button>
`
: html`
@@ -204,17 +198,13 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
`}
<span slot="title" .title=${dialogTitle}> ${dialogTitle} </span>
</ha-dialog-header>
<div class="content">
<div slot="content" class="content">
${this._step === "data" ? this._renderData() : this._renderSync()}
</div>
<ha-dialog-footer slot="footer">
<div slot="actions">
${isFirstStep
? html`
<ha-button
slot="secondaryAction"
@click=${this.closeDialog}
appearance="plain"
>
<ha-button @click=${this.closeDialog} appearance="plain">
${this.hass.localize("ui.common.cancel")}
</ha-button>
`
@@ -222,7 +212,6 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
${isLastStep
? html`
<ha-button
slot="primaryAction"
@click=${this._submit}
.disabled=${this._formData.agents_mode === "custom" &&
!selectedAgents.length}
@@ -234,15 +223,14 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
`
: html`
<ha-button
slot="primaryAction"
@click=${this._nextStep}
.disabled=${this._step === "data" && this._noDataSelected}
>
${this.hass.localize("ui.common.next")}
</ha-button>
`}
</ha-dialog-footer>
</ha-wa-dialog>
</div>
</ha-md-dialog>
`;
}
@@ -448,8 +436,9 @@ class DialogGenerateBackup extends LitElement implements HassDialog {
haStyle,
haStyleDialog,
css`
ha-wa-dialog {
ha-md-dialog {
--dialog-content-padding: 24px;
max-height: calc(100vh - 48px);
}
ha-md-list {
background: none;

View File

@@ -71,9 +71,6 @@ import { showGenerateBackupDialog } from "./dialogs/show-dialog-generate-backup"
import { showNewBackupDialog } from "./dialogs/show-dialog-new-backup";
import { showUploadBackupDialog } from "./dialogs/show-dialog-upload-backup";
import { downloadBackup } from "./helper/download_backup";
import type { HaMdMenu } from "../../../components/ha-md-menu";
import "../../../components/ha-md-menu";
import "../../../components/ha-md-menu-item";
interface BackupRow extends DataTableRowData, BackupContent {
formatted_type: string;
@@ -123,10 +120,6 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
@query("hass-tabs-subpage-data-table", true)
private _dataTable!: HaTabsSubpageDataTable;
@query("#overflow-menu") private _overflowMenu?: HaMdMenu;
private _overflowBackup?: BackupContent;
public connectedCallback() {
super.connectedCallback();
window.addEventListener("location-changed", this._locationChanged);
@@ -261,12 +254,24 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
hideable: false,
type: "overflow-menu",
template: (backup) => html`
<ha-icon-button
.selected=${backup}
.label=${this.hass.localize("ui.common.overflow_menu")}
.path=${mdiDotsVertical}
@click=${this._toggleOverflowMenu}
></ha-icon-button>
<ha-icon-overflow-menu
.hass=${this.hass}
narrow
.items=${[
{
label: this.hass.localize("ui.common.download"),
path: mdiDownload,
action: () => this._downloadBackup(backup),
},
{
label: this.hass.localize("ui.common.delete"),
path: mdiDelete,
action: () => this._deleteBackup(backup),
warning: true,
},
]}
>
</ha-icon-overflow-menu>
`,
},
})
@@ -285,20 +290,6 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
: undefined
);
private _toggleOverflowMenu = (ev) => {
if (!this._overflowMenu) {
return;
}
if (this._overflowMenu.open) {
this._overflowMenu.close();
return;
}
this._overflowBackup = ev.target.selected;
this._overflowMenu.anchorElement = ev.target;
this._overflowMenu.show();
};
private _handleGroupingChanged(ev: CustomEvent) {
this._activeGrouping = ev.detail.value;
}
@@ -375,16 +366,14 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
clickable
id="backup_id"
has-filters
.filters=${
Object.values(this._filters).filter((filter) =>
Array.isArray(filter)
? filter.length
: filter &&
Object.values(filter).some((val) =>
Array.isArray(val) ? val.length : val
)
).length
}
.filters=${Object.values(this._filters).filter((filter) =>
Array.isArray(filter)
? filter.length
: filter &&
Object.values(filter).some((val) =>
Array.isArray(val) ? val.length : val
)
).length}
selectable
.selected=${this._selected.length}
.initialGroupColumn=${this._activeGrouping}
@@ -426,30 +415,28 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
</div>
<div slot="selection-bar">
${
!this.narrow
? html`
<ha-button
appearance="plain"
@click=${this._deleteSelected}
variant="danger"
>
${this.hass.localize(
"ui.panel.config.backup.backups.delete_selected"
)}
</ha-button>
`
: html`
<ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.backup.backups.delete_selected"
)}
.path=${mdiDelete}
class="warning"
@click=${this._deleteSelected}
></ha-icon-button>
`
}
${!this.narrow
? html`
<ha-button
appearance="plain"
@click=${this._deleteSelected}
variant="danger"
>
${this.hass.localize(
"ui.panel.config.backup.backups.delete_selected"
)}
</ha-button>
`
: html`
<ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.backup.backups.delete_selected"
)}
.path=${mdiDelete}
class="warning"
@click=${this._deleteSelected}
></ha-icon-button>
`}
</div>
<ha-filter-states
@@ -462,43 +449,29 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
expanded
.narrow=${this.narrow}
></ha-filter-states>
${
!this._needsOnboarding
? html`
<ha-fab
slot="fab"
?disabled=${backupInProgress}
.label=${this.hass.localize(
"ui.panel.config.backup.backups.new_backup"
)}
extended
@click=${this._newBackup}
>
${backupInProgress
? html`<div slot="icon" class="loading">
<ha-spinner .size=${"small"}></ha-spinner>
</div>`
: html`<ha-svg-icon
slot="icon"
.path=${mdiPlus}
></ha-svg-icon>`}
</ha-fab>
`
: nothing
}
${!this._needsOnboarding
? html`
<ha-fab
slot="fab"
?disabled=${backupInProgress}
.label=${this.hass.localize(
"ui.panel.config.backup.backups.new_backup"
)}
extended
@click=${this._newBackup}
>
${backupInProgress
? html`<div slot="icon" class="loading">
<ha-spinner .size=${"small"}></ha-spinner>
</div>`
: html`<ha-svg-icon
slot="icon"
.path=${mdiPlus}
></ha-svg-icon>`}
</ha-fab>
`
: nothing}
</hass-tabs-subpage-data-table>
<ha-md-menu id="overflow-menu" positioning="fixed">
<ha-md-menu-item .clickAction=${this._downloadBackup}>
<ha-svg-icon slot="start" .path=${mdiDownload}></ha-svg-icon>
${this.hass.localize("ui.common.download")}
</ha-md-menu-item>
<ha-md-menu-item class="warning" .clickAction=${this._deleteBackup}>
<ha-svg-icon slot="start" .path=${mdiDelete}></ha-svg-icon>
${this.hass.localize("ui.common.delete")}
</ha-md-menu-item>
</ha-md-menu>
>
</ha-icon-overflow-menu>
`;
}
@@ -572,18 +545,11 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
navigate(`/config/backup/details/${id}`);
}
private async _downloadBackup(): Promise<void> {
if (!this._overflowBackup) {
return;
}
downloadBackup(this.hass, this, this._overflowBackup, this.config);
private async _downloadBackup(backup: BackupContent): Promise<void> {
downloadBackup(this.hass, this, backup, this.config);
}
private async _deleteBackup(): Promise<void> {
if (!this._overflowBackup) {
return;
}
private async _deleteBackup(backup: BackupContent): Promise<void> {
const confirm = await showConfirmationDialog(this, {
title: this.hass.localize("ui.panel.config.backup.dialogs.delete.title"),
text: this.hass.localize("ui.panel.config.backup.dialogs.delete.text"),
@@ -596,11 +562,9 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
}
try {
await deleteBackup(this.hass, this._overflowBackup.backup_id);
if (this._selected.includes(this._overflowBackup.backup_id)) {
this._selected = this._selected.filter(
(id) => id !== this._overflowBackup!.backup_id
);
await deleteBackup(this.hass, backup.backup_id);
if (this._selected.includes(backup.backup_id)) {
this._selected = this._selected.filter((id) => id !== backup.backup_id);
}
} catch (err: any) {
showAlertDialog(this, {

View File

@@ -770,39 +770,43 @@ export class HaConfigDevicePage extends LitElement {
${firstDeviceAction || actions.length
? html`
<div class="card-actions" slot="actions">
<ha-button
href=${ifDefined(firstDeviceAction!.href)}
rel=${ifDefined(
firstDeviceAction!.target ? "noreferrer" : undefined
)}
appearance="plain"
target=${ifDefined(firstDeviceAction!.target)}
class=${ifDefined(firstDeviceAction!.classes)}
.variant=${firstDeviceAction!.classes?.includes("warning")
? "danger"
: "brand"}
.action=${firstDeviceAction!.action}
@click=${this._deviceActionClicked}
>
${firstDeviceAction!.label}
${firstDeviceAction!.icon
? html`
<ha-svg-icon
class=${ifDefined(firstDeviceAction!.classes)}
.path=${firstDeviceAction!.icon}
slot="start"
></ha-svg-icon>
`
: nothing}
${firstDeviceAction!.trailingIcon
? html`
<ha-svg-icon
.path=${firstDeviceAction!.trailingIcon}
slot="end"
></ha-svg-icon>
`
: nothing}
</ha-button>
<div>
<ha-button
href=${ifDefined(firstDeviceAction!.href)}
rel=${ifDefined(
firstDeviceAction!.target ? "noreferrer" : undefined
)}
appearance="plain"
target=${ifDefined(firstDeviceAction!.target)}
class=${ifDefined(firstDeviceAction!.classes)}
.variant=${firstDeviceAction!.classes?.includes(
"warning"
)
? "danger"
: "brand"}
.action=${firstDeviceAction!.action}
@click=${this._deviceActionClicked}
>
${firstDeviceAction!.label}
${firstDeviceAction!.icon
? html`
<ha-svg-icon
class=${ifDefined(firstDeviceAction!.classes)}
.path=${firstDeviceAction!.icon}
slot="start"
></ha-svg-icon>
`
: nothing}
${firstDeviceAction!.trailingIcon
? html`
<ha-svg-icon
.path=${firstDeviceAction!.trailingIcon}
slot="end"
></ha-svg-icon>
`
: nothing}
</ha-button>
</div>
${actions.length
? html`

View File

@@ -26,7 +26,6 @@ import type {
EnergySource,
FlowFromGridSourceEnergyPreference,
FlowToGridSourceEnergyPreference,
GridPowerSourceEnergyPreference,
GridSourceTypeEnergyPreference,
} from "../../../../data/energy";
import {
@@ -48,7 +47,6 @@ import { documentationUrl } from "../../../../util/documentation-url";
import {
showEnergySettingsGridFlowFromDialog,
showEnergySettingsGridFlowToDialog,
showEnergySettingsGridPowerDialog,
} from "../dialogs/show-dialogs-energy";
import "./ha-energy-validation-result";
import { energyCardStyles } from "./styles";
@@ -228,58 +226,6 @@ export class EnergyGridSettings extends LitElement {
>
</div>
<h3>
${this.hass.localize("ui.panel.config.energy.grid.grid_power")}
</h3>
${gridSource.power?.map((power) => {
const entityState = this.hass.states[power.stat_power];
return html`
<div class="row" .source=${power}>
${entityState?.attributes.icon
? html`<ha-icon
.icon=${entityState.attributes.icon}
></ha-icon>`
: html`<ha-svg-icon
.path=${mdiTransmissionTower}
></ha-svg-icon>`}
<span class="content"
>${getStatisticLabel(
this.hass,
power.stat_power,
this.statsMetadata?.[power.stat_power]
)}</span
>
<ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.energy.grid.edit_power"
)}
@click=${this._editPowerSource}
.path=${mdiPencil}
></ha-icon-button>
<ha-icon-button
.label=${this.hass.localize(
"ui.panel.config.energy.grid.delete_power"
)}
@click=${this._deletePowerSource}
.path=${mdiDelete}
></ha-icon-button>
</div>
`;
})}
<div class="row border-bottom">
<ha-svg-icon .path=${mdiTransmissionTower}></ha-svg-icon>
<ha-button
@click=${this._addPowerSource}
appearance="filled"
size="small"
>
<ha-svg-icon .path=${mdiPlus} slot="start"></ha-svg-icon
>${this.hass.localize(
"ui.panel.config.energy.grid.add_power"
)}</ha-button
>
</div>
<h3>
${this.hass.localize(
"ui.panel.config.energy.grid.grid_carbon_footprint"
@@ -553,97 +499,6 @@ export class EnergyGridSettings extends LitElement {
await this._savePreferences(cleanedPreferences);
}
private _addPowerSource() {
const gridSource = this.preferences.energy_sources.find(
(src) => src.type === "grid"
) as GridSourceTypeEnergyPreference | undefined;
showEnergySettingsGridPowerDialog(this, {
grid_source: gridSource,
saveCallback: async (power) => {
let preferences: EnergyPreferences;
if (!gridSource) {
preferences = {
...this.preferences,
energy_sources: [
...this.preferences.energy_sources,
{
...emptyGridSourceEnergyPreference(),
power: [power],
},
],
};
} else {
preferences = {
...this.preferences,
energy_sources: this.preferences.energy_sources.map((src) =>
src.type === "grid"
? { ...src, power: [...(gridSource.power || []), power] }
: src
),
};
}
await this._savePreferences(preferences);
},
});
}
private _editPowerSource(ev) {
const origSource: GridPowerSourceEnergyPreference =
ev.currentTarget.closest(".row").source;
const gridSource = this.preferences.energy_sources.find(
(src) => src.type === "grid"
) as GridSourceTypeEnergyPreference | undefined;
showEnergySettingsGridPowerDialog(this, {
source: { ...origSource },
grid_source: gridSource,
saveCallback: async (source) => {
const power =
energySourcesByType(this.preferences).grid![0].power || [];
const preferences: EnergyPreferences = {
...this.preferences,
energy_sources: this.preferences.energy_sources.map((src) =>
src.type === "grid"
? {
...src,
power: power.map((p) => (p === origSource ? source : p)),
}
: src
),
};
await this._savePreferences(preferences);
},
});
}
private async _deletePowerSource(ev) {
const sourceToDelete: GridPowerSourceEnergyPreference =
ev.currentTarget.closest(".row").source;
if (
!(await showConfirmationDialog(this, {
title: this.hass.localize("ui.panel.config.energy.delete_source"),
}))
) {
return;
}
const power =
energySourcesByType(this.preferences).grid![0].power?.filter(
(p) => p !== sourceToDelete
) || [];
const preferences: EnergyPreferences = {
...this.preferences,
energy_sources: this.preferences.energy_sources.map((source) =>
source.type === "grid" ? { ...source, power } : source
),
};
const cleanedPreferences = this._removeEmptySources(preferences);
await this._savePreferences(cleanedPreferences);
}
private _removeEmptySources(preferences: EnergyPreferences) {
// Check if grid sources became an empty type and remove if so
preferences.energy_sources = preferences.energy_sources.reduce<
@@ -652,8 +507,7 @@ export class EnergyGridSettings extends LitElement {
if (
source.type !== "grid" ||
source.flow_from.length > 0 ||
source.flow_to.length > 0 ||
(source.power && source.power.length > 0)
source.flow_to.length > 0
) {
acc.push(source);
}

View File

@@ -18,7 +18,6 @@ import type { HomeAssistant } from "../../../../types";
import type { EnergySettingsBatteryDialogParams } from "./show-dialogs-energy";
const energyUnitClasses = ["energy"];
const powerUnitClasses = ["power"];
@customElement("dialog-energy-battery-settings")
export class DialogEnergyBatterySettings
@@ -33,14 +32,10 @@ export class DialogEnergyBatterySettings
@state() private _energy_units?: string[];
@state() private _power_units?: string[];
@state() private _error?: string;
private _excludeList?: string[];
private _excludeListPower?: string[];
public async showDialog(
params: EnergySettingsBatteryDialogParams
): Promise<void> {
@@ -51,9 +46,6 @@ export class DialogEnergyBatterySettings
this._energy_units = (
await getSensorDeviceClassConvertibleUnits(this.hass, "energy")
).units;
this._power_units = (
await getSensorDeviceClassConvertibleUnits(this.hass, "power")
).units;
const allSources: string[] = [];
this._params.battery_sources.forEach((entry) => {
allSources.push(entry.stat_energy_from);
@@ -64,9 +56,6 @@ export class DialogEnergyBatterySettings
id !== this._source?.stat_energy_from &&
id !== this._source?.stat_energy_to
);
this._excludeListPower = this._params.battery_sources
.map((entry) => entry.stat_power)
.filter((id) => id && id !== this._source?.stat_power) as string[];
}
public closeDialog() {
@@ -83,6 +72,8 @@ export class DialogEnergyBatterySettings
return nothing;
}
const pickableUnit = this._energy_units?.join(", ") || "";
return html`
<ha-dialog
open
@@ -94,6 +85,12 @@ export class DialogEnergyBatterySettings
@closed=${this.closeDialog}
>
${this._error ? html`<p class="error">${this._error}</p>` : ""}
<div>
${this.hass.localize(
"ui.panel.config.energy.battery.dialog.entity_para",
{ unit: pickableUnit }
)}
</div>
<ha-statistic-picker
.hass=${this.hass}
@@ -108,10 +105,6 @@ export class DialogEnergyBatterySettings
this._source.stat_energy_from,
]}
@value-changed=${this._statisticToChanged}
.helper=${this.hass.localize(
"ui.panel.config.energy.battery.dialog.energy_helper_into",
{ unit: this._energy_units?.join(", ") || "" }
)}
dialogInitialFocus
></ha-statistic-picker>
@@ -128,25 +121,6 @@ export class DialogEnergyBatterySettings
this._source.stat_energy_to,
]}
@value-changed=${this._statisticFromChanged}
.helper=${this.hass.localize(
"ui.panel.config.energy.battery.dialog.energy_helper_out",
{ unit: this._energy_units?.join(", ") || "" }
)}
></ha-statistic-picker>
<ha-statistic-picker
.hass=${this.hass}
.includeUnitClass=${powerUnitClasses}
.value=${this._source.stat_power}
.label=${this.hass.localize(
"ui.panel.config.energy.battery.dialog.power"
)}
.excludeStatistics=${this._excludeListPower}
@value-changed=${this._powerChanged}
.helper=${this.hass.localize(
"ui.panel.config.energy.battery.dialog.power_helper",
{ unit: this._power_units?.join(", ") || "" }
)}
></ha-statistic-picker>
<ha-button
@@ -176,10 +150,6 @@ export class DialogEnergyBatterySettings
this._source = { ...this._source!, stat_energy_from: ev.detail.value };
}
private _powerChanged(ev: CustomEvent<{ value: string }>) {
this._source = { ...this._source!, stat_power: ev.detail.value };
}
private async _save() {
try {
await this._params!.saveCallback(this._source!);
@@ -198,11 +168,7 @@ export class DialogEnergyBatterySettings
--mdc-dialog-max-width: 430px;
}
ha-statistic-picker {
display: block;
margin-bottom: var(--ha-space-4);
}
ha-statistic-picker:last-of-type {
margin-bottom: 0;
width: 100%;
}
`,
];

View File

@@ -21,7 +21,6 @@ import type { HomeAssistant } from "../../../../types";
import type { EnergySettingsDeviceDialogParams } from "./show-dialogs-energy";
const energyUnitClasses = ["energy"];
const powerUnitClasses = ["power"];
@customElement("dialog-energy-device-settings")
export class DialogEnergyDeviceSettings
@@ -36,14 +35,10 @@ export class DialogEnergyDeviceSettings
@state() private _energy_units?: string[];
@state() private _power_units?: string[];
@state() private _error?: string;
private _excludeList?: string[];
private _excludeListPower?: string[];
private _possibleParents: DeviceConsumptionEnergyPreference[] = [];
public async showDialog(
@@ -55,15 +50,9 @@ export class DialogEnergyDeviceSettings
this._energy_units = (
await getSensorDeviceClassConvertibleUnits(this.hass, "energy")
).units;
this._power_units = (
await getSensorDeviceClassConvertibleUnits(this.hass, "power")
).units;
this._excludeList = this._params.device_consumptions
.map((entry) => entry.stat_consumption)
.filter((id) => id !== this._device?.stat_consumption);
this._excludeListPower = this._params.device_consumptions
.map((entry) => entry.stat_power)
.filter((id) => id && id !== this._device?.stat_power) as string[];
}
private _computePossibleParents() {
@@ -104,6 +93,8 @@ export class DialogEnergyDeviceSettings
return nothing;
}
const pickableUnit = this._energy_units?.join(", ") || "";
return html`
<ha-dialog
open
@@ -117,6 +108,12 @@ export class DialogEnergyDeviceSettings
@closed=${this.closeDialog}
>
${this._error ? html`<p class="error">${this._error}</p>` : ""}
<div>
${this.hass.localize(
"ui.panel.config.energy.device_consumption.dialog.selected_stat_intro",
{ unit: pickableUnit }
)}
</div>
<ha-statistic-picker
.hass=${this.hass}
@@ -128,28 +125,9 @@ export class DialogEnergyDeviceSettings
)}
.excludeStatistics=${this._excludeList}
@value-changed=${this._statisticChanged}
.helper=${this.hass.localize(
"ui.panel.config.energy.device_consumption.dialog.selected_stat_intro",
{ unit: this._energy_units?.join(", ") || "" }
)}
dialogInitialFocus
></ha-statistic-picker>
<ha-statistic-picker
.hass=${this.hass}
.includeUnitClass=${powerUnitClasses}
.value=${this._device?.stat_power}
.label=${this.hass.localize(
"ui.panel.config.energy.device_consumption.dialog.device_consumption_power"
)}
.excludeStatistics=${this._excludeListPower}
@value-changed=${this._powerStatisticChanged}
.helper=${this.hass.localize(
"ui.panel.config.energy.device_consumption.dialog.selected_stat_intro",
{ unit: this._power_units?.join(", ") || "" }
)}
></ha-statistic-picker>
<ha-textfield
.label=${this.hass.localize(
"ui.panel.config.energy.device_consumption.dialog.display_name"
@@ -232,20 +210,6 @@ export class DialogEnergyDeviceSettings
this._computePossibleParents();
}
private _powerStatisticChanged(ev: CustomEvent<{ value: string }>) {
if (!this._device) {
return;
}
const newDevice = {
...this._device,
stat_power: ev.detail.value,
} as DeviceConsumptionEnergyPreference;
if (!newDevice.stat_power) {
delete newDevice.stat_power;
}
this._device = newDevice;
}
private _nameChanged(ev) {
const newDevice = {
...this._device!,
@@ -281,19 +245,15 @@ export class DialogEnergyDeviceSettings
return [
haStyleDialog,
css`
ha-statistic-picker {
display: block;
margin-bottom: var(--ha-space-2);
}
ha-statistic-picker {
width: 100%;
}
ha-select {
margin-top: var(--ha-space-4);
margin-top: 16px;
width: 100%;
}
ha-textfield {
margin-top: var(--ha-space-4);
margin-top: 16px;
width: 100%;
}
`,

View File

@@ -115,6 +115,8 @@ export class DialogEnergyGridFlowSettings
return nothing;
}
const pickableUnit = this._energy_units?.join(", ") || "";
const unitPriceSensor = this._pickedDisplayUnit
? `${this.hass.config.currency}/${this._pickedDisplayUnit}`
: undefined;
@@ -148,11 +150,19 @@ export class DialogEnergyGridFlowSettings
@closed=${this.closeDialog}
>
${this._error ? html`<p class="error">${this._error}</p>` : ""}
<p>
${this.hass.localize(
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.paragraph`
)}
</p>
<div>
<p>
${this.hass.localize(
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.paragraph`
)}
</p>
<p>
${this.hass.localize(
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.entity_para`,
{ unit: pickableUnit }
)}
</p>
</div>
<ha-statistic-picker
.hass=${this.hass}
@@ -168,10 +178,6 @@ export class DialogEnergyGridFlowSettings
)}
.excludeStatistics=${this._excludeList}
@value-changed=${this._statisticChanged}
.helper=${this.hass.localize(
`ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.entity_para`,
{ unit: this._energy_units?.join(", ") || "" }
)}
dialogInitialFocus
></ha-statistic-picker>
@@ -374,10 +380,6 @@ export class DialogEnergyGridFlowSettings
ha-dialog {
--mdc-dialog-max-width: 430px;
}
ha-statistic-picker {
display: block;
margin: var(--ha-space-4) 0;
}
ha-formfield {
display: block;
}

View File

@@ -1,153 +0,0 @@
import { mdiTransmissionTower } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/entity/ha-statistic-picker";
import "../../../../components/ha-dialog";
import "../../../../components/ha-button";
import type { GridPowerSourceEnergyPreference } from "../../../../data/energy";
import { energyStatisticHelpUrl } from "../../../../data/energy";
import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import type { EnergySettingsGridPowerDialogParams } from "./show-dialogs-energy";
const powerUnitClasses = ["power"];
@customElement("dialog-energy-grid-power-settings")
export class DialogEnergyGridPowerSettings
extends LitElement
implements HassDialog<EnergySettingsGridPowerDialogParams>
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: EnergySettingsGridPowerDialogParams;
@state() private _source?: GridPowerSourceEnergyPreference;
@state() private _power_units?: string[];
@state() private _error?: string;
private _excludeListPower?: string[];
public async showDialog(
params: EnergySettingsGridPowerDialogParams
): Promise<void> {
this._params = params;
this._source = params.source ? { ...params.source } : { stat_power: "" };
const initialSourceIdPower = this._source.stat_power;
this._power_units = (
await getSensorDeviceClassConvertibleUnits(this.hass, "power")
).units;
this._excludeListPower = [
...(this._params.grid_source?.power?.map((entry) => entry.stat_power) ||
[]),
].filter((id) => id && id !== initialSourceIdPower) as string[];
}
public closeDialog() {
this._params = undefined;
this._source = undefined;
this._error = undefined;
this._excludeListPower = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
return true;
}
protected render() {
if (!this._params || !this._source) {
return nothing;
}
return html`
<ha-dialog
open
.heading=${html`<ha-svg-icon
.path=${mdiTransmissionTower}
style="--mdc-icon-size: 32px;"
></ha-svg-icon
>${this.hass.localize(
"ui.panel.config.energy.grid.power_dialog.header"
)}`}
@closed=${this.closeDialog}
>
${this._error ? html`<p class="error">${this._error}</p>` : ""}
<ha-statistic-picker
.hass=${this.hass}
.helpMissingEntityUrl=${energyStatisticHelpUrl}
.includeUnitClass=${powerUnitClasses}
.value=${this._source.stat_power}
.label=${this.hass.localize(
"ui.panel.config.energy.grid.power_dialog.power_stat"
)}
.excludeStatistics=${this._excludeListPower}
@value-changed=${this._powerStatisticChanged}
.helper=${this.hass.localize(
"ui.panel.config.energy.grid.power_dialog.power_helper",
{ unit: this._power_units?.join(", ") || "" }
)}
dialogInitialFocus
></ha-statistic-picker>
<ha-button
appearance="plain"
@click=${this.closeDialog}
slot="primaryAction"
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
@click=${this._save}
.disabled=${!this._source.stat_power}
slot="primaryAction"
>
${this.hass.localize("ui.common.save")}
</ha-button>
</ha-dialog>
`;
}
private _powerStatisticChanged(ev: CustomEvent<{ value: string }>) {
this._source = {
...this._source!,
stat_power: ev.detail.value,
};
}
private async _save() {
try {
await this._params!.saveCallback(this._source!);
this.closeDialog();
} catch (err: any) {
this._error = err.message;
}
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-max-width: 430px;
}
ha-statistic-picker {
display: block;
margin: var(--ha-space-4) 0;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-energy-grid-power-settings": DialogEnergyGridPowerSettings;
}
}

View File

@@ -28,7 +28,6 @@ import { brandsUrl } from "../../../../util/brands-url";
import type { EnergySettingsSolarDialogParams } from "./show-dialogs-energy";
const energyUnitClasses = ["energy"];
const powerUnitClasses = ["power"];
@customElement("dialog-energy-solar-settings")
export class DialogEnergySolarSettings
@@ -47,14 +46,10 @@ export class DialogEnergySolarSettings
@state() private _energy_units?: string[];
@state() private _power_units?: string[];
@state() private _error?: string;
private _excludeList?: string[];
private _excludeListPower?: string[];
public async showDialog(
params: EnergySettingsSolarDialogParams
): Promise<void> {
@@ -67,15 +62,9 @@ export class DialogEnergySolarSettings
this._energy_units = (
await getSensorDeviceClassConvertibleUnits(this.hass, "energy")
).units;
this._power_units = (
await getSensorDeviceClassConvertibleUnits(this.hass, "power")
).units;
this._excludeList = this._params.solar_sources
.map((entry) => entry.stat_energy_from)
.filter((id) => id !== this._source?.stat_energy_from);
this._excludeListPower = this._params.solar_sources
.map((entry) => entry.stat_power)
.filter((id) => id && id !== this._source?.stat_power) as string[];
}
public closeDialog() {
@@ -92,6 +81,8 @@ export class DialogEnergySolarSettings
return nothing;
}
const pickableUnit = this._energy_units?.join(", ") || "";
return html`
<ha-dialog
open
@@ -103,6 +94,12 @@ export class DialogEnergySolarSettings
@closed=${this.closeDialog}
>
${this._error ? html`<p class="error">${this._error}</p>` : ""}
<div>
${this.hass.localize(
"ui.panel.config.energy.solar.dialog.entity_para",
{ unit: pickableUnit }
)}
</div>
<ha-statistic-picker
.hass=${this.hass}
@@ -114,28 +111,9 @@ export class DialogEnergySolarSettings
)}
.excludeStatistics=${this._excludeList}
@value-changed=${this._statisticChanged}
.helper=${this.hass.localize(
"ui.panel.config.energy.solar.dialog.entity_para",
{ unit: this._energy_units?.join(", ") || "" }
)}
dialogInitialFocus
></ha-statistic-picker>
<ha-statistic-picker
.hass=${this.hass}
.includeUnitClass=${powerUnitClasses}
.value=${this._source.stat_power}
.label=${this.hass.localize(
"ui.panel.config.energy.solar.dialog.solar_production_power"
)}
.excludeStatistics=${this._excludeListPower}
@value-changed=${this._powerStatisticChanged}
.helper=${this.hass.localize(
"ui.panel.config.energy.solar.dialog.entity_para",
{ unit: this._power_units?.join(", ") || "" }
)}
></ha-statistic-picker>
<h3>
${this.hass.localize(
"ui.panel.config.energy.solar.dialog.solar_production_forecast"
@@ -289,10 +267,6 @@ export class DialogEnergySolarSettings
this._source = { ...this._source!, stat_energy_from: ev.detail.value };
}
private _powerStatisticChanged(ev: CustomEvent<{ value: string }>) {
this._source = { ...this._source!, stat_power: ev.detail.value };
}
private async _save() {
try {
if (!this._forecast) {
@@ -313,10 +287,6 @@ export class DialogEnergySolarSettings
ha-dialog {
--mdc-dialog-max-width: 430px;
}
ha-statistic-picker {
display: block;
margin-bottom: var(--ha-space-4);
}
img {
height: 24px;
margin-right: 16px;

View File

@@ -7,7 +7,6 @@ import type {
FlowFromGridSourceEnergyPreference,
FlowToGridSourceEnergyPreference,
GasSourceTypeEnergyPreference,
GridPowerSourceEnergyPreference,
GridSourceTypeEnergyPreference,
SolarSourceTypeEnergyPreference,
WaterSourceTypeEnergyPreference,
@@ -42,12 +41,6 @@ export interface EnergySettingsGridFlowToDialogParams {
saveCallback: (source: FlowToGridSourceEnergyPreference) => Promise<void>;
}
export interface EnergySettingsGridPowerDialogParams {
source?: GridPowerSourceEnergyPreference;
grid_source?: GridSourceTypeEnergyPreference;
saveCallback: (source: GridPowerSourceEnergyPreference) => Promise<void>;
}
export interface EnergySettingsSolarDialogParams {
info: EnergyInfo;
source?: SolarSourceTypeEnergyPreference;
@@ -159,14 +152,3 @@ export const showEnergySettingsGridFlowToDialog = (
dialogParams: { ...dialogParams, direction: "to" },
});
};
export const showEnergySettingsGridPowerDialog = (
element: HTMLElement,
dialogParams: EnergySettingsGridPowerDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-energy-grid-power-settings",
dialogImport: () => import("./dialog-energy-grid-power-settings"),
dialogParams: dialogParams,
});
};

View File

@@ -110,7 +110,7 @@ class MoveDatadiskDialog extends LitElement {
>
${this._moving
? html`
<ha-spinner aria-label="Moving" size="large"></ha-spinner>
<ha-spinner aria-label="Moving" size="large"> </ha-spinner>
<p class="progress-text">
${this.hass.localize(
"ui.panel.config.storage.datadisk.moving_desc"
@@ -206,7 +206,8 @@ class MoveDatadiskDialog extends LitElement {
}
ha-spinner {
display: block;
margin: 32px auto;
margin: 32px;
text-align: center;
}
.progress-text {

View File

@@ -139,8 +139,7 @@ export class DialogStatisticsFixUnitsChanged extends LitElement {
await updateStatisticsMetadata(
this.hass,
this._params!.issue.data.statistic_id,
this._params!.issue.data.state_unit,
this._params!.issue.data.state_unit_class
this._params!.issue.data.state_unit
);
}
this._params?.fixedCallback!();

View File

@@ -16,7 +16,6 @@ import {
import type {
BarSeriesOption,
CallbackDataParams,
LineSeriesOption,
TopLevelFormatterParams,
} from "echarts/types/dist/shared";
import type { FrontendLocaleData } from "../../../../../data/translation";
@@ -28,7 +27,6 @@ import {
} from "../../../../../common/datetime/format_date";
import { formatTime } from "../../../../../common/datetime/format_time";
import type { ECOption } from "../../../../../resources/echarts";
import { filterXSS } from "../../../../../common/util/xss";
export function getSuggestedMax(dayDifference: number, end: Date): number {
let suggestedMax = new Date(end);
@@ -171,10 +169,11 @@ function formatTooltip(
compare
? `${(showCompareYear ? formatDateShort : formatDateVeryShort)(date, locale, config)}: `
: ""
}${formatTime(date, locale, config)}`;
if (params[0].componentSubType === "bar") {
period += ` ${formatTime(addHours(date, 1), locale, config)}`;
}
}${formatTime(date, locale, config)} ${formatTime(
addHours(date, 1),
locale,
config
)}`;
}
const title = `<h4 style="text-align: center; margin: 0;">${period}</h4>`;
@@ -202,7 +201,7 @@ function formatTooltip(
countNegative++;
}
}
return `${param.marker} ${filterXSS(param.seriesName!)}: ${value} ${unit}`;
return `${param.marker} ${param.seriesName}: ${value} ${unit}`;
})
.filter(Boolean);
let footer = "";
@@ -281,35 +280,6 @@ export function fillDataGapsAndRoundCaps(datasets: BarSeriesOption[]) {
});
}
export function fillLineGaps(datasets: LineSeriesOption[]) {
const buckets = Array.from(
new Set(
datasets
.map((dataset) =>
dataset.data!.map((datapoint) => Number(datapoint![0]))
)
.flat()
)
).sort((a, b) => a - b);
buckets.forEach((bucket, index) => {
for (let i = datasets.length - 1; i >= 0; i--) {
const dataPoint = datasets[i].data![index];
const item: any =
dataPoint && typeof dataPoint === "object" && "value" in dataPoint
? dataPoint
: { value: dataPoint };
const x = item.value?.[0];
if (x === undefined) {
continue;
}
if (Number(x) !== bucket) {
datasets[i].data?.splice(index, 0, [bucket, 0]);
}
}
});
return datasets;
}
export function getCompareTransform(start: Date, compareStart?: Date) {
if (!compareStart) {
return (ts: Date) => ts;

View File

@@ -6,7 +6,6 @@ import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import type { BarSeriesOption } from "echarts/charts";
import type { ECElementEvent } from "echarts/types/dist/shared";
import { filterXSS } from "../../../../common/util/xss";
import { getGraphColorByIndex } from "../../../../common/color/colors";
import { formatNumber } from "../../../../common/number/format_number";
import "../../../../components/chart/ha-chart-base";
@@ -97,8 +96,9 @@ export class HuiEnergyDevicesGraphCard
}
private _renderTooltip(params: any) {
const deviceName = filterXSS(this._getDeviceName(params.value[1]));
const title = `<h4 style="text-align: center; margin: 0;">${deviceName}</h4>`;
const title = `<h4 style="text-align: center; margin: 0;">${this._getDeviceName(
params.value[1]
)}</h4>`;
const value = `${formatNumber(
params.value[0] as number,
this.hass.locale,

View File

@@ -1,305 +0,0 @@
import { endOfToday, isToday, startOfToday } from "date-fns";
import type { HassConfig, UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import type { LineSeriesOption } from "echarts/charts";
import { graphic } from "echarts";
import "../../../../components/chart/ha-chart-base";
import "../../../../components/ha-card";
import type { EnergyData } from "../../../../data/energy";
import { getEnergyDataCollection } from "../../../../data/energy";
import type { StatisticValue } from "../../../../data/recorder";
import type { FrontendLocaleData } from "../../../../data/translation";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import type { HomeAssistant } from "../../../../types";
import type { LovelaceCard } from "../../types";
import type { PowerSourcesGraphCardConfig } from "../types";
import { hasConfigChanged } from "../../common/has-changed";
import { getCommonOptions, fillLineGaps } from "./common/energy-chart-options";
import type { ECOption } from "../../../../resources/echarts";
import { hex2rgb } from "../../../../common/color/convert-color";
@customElement("hui-power-sources-graph-card")
export class HuiPowerSourcesGraphCard
extends SubscribeMixin(LitElement)
implements LovelaceCard
{
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _config?: PowerSourcesGraphCardConfig;
@state() private _chartData: LineSeriesOption[] = [];
@state() private _start = startOfToday();
@state() private _end = endOfToday();
@state() private _compareStart?: Date;
@state() private _compareEnd?: Date;
protected hassSubscribeRequiredHostProps = ["_config"];
public hassSubscribe(): UnsubscribeFunc[] {
return [
getEnergyDataCollection(this.hass, {
key: this._config?.collection_key,
}).subscribe((data) => this._getStatistics(data)),
];
}
public getCardSize(): Promise<number> | number {
return 3;
}
public setConfig(config: PowerSourcesGraphCardConfig): void {
this._config = config;
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
return (
hasConfigChanged(this, changedProps) ||
changedProps.size > 1 ||
!changedProps.has("hass")
);
}
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
return html`
<ha-card>
${this._config.title
? html`<h1 class="card-header">${this._config.title}</h1>`
: ""}
<div
class="content ${classMap({
"has-header": !!this._config.title,
})}"
>
<ha-chart-base
.hass=${this.hass}
.data=${this._chartData}
.options=${this._createOptions(
this._start,
this._end,
this.hass.locale,
this.hass.config,
this._compareStart,
this._compareEnd
)}
></ha-chart-base>
${!this._chartData.some((dataset) => dataset.data!.length)
? html`<div class="no-data">
${isToday(this._start)
? this.hass.localize("ui.panel.lovelace.cards.energy.no_data")
: this.hass.localize(
"ui.panel.lovelace.cards.energy.no_data_period"
)}
</div>`
: nothing}
</div>
</ha-card>
`;
}
private _createOptions = memoizeOne(
(
start: Date,
end: Date,
locale: FrontendLocaleData,
config: HassConfig,
compareStart?: Date,
compareEnd?: Date
): ECOption =>
getCommonOptions(
start,
end,
locale,
config,
"kW",
compareStart,
compareEnd
)
);
private async _getStatistics(energyData: EnergyData): Promise<void> {
const datasets: LineSeriesOption[] = [];
const statIds = {
solar: {
stats: [] as string[],
color: "--energy-solar-color",
name: this.hass.localize(
"ui.panel.lovelace.cards.energy.power_graph.solar"
),
},
grid: {
stats: [] as string[],
color: "--energy-grid-consumption-color",
name: this.hass.localize(
"ui.panel.lovelace.cards.energy.power_graph.grid"
),
},
battery: {
stats: [] as string[],
color: "--energy-battery-out-color",
name: this.hass.localize(
"ui.panel.lovelace.cards.energy.power_graph.battery"
),
},
};
const computedStyles = getComputedStyle(this);
for (const source of energyData.prefs.energy_sources) {
if (source.type === "solar") {
if (source.stat_power) {
statIds.solar.stats.push(source.stat_power);
}
continue;
}
if (source.type === "battery") {
if (source.stat_power) {
statIds.battery.stats.push(source.stat_power);
}
continue;
}
if (source.type === "grid" && source.power) {
statIds.grid.stats.push(...source.power.map((p) => p.stat_power));
}
}
const commonSeriesOptions: LineSeriesOption = {
type: "line",
smooth: 0.4,
smoothMonotone: "x",
lineStyle: {
width: 1,
},
};
Object.keys(statIds).forEach((key, keyIndex) => {
if (statIds[key].stats.length) {
const colorHex = computedStyles.getPropertyValue(statIds[key].color);
const rgb = hex2rgb(colorHex);
const { positive, negative } = this._processData(
statIds[key].stats.map((id: string) => energyData.stats[id] ?? [])
);
datasets.push({
...commonSeriesOptions,
id: key,
name: statIds[key].name,
color: colorHex,
stack: "positive",
areaStyle: {
color: new graphic.LinearGradient(0, 0, 0, 1, [
{
offset: 0,
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.75)`,
},
{
offset: 1,
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.25)`,
},
]),
},
data: positive,
z: 3 - keyIndex, // draw in reverse order so 0 value lines are overwritten
});
if (key !== "solar") {
datasets.push({
...commonSeriesOptions,
id: `${key}-negative`,
name: statIds[key].name,
color: colorHex,
stack: "negative",
areaStyle: {
color: new graphic.LinearGradient(0, 1, 0, 0, [
{
offset: 0,
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.75)`,
},
{
offset: 1,
color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.25)`,
},
]),
},
data: negative,
z: 4 - keyIndex, // draw in reverse order but above positive series
});
}
}
});
this._start = energyData.start;
this._end = energyData.end || endOfToday();
this._chartData = fillLineGaps(datasets);
}
private _processData(stats: StatisticValue[][]) {
const data: Record<number, number[]> = {};
stats.forEach((statSet) => {
statSet.forEach((point) => {
if (point.mean == null) {
return;
}
const x = (point.start + point.end) / 2;
data[x] = [...(data[x] ?? []), point.mean];
});
});
const positive: [number, number][] = [];
const negative: [number, number][] = [];
Object.entries(data).forEach(([x, y]) => {
const ts = Number(x);
const meanY = y.reduce((a, b) => a + b, 0) / y.length;
positive.push([ts, Math.max(0, meanY)]);
negative.push([ts, Math.min(0, meanY)]);
});
return { positive, negative };
}
static styles = css`
ha-card {
height: 100%;
}
.card-header {
padding-bottom: 0;
}
.content {
padding: var(--ha-space-4);
}
.has-header {
padding-top: 0;
}
.no-data {
position: absolute;
height: 100%;
top: 0;
left: 0;
right: 0;
display: flex;
justify-content: center;
align-items: center;
padding: 20%;
margin-left: var(--ha-space-8);
margin-inline-start: var(--ha-space-8);
margin-inline-end: initial;
box-sizing: border-box;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"hui-power-sources-graph-card": HuiPowerSourcesGraphCard;
}
}

View File

@@ -1,4 +1,4 @@
import { mdiTextureBox } from "@mdi/js";
import { mdiPlay, mdiTextureBox } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import {
css,
@@ -13,6 +13,10 @@ import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import {
computeActiveAreaMediaStates,
type MediaPlayerEntity,
} from "../../../data/media-player";
import { computeCssColor } from "../../../common/color/compute-color";
import { BINARY_STATE_ON } from "../../../common/const";
import { computeAreaName } from "../../../common/entity/compute_area_name";
@@ -285,15 +289,19 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
);
}
private _renderAlertSensorBadge(): TemplateResult<1> | typeof nothing {
const states = this._computeActiveAlertStates();
private _computeActiveAreaMediaStates(): MediaPlayerEntity[] {
return computeActiveAreaMediaStates(this.hass, this._config?.area || "");
}
if (states.length === 0) {
private _renderAlertSensorBadge(
alertStates: HassEntity[]
): TemplateResult<1> | typeof nothing {
if (alertStates.length === 0) {
return nothing;
}
// Only render the first one when using a badge
const stateObj = states[0] as HassEntity | undefined;
const stateObj = alertStates[0] as HassEntity | undefined;
return html`
<ha-tile-badge class="alert-badge">
@@ -302,6 +310,30 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
`;
}
private _renderMediaBadge(): TemplateResult<1> | typeof nothing {
const states = this._computeActiveAreaMediaStates();
if (states.length === 0) {
return nothing;
}
return html`
<ha-tile-badge
class="media-badge"
.label=${this.hass.localize("ui.card.area.media_playing")}
>
<ha-svg-icon .path=${mdiPlay}></ha-svg-icon>
</ha-tile-badge>
`;
}
private _renderCompactBadge(): TemplateResult<1> | typeof nothing {
const alertStates = this._computeActiveAlertStates();
return alertStates.length > 0
? this._renderAlertSensorBadge(alertStates)
: this._renderMediaBadge();
}
private _renderAlertSensors(): TemplateResult<1> | typeof nothing {
const states = this._computeActiveAlertStates();
@@ -563,7 +595,7 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
<div class="content ${classMap(contentClasses)}">
<ha-tile-icon>
${displayType === "compact"
? this._renderAlertSensorBadge()
? this._renderCompactBadge()
: nothing}
${icon
? html`<ha-icon slot="icon" .icon=${icon}></ha-icon>`
@@ -741,6 +773,9 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
.alert-badge {
--tile-badge-background-color: var(--orange-color);
}
.media-badge {
--tile-badge-background-color: var(--light-blue-color);
}
.alerts {
position: absolute;
top: 0;

View File

@@ -49,8 +49,6 @@ import type {
LovelaceGridOptions,
} from "../types";
import type { ButtonCardConfig } from "./types";
import { computeCssColor } from "../../../common/color/compute-color";
import { stateActive } from "../../../common/entity/state_active";
export const getEntityDefaultButtonAction = (entityId?: string) =>
entityId && DOMAINS_TOGGLE.has(computeDomain(entityId))
@@ -124,6 +122,11 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
})
_entity?: EntityRegistryDisplayEntry;
private _getStateColor(stateObj: HassEntity, config: ButtonCardConfig) {
const domain = stateObj ? computeStateDomain(stateObj) : undefined;
return config && (config.state_color ?? domain === "light");
}
public getCardSize(): number {
return (
(this._config?.show_icon ? 4 : 0) + (this._config?.show_name ? 1 : 0)
@@ -163,8 +166,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
double_tap_action: { action: "none" },
show_icon: true,
show_name: true,
color:
config.color ?? (config.state_color === false ? "none" : undefined),
state_color: true,
...config,
};
}
@@ -187,6 +189,8 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
? this._config.name || (stateObj ? computeStateName(stateObj) : "")
: "";
const colored = stateObj && this._getStateColor(stateObj, this._config);
return html`
<ha-card
@action=${this._handleAction}
@@ -201,10 +205,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
hasAction(this._config.tap_action) ? "0" : undefined
)}
style=${styleMap({
"--state-color":
this._config.color !== "none"
? this._computeColor(stateObj, this._config)
: undefined,
"--state-color": colored ? this._computeColor(stateObj) : undefined,
})}
>
<ha-ripple></ha-ripple>
@@ -220,7 +221,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
.hass=${this.hass}
.stateObj=${stateObj}
style=${styleMap({
filter: stateObj ? stateColorBrightness(stateObj) : undefined,
filter: colored ? stateColorBrightness(stateObj) : undefined,
height: this._config.icon_height
? this._config.icon_height
: undefined,
@@ -333,20 +334,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
];
}
private _computeColor(
stateObj: HassEntity | undefined,
config: ButtonCardConfig
): string | undefined {
if (config.color) {
return !stateObj || stateActive(stateObj)
? computeCssColor(config.color)
: undefined;
}
if (!stateObj) {
return undefined;
}
private _computeColor(stateObj: HassEntity): string | undefined {
if (stateObj.attributes.rgb_color) {
return `rgb(${stateObj.attributes.rgb_color.join(",")})`;
}

View File

@@ -83,21 +83,6 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
private _footerElement?: LovelaceHeaderFooter;
connectedCallback(): void {
super.connectedCallback();
this.addEventListener("row-visibility-changed", (ev) =>
this._updateRowVisibility(ev)
);
}
disconnectedCallback(): void {
super.disconnectedCallback();
this.removeEventListener(
"row-visibility-changed",
this._updateRowVisibility
);
}
set hass(hass: HomeAssistant) {
this._hass = hass;
this.shadowRoot
@@ -266,9 +251,18 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
#states {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--entities-card-row-gap, var(--card-row-gap, 8px));
}
#states > * {
margin: 8px 0;
}
#states > *:first-child {
margin-top: 0;
}
#states > *:last-child {
margin-bottom: 0;
}
#states > div > * {
@@ -326,16 +320,8 @@ class HuiEntitiesCard extends LitElement implements LovelaceCard {
element.hass = this._hass;
}
return html`<div ?hidden=${element.hidden}>${element}</div>`;
return html`<div>${element}</div>`;
}
private _updateRowVisibility = (ev) => {
if (ev.detail?.value === false) {
ev.detail?.row?.parentElement!.style.setProperty("display", "none");
} else {
ev.detail?.row?.parentElement!.style.setProperty("display", "");
}
};
}
declare global {

View File

@@ -93,21 +93,17 @@ export class HuiPictureCard extends LitElement implements LovelaceCard {
changedProps.has("_config") &&
changedProps.get("_config")?.image !== this._config?.image;
const image =
(typeof this._config?.image === "object" &&
this._config.image.media_content_id) ||
(this._config.image as string | undefined);
if (
(firstHass || imageChanged) &&
typeof image === "string" &&
isMediaSourceContentId(image)
typeof this._config?.image === "string" &&
isMediaSourceContentId(this._config.image)
) {
this._resolvedImage = undefined;
resolveMediaSource(this.hass, image).then((result) => {
resolveMediaSource(this.hass, this._config?.image).then((result) => {
this._resolvedImage = result.url;
});
} else if (imageChanged) {
this._resolvedImage = image;
this._resolvedImage = this._config?.image;
}
}

View File

@@ -1,5 +1,5 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
@@ -9,7 +9,7 @@ import { computeCssColor } from "../../../common/color/compute-color";
import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color";
import { DOMAINS_TOGGLE } from "../../../common/const";
import { computeDomain } from "../../../common/entity/compute_domain";
import type { EntityNameItem } from "../../../common/entity/compute_entity_name_display";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { stateActive } from "../../../common/entity/state_active";
import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-card";
@@ -47,11 +47,6 @@ export const getEntityDefaultTileIconAction = (entityId: string) => {
return supportsIconAction ? "toggle" : "none";
};
export const DEFAULT_NAME = [
{ type: "device" },
{ type: "entity" },
] satisfies EntityNameItem[];
@customElement("hui-tile-card")
export class HuiTileCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
@@ -260,13 +255,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
const contentClasses = { vertical: Boolean(this._config.vertical) };
const nameConfig = this._config.name;
const nameDisplay =
typeof nameConfig === "string"
? nameConfig
: this.hass.formatEntityName(stateObj, nameConfig || DEFAULT_NAME);
const name = this._config.name || computeStateName(stateObj);
const active = stateActive(stateObj);
const color = this._computeStateColor(stateObj, this._config.color);
const domain = computeDomain(stateObj.entity_id);
@@ -278,7 +267,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
.stateObj=${stateObj}
.hass=${this.hass}
.content=${this._config.state_content}
.name=${nameDisplay}
.name=${this._config.name}
>
</state-display>
`;
@@ -337,7 +326,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
${renderTileBadge(stateObj, this.hass)}
</ha-tile-icon>
<ha-tile-info id="info">
<span slot="primary" class="primary">${nameDisplay}</span>
<span slot="primary" class="primary">${name}</span>
${stateDisplay
? html`<span slot="secondary">${stateDisplay}</span>`
: nothing}

View File

@@ -331,16 +331,14 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
: nothing}
${!this._reordering && uncheckedItems.length
? html`
${!this._config.hide_section_headers
? html`<div class="header">
<h2>
${this.hass!.localize(
"ui.panel.lovelace.cards.todo-list.unchecked_items"
)}
</h2>
${this._renderMenu(this._config, unavailable)}
</div>`
: nothing}
<div class="header" role="separator">
<h2>
${this.hass!.localize(
"ui.panel.lovelace.cards.todo-list.unchecked_items"
)}
</h2>
${this._renderMenu(this._config, unavailable)}
</div>
${this._renderItems(uncheckedItems, unavailable)}
`
: nothing}
@@ -368,41 +366,39 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
? html`
<div>
<div class="divider" role="separator"></div>
${!this._config.hide_section_headers
? html`<div class="header">
<h2>
${this.hass!.localize(
"ui.panel.lovelace.cards.todo-list.checked_items"
)}
</h2>
${this._todoListSupportsFeature(
TodoListEntityFeature.DELETE_TODO_ITEM
)
? html`<ha-button-menu
@closed=${stopPropagation}
fixed
@action=${this._handleCompletedMenuAction}
<div class="header">
<h2>
${this.hass!.localize(
"ui.panel.lovelace.cards.todo-list.checked_items"
)}
</h2>
${this._todoListSupportsFeature(
TodoListEntityFeature.DELETE_TODO_ITEM
)
? html`<ha-button-menu
@closed=${stopPropagation}
fixed
@action=${this._handleCompletedMenuAction}
>
<ha-icon-button
slot="trigger"
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-list-item graphic="icon" class="warning">
${this.hass!.localize(
"ui.panel.lovelace.cards.todo-list.clear_items"
)}
<ha-svg-icon
class="warning"
slot="graphic"
.path=${mdiDeleteSweep}
.disabled=${unavailable}
>
<ha-icon-button
slot="trigger"
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-list-item graphic="icon" class="warning">
${this.hass!.localize(
"ui.panel.lovelace.cards.todo-list.clear_items"
)}
<ha-svg-icon
class="warning"
slot="graphic"
.path=${mdiDeleteSweep}
.disabled=${unavailable}
>
</ha-svg-icon>
</ha-list-item>
</ha-button-menu>`
: nothing}
</div>`
: nothing}
</ha-svg-icon>
</ha-list-item>
</ha-button-menu>`
: nothing}
</div>
</div>
${this._renderItems(checkedItems, unavailable)}
`

View File

@@ -1,11 +1,8 @@
import type { HassServiceTarget } from "home-assistant-js-websocket";
import type { EntityNameItem } from "../../../common/entity/compute_entity_name_display";
import type { HaDurationData } from "../../../components/ha-duration-input";
import type { EnergySourceByType } from "../../../data/energy";
import type { ActionConfig } from "../../../data/lovelace/config/action";
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
import type { Statistic, StatisticType } from "../../../data/recorder";
import type { TimeFormat } from "../../../data/translation";
import type { ForecastType } from "../../../data/weather";
import type {
FullCalendarView,
@@ -28,8 +25,9 @@ import type {
} from "../entity-rows/types";
import type { LovelaceHeaderFooterConfig } from "../header-footer/types";
import type { LovelaceHeadingBadgeConfig } from "../heading-badges/types";
import type { TimeFormat } from "../../../data/translation";
import type { HomeSummary } from "../strategies/home/helpers/home-summaries";
import type { MediaSelectorValue } from "../../../data/selector";
import type { EnergySourceByType } from "../../../data/energy";
export type AlarmPanelCardConfigState =
| "arm_away"
@@ -137,10 +135,8 @@ export interface ButtonCardConfig extends LovelaceCardConfig {
tap_action?: ActionConfig;
hold_action?: ActionConfig;
double_tap_action?: ActionConfig;
/** @deprecated use `color` instead */
state_color?: boolean;
show_state?: boolean;
color?: string;
}
export interface EnergyCardBaseConfig extends LovelaceCardConfig {
@@ -227,11 +223,6 @@ export interface EnergySankeyCardConfig extends EnergyCardBaseConfig {
group_by_area?: boolean;
}
export interface PowerSourcesGraphCardConfig extends EnergyCardBaseConfig {
type: "power-sources-graph";
title?: string;
}
export interface EntityFilterCardConfig extends LovelaceCardConfig {
type: "entity-filter";
entities: (EntityFilterEntityConfig | string)[];
@@ -447,7 +438,7 @@ export interface StatisticCardConfig extends LovelaceCardConfig {
}
export interface PictureCardConfig extends LovelaceCardConfig {
image?: string | MediaSelectorValue;
image?: string;
image_entity?: string;
tap_action?: ActionConfig;
hold_action?: ActionConfig;
@@ -540,7 +531,6 @@ export interface TodoListCardConfig extends LovelaceCardConfig {
entity?: string;
hide_completed?: boolean;
hide_create?: boolean;
hide_section_headers?: boolean;
sort?: string;
}
@@ -578,7 +568,7 @@ export interface WeatherForecastCardConfig extends LovelaceCardConfig {
export interface TileCardConfig extends LovelaceCardConfig {
entity: string;
name?: string | EntityNameItem | EntityNameItem[];
name?: string;
hide_state?: boolean;
state_content?: string | string[];
icon?: string;

View File

@@ -3,8 +3,6 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { fireEvent } from "../../../common/dom/fire_event";
import { entityUseDeviceName } from "../../../common/entity/compute_entity_name";
import { computeRTL } from "../../../common/util/compute_rtl";
import "../../../components/entity/ha-entity-picker";
import type {
HaEntityPicker,
@@ -14,10 +12,11 @@ import "../../../components/ha-icon-button";
import "../../../components/ha-sortable";
import type { HomeAssistant } from "../../../types";
import type { EntityConfig } from "../entity-rows/types";
import { computeRTL } from "../../../common/util/compute_rtl";
@customElement("hui-entity-editor")
export class HuiEntityEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public entities?: EntityConfig[];
@@ -39,32 +38,20 @@ export class HuiEntityEditor extends LitElement {
}
private _renderItem(item: EntityConfig, index: number) {
const stateObj = this.hass.states[item.entity];
const stateObj = this.hass!.states[item.entity];
const useDeviceName = entityUseDeviceName(
stateObj,
this.hass.entities,
this.hass.devices
);
const entityName =
stateObj && this.hass!.formatEntityName(stateObj, "entity");
const deviceName =
stateObj && this.hass!.formatEntityName(stateObj, "device");
const areaName = stateObj && this.hass!.formatEntityName(stateObj, "area");
const name = this.hass.formatEntityName(
stateObj,
useDeviceName ? { type: "device" } : { type: "entity" }
);
const isRTL = computeRTL(this.hass!);
const isRTL = computeRTL(this.hass);
const primary = item.name || name || item.entity;
const secondary = this.hass.formatEntityName(
stateObj,
useDeviceName
? [{ type: "area" }]
: [{ type: "area" }, { type: "device" }],
{
separator: isRTL ? " ◂ " : " ▸ ",
}
);
const primary = item.name || entityName || deviceName || item.entity;
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
return html`
<ha-md-list-item class="item">
@@ -80,14 +67,14 @@ export class HuiEntityEditor extends LitElement {
slot="end"
.item=${item}
.index=${index}
.label=${this.hass.localize("ui.common.edit")}
.label=${this.hass!.localize("ui.common.edit")}
.path=${mdiPencil}
@click=${this._editItem}
></ha-icon-button>
<ha-icon-button
slot="end"
.index=${index}
.label=${this.hass.localize("ui.common.delete")}
.label=${this.hass!.localize("ui.common.delete")}
.path=${mdiClose}
@click=${this._deleteItem}
></ha-icon-button>
@@ -122,9 +109,9 @@ export class HuiEntityEditor extends LitElement {
return html`
<h3>
${this.label ||
this.hass.localize("ui.panel.lovelace.editor.card.generic.entities") +
this.hass!.localize("ui.panel.lovelace.editor.card.generic.entities") +
" (" +
this.hass.localize("ui.panel.lovelace.editor.card.config.required") +
this.hass!.localize("ui.panel.lovelace.editor.card.config.required") +
")"}
</h3>
${this.canEdit

View File

@@ -66,8 +66,6 @@ const LAZY_LOAD_TYPES = {
"energy-usage-graph": () =>
import("../cards/energy/hui-energy-usage-graph-card"),
"energy-sankey": () => import("../cards/energy/hui-energy-sankey-card"),
"power-sources-graph": () =>
import("../cards/energy/hui-power-sources-graph-card"),
"entity-filter": () => import("../cards/hui-entity-filter-card"),
error: () => import("../cards/hui-error-card"),
"home-summary": () => import("../cards/hui-home-summary-card"),

View File

@@ -6,7 +6,6 @@ import memoizeOne from "memoize-one";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeDomain } from "../../../../common/entity/compute_domain";
import { computeEntityNameList } from "../../../../common/entity/compute_entity_name_display";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import { computeRTL } from "../../../../common/util/compute_rtl";
import "../../../../components/data-table/ha-data-table";
@@ -63,14 +62,9 @@ export class HuiEntityPickerTable extends LitElement {
(entity) => {
const stateObj = this.hass.states[entity];
const [entityName, deviceName, areaName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
const entityName = this.hass.formatEntityName(stateObj, "entity");
const deviceName = this.hass.formatEntityName(stateObj, "device");
const areaName = this.hass.formatEntityName(stateObj, "area");
const name = [deviceName, entityName].filter(Boolean).join(" ");
const domain = computeDomain(entity);

View File

@@ -32,8 +32,6 @@ const cardConfigStruct = assign(
double_tap_action: optional(actionConfigStruct),
theme: optional(string()),
show_state: optional(boolean()),
state_color: optional(boolean()),
color: optional(string()),
})
);
@@ -48,19 +46,6 @@ export class HuiButtonCardEditor
public setConfig(config: ButtonCardConfig): void {
assert(config, cardConfigStruct);
// Migrate state_color to color
if (config.state_color !== undefined) {
config = {
...config,
color: config.state_color ? undefined : "none",
};
delete config.state_color;
fireEvent(this, "config-changed", { config: config });
return;
}
this._config = config;
}
@@ -68,11 +53,11 @@ export class HuiButtonCardEditor
(entityId: string | undefined) =>
[
{ name: "entity", selector: { entity: {} } },
{ name: "name", selector: { text: {} } },
{
name: "",
type: "grid",
schema: [
{ name: "name", selector: { text: {} } },
{
name: "icon",
selector: {
@@ -82,18 +67,6 @@ export class HuiButtonCardEditor
icon_entity: "entity",
},
},
{ name: "icon_height", selector: { text: { suffix: "px" } } },
{
name: "color",
selector: {
ui_color: {
default_color: "state",
include_state: true,
include_none: true,
},
},
},
{ name: "theme", selector: { theme: {} } },
],
},
{
@@ -106,6 +79,14 @@ export class HuiButtonCardEditor
{ name: "show_icon", selector: { boolean: {} } },
],
},
{
name: "",
type: "grid",
schema: [
{ name: "icon_height", selector: { text: { suffix: "px" } } },
{ name: "theme", selector: { theme: {} } },
],
},
{
name: "interactions",
type: "expandable",

View File

@@ -1,8 +1,7 @@
import { mdiGestureTap } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { assert, assign, object, optional, string, union } from "superstruct";
import memoizeOne from "memoize-one";
import { assert, assign, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { SchemaUnion } from "../../../../components/ha-form/types";
import "../../../../components/ha-theme-picker";
@@ -12,12 +11,11 @@ import "../../components/hui-action-editor";
import type { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import type { LocalizeFunc } from "../../../../common/translations/localize";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
image: optional(union([string(), object()])),
image: optional(string()),
image_entity: optional(string()),
tap_action: optional(actionConfigStruct),
hold_action: optional(actionConfigStruct),
@@ -27,6 +25,47 @@ const cardConfigStruct = assign(
})
);
const SCHEMA = [
{ name: "image", selector: { image: {} } },
{
name: "image_entity",
selector: { entity: { domain: ["image", "person"] } },
},
{ name: "alt_text", selector: { text: {} } },
{ name: "theme", selector: { theme: {} } },
{
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: (["hold_action", "double_tap_action"] as const).map(
(action) => ({
name: action,
selector: {
ui_action: {
default_action: "none" as const,
},
},
})
),
},
],
},
] as const;
@customElement("hui-picture-card-editor")
export class HuiPictureCardEditor
extends LitElement
@@ -36,63 +75,6 @@ export class HuiPictureCardEditor
@state() private _config?: PictureCardConfig;
private _schema = memoizeOne(
(localize: LocalizeFunc) =>
[
{
name: "image",
selector: {
media: {
accept: ["image/*"] as string[],
clearable: true,
image_upload: true,
hide_content_type: true,
content_id_helper: localize(
"ui.panel.lovelace.editor.card.picture.content_id_helper"
),
},
},
},
{
name: "image_entity",
selector: { entity: { domain: ["image", "person"] } },
},
{ name: "alt_text", selector: { text: {} } },
{ name: "theme", selector: { theme: {} } },
{
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{
name: "tap_action",
selector: {
ui_action: {
default_action: "more-info",
},
},
},
{
name: "",
type: "optional_actions",
flatten: true,
schema: (["hold_action", "double_tap_action"] as const).map(
(action) => ({
name: action,
selector: {
ui_action: {
default_action: "none" as const,
},
},
})
),
},
],
},
] as const
);
public setConfig(config: PictureCardConfig): void {
assert(config, cardConfigStruct);
this._config = config;
@@ -106,28 +88,19 @@ export class HuiPictureCardEditor
return html`
<ha-form
.hass=${this.hass}
.data=${this._processData(this._config)}
.schema=${this._schema(this.hass.localize)}
.data=${this._config}
.schema=${SCHEMA}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _processData = memoizeOne((config: PictureCardConfig) => ({
...config,
...(typeof config.image === "string"
? { image: { media_content_id: config.image } }
: {}),
}));
private _valueChanged(ev: CustomEvent): void {
fireEvent(this, "config-changed", { config: ev.detail.value });
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
switch (schema.name) {
case "theme":
return `${this.hass!.localize(

View File

@@ -30,15 +30,11 @@ import type {
LovelaceCardFeatureConfig,
LovelaceCardFeatureContext,
} from "../../card-features/types";
import {
DEFAULT_NAME,
getEntityDefaultTileIconAction,
} from "../../cards/hui-tile-card";
import { getEntityDefaultTileIconAction } from "../../cards/hui-tile-card";
import type { TileCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import { entityNameStruct } from "../structs/entity-name-struct";
import type { EditDetailElementEvent, EditSubElementEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
import { getSupportedFeaturesType } from "./hui-card-features-editor";
@@ -47,7 +43,7 @@ const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
entity: optional(string()),
name: optional(entityNameStruct),
name: optional(string()),
icon: optional(string()),
color: optional(string()),
show_entity_picture: optional(boolean()),
@@ -101,19 +97,11 @@ export class HuiTileCardEditor
type: "expandable",
iconPath: mdiTextShort,
schema: [
{
name: "name",
selector: {
entity_name: {
default_name: DEFAULT_NAME,
},
},
context: { entity: "entity" },
},
{
name: "",
type: "grid",
schema: [
{ name: "name", selector: { text: {} } },
{
name: "icon",
selector: {

View File

@@ -32,7 +32,6 @@ const cardConfigStruct = assign(
entity: optional(string()),
hide_completed: optional(boolean()),
hide_create: optional(boolean()),
hide_section_headers: optional(boolean()),
display_order: optional(string()),
item_tap_action: optional(string()),
})
@@ -60,7 +59,6 @@ export class HuiTodoListEditor
{ name: "theme", selector: { theme: {} } },
{ name: "hide_completed", selector: { boolean: {} } },
{ name: "hide_create", selector: { boolean: {} } },
{ name: "hide_section_headers", selector: { boolean: {} } },
{
name: "display_order",
selector: {
@@ -133,7 +131,6 @@ export class HuiTodoListEditor
.data=${this._data(this._config)}
.schema=${this._schema(this.hass.localize, this._todoListSupportsFeature(TodoListEntityFeature.MOVE_TODO_ITEM))}
.computeLabel=${this._computeLabelCallback}
.computeHelper=${this._computeHelperCallback}
@value-changed=${this._valueChanged}
></ha-form>
</div>
@@ -167,7 +164,6 @@ export class HuiTodoListEditor
)})`;
case "hide_completed":
case "hide_create":
case "hide_section_headers":
case "display_order":
case "item_tap_action":
return this.hass!.localize(
@@ -180,19 +176,6 @@ export class HuiTodoListEditor
}
};
private _computeHelperCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
switch (schema.name) {
case "hide_section_headers":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.todo-list.${schema.name}_helper`
);
default:
return undefined;
}
};
static get styles(): CSSResultGroup {
return configElementStyle;
}

View File

@@ -22,7 +22,6 @@ const NON_STANDARD_URLS = {
"energy-devices-graph": "energy/#devices-energy-graph",
"energy-devices-detail-graph": "energy/#detail-devices-energy-graph",
"energy-sankey": "energy/#sankey-energy-graph",
"power-sources-graph": "energy/#power-sources-graph",
};
export const getCardDocumentationURL = (

View File

@@ -4,8 +4,8 @@ import { property, query, state } from "lit/decorators";
import { cache } from "lit/directives/cache";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { fireEvent } from "../../../common/dom/fire_event";
import { handleStructError } from "../../../common/structs/handle-errors";
import { debounce } from "../../../common/util/debounce";
import { handleStructError } from "../../../common/structs/handle-errors";
import { deepEqual } from "../../../common/util/deep-equal";
import "../../../components/ha-alert";
import "../../../components/ha-spinner";
@@ -57,6 +57,8 @@ export abstract class HuiElementEditor<
@property({ attribute: false }) public context?: C;
@property({ attribute: false }) public schema?;
@state() private _config?: T;
@state() private _configElement?: LovelaceGenericElementEditor;
@@ -312,6 +314,9 @@ export abstract class HuiElementEditor<
if (this._configElement && changedProperties.has("context")) {
this._configElement.context = this.context;
}
if (this._configElement && changedProperties.has("schema")) {
this._configElement.schema = this.schema;
}
}
private _handleUIConfigChanged(ev: UIConfigChangedEvent<T>) {
@@ -399,6 +404,7 @@ export abstract class HuiElementEditor<
configElement.lovelace = this.lovelace;
}
configElement.context = this.context;
configElement.schema = this.schema;
configElement.addEventListener("config-changed", (ev) =>
this._handleUIConfigChanged(ev as UIConfigChangedEvent<T>)
);

View File

@@ -1,19 +0,0 @@
import { customElement, property } from "lit/decorators";
import type { HaFormSchema } from "../../../components/ha-form/types";
import type { LovelaceConfigForm } from "../types";
import { HuiElementEditor } from "./hui-element-editor";
@customElement("hui-form-element-editor")
export class HuiFormElementEditor extends HuiElementEditor {
@property({ attribute: false }) public schema!: HaFormSchema[];
protected async getConfigForm(): Promise<LovelaceConfigForm | undefined> {
return { schema: this.schema };
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-form-element-editor": HuiFormElementEditor;
}
}

View File

@@ -12,7 +12,6 @@ import "./feature-editor/hui-card-feature-element-editor";
import "./header-footer-editor/hui-header-footer-element-editor";
import "./heading-badge-editor/hui-heading-badge-element-editor";
import type { HuiElementEditor } from "./hui-element-editor";
import "./hui-form-element-editor";
import "./picture-element-editor/hui-picture-element-element-editor";
import type { GUIModeChangedEvent, SubElementEditorConfig } from "./types";
@@ -84,18 +83,6 @@ export class HuiSubElementEditor extends LitElement {
private _renderEditor() {
const type = this.config.type;
if (this.schema) {
return html`
<hui-form-element-editor
class="editor"
.hass=${this.hass}
.value=${this.config.elementConfig}
.schema=${this.schema}
.context=${this.config.context}
@config-changed=${this._handleConfigChanged}
></hui-form-element-editor>
`;
}
switch (type) {
case "row":
return html`
@@ -104,6 +91,7 @@ export class HuiSubElementEditor extends LitElement {
.hass=${this.hass}
.value=${this.config.elementConfig}
.context=${this.config.context}
.schema=${this.schema}
@config-changed=${this._handleConfigChanged}
@GUImode-changed=${this._handleGUIModeChanged}
></hui-row-element-editor>

View File

@@ -1,22 +0,0 @@
import { array, literal, object, string, union } from "superstruct";
const entityNameItemStruct = union([
object({
type: literal("text"),
text: string(),
}),
object({
type: union([
literal("entity"),
literal("device"),
literal("area"),
literal("floor"),
]),
}),
string(),
]);
export const entityNameStruct = union([
entityNameItemStruct,
array(entityNameItemStruct),
]);

View File

@@ -318,7 +318,7 @@ class HUIRoot extends LitElement {
menu-corner="END"
>
<ha-icon-button
.id="button-${index}"
.label=${label}
.path=${item.icon}
slot="trigger"
></ha-icon-button>
@@ -340,9 +340,6 @@ class HUIRoot extends LitElement {
`
)}
</ha-button-menu>
<ha-tooltip placement="bottom" .for="button-${index}">
${label}
</ha-tooltip>
`
: html`
<ha-icon-button

View File

@@ -7,13 +7,7 @@ import type {
EntityConfig,
LovelaceRow,
} from "../entity-rows/types";
import { fireEvent } from "../../../common/dom/fire_event";
declare global {
interface HASSDomEvents {
"row-visibility-changed": { row: LovelaceRow; value: boolean };
}
}
@customElement("hui-conditional-row")
class HuiConditionalRow extends HuiConditionalBase implements LovelaceRow {
public setConfig(config: ConditionalRowConfig): void {
@@ -32,15 +26,6 @@ class HuiConditionalRow extends HuiConditionalBase implements LovelaceRow {
: config.row
) as LovelaceRow;
}
protected setVisibility(conditionMet: boolean): void {
const visible = this.preview || conditionMet;
const previouslyHidden = this.hidden;
super.setVisibility(conditionMet);
if (previouslyHidden !== this.hidden) {
fireEvent(this, "row-visibility-changed", { row: this, value: visible });
}
}
}
declare global {

View File

@@ -270,12 +270,15 @@ export class HomeAreaViewStrategy extends ReactiveElement {
})),
],
} satisfies HeadingCardConfig,
...entities.map((e) => ({
...computeTileCard(e),
name: {
type: "entity",
},
})),
...entities.map((e) => {
const stateObj = hass.states[e];
return {
...computeTileCard(e),
name:
hass.formatEntityName(stateObj, "entity") ||
hass.formatEntityName(stateObj, "device"),
};
}),
],
});
}

View File

@@ -4,7 +4,6 @@ import { isComponentLoaded } from "../../../../common/config/is_component_loaded
import { generateEntityFilter } from "../../../../common/entity/entity_filter";
import type { AreaRegistryEntry } from "../../../../data/area_registry";
import { getEnergyPreferences } from "../../../../data/energy";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type {
LovelaceSectionConfig,
LovelaceSectionRawConfig,
@@ -19,9 +18,8 @@ import type {
TileCardConfig,
WeatherForecastCardConfig,
} from "../../cards/types";
import { getAreas, getFloors } from "../areas/helpers/areas-strategy-helper";
import { getAreas } from "../areas/helpers/areas-strategy-helper";
import type { CommonControlSectionStrategyConfig } from "../usage_prediction/common-controls-section-strategy";
import { getHomeStructure } from "./helpers/home-structure";
export interface HomeMainViewStrategyConfig {
type: "home-main";
@@ -61,67 +59,25 @@ export class HomeMainViewStrategy extends ReactiveElement {
hass: HomeAssistant
): Promise<LovelaceViewConfig> {
const areas = getAreas(hass.areas);
const floors = getFloors(hass.floors);
const home = getHomeStructure(floors, areas);
const floorCount = home.floors.length + (home.areas.length ? 1 : 0);
const areasSection: LovelaceSectionConfig = {
type: "grid",
column_span: 2,
cards: [
{
type: "heading",
heading_style: "title",
heading: hass.localize("ui.panel.lovelace.strategy.home.areas"),
},
...areas.map<AreaCardConfig>((area) =>
computeAreaCard(area.area_id, hass)
),
],
};
// Allow between 2 and 3 columns (the max should be set to define the width of the header)
const maxColumns = 2;
const floorsSections: LovelaceSectionConfig[] = [];
for (const floorStructure of home.floors) {
const floorId = floorStructure.id;
const areaIds = floorStructure.areas;
const floor = hass.floors[floorId];
const cards: LovelaceCardConfig[] = [];
for (const areaId of areaIds) {
cards.push(computeAreaCard(areaId, hass));
}
if (cards.length) {
floorsSections.push({
type: "grid",
column_span: maxColumns,
cards: [
{
type: "heading",
heading:
floorCount > 1
? floor.name
: hass.localize("ui.panel.lovelace.strategy.home.areas"),
heading_style: "title",
},
...cards,
],
});
}
}
if (home.areas.length) {
const cards: LovelaceCardConfig[] = [];
for (const areaId of home.areas) {
cards.push(computeAreaCard(areaId, hass));
}
floorsSections.push({
type: "grid",
column_span: maxColumns,
cards: [
{
type: "heading",
heading:
floorCount > 1
? hass.localize("ui.panel.lovelace.strategy.home.other_areas")
: hass.localize("ui.panel.lovelace.strategy.home.areas"),
heading_style: "title",
},
...cards,
],
});
}
const favoriteSection: LovelaceSectionConfig = {
type: "grid",
column_span: maxColumns,
@@ -278,7 +234,7 @@ export class HomeMainViewStrategy extends ReactiveElement {
favoriteSection.cards && favoriteSection,
commonControlsSection,
summarySection,
...floorsSections,
areasSection,
widgetSection.cards && widgetSection,
] satisfies (LovelaceSectionRawConfig | undefined)[]
).filter(Boolean) as LovelaceSectionRawConfig[];

View File

@@ -161,9 +161,6 @@ export const haStyleDialog = css`
--mdc-dialog-min-height: 100svh;
--mdc-dialog-max-height: 100vh;
--mdc-dialog-max-height: 100svh;
--dialog-surface-padding: var(--safe-area-inset-top)
var(--safe-area-inset-right) var(--safe-area-inset-bottom)
var(--safe-area-inset-left);
--vertical-align-dialog: flex-end;
--ha-dialog-border-radius: var(--ha-border-radius-square);
}

View File

@@ -152,10 +152,6 @@ export const semanticColorStyles = css`
--ha-color-on-success-quiet: var(--ha-color-green-50);
--ha-color-on-success-normal: var(--ha-color-green-40);
--ha-color-on-success-loud: var(--white-color);
/* Surfaces */
--ha-color-surface-default: var(--ha-color-neutral-95);
--ha-color-on-surface-default: var(--ha-color-neutral-05);
}
`;
@@ -284,9 +280,5 @@ export const darkSemanticColorStyles = css`
--ha-color-on-success-quiet: var(--ha-color-green-70);
--ha-color-on-success-normal: var(--ha-color-green-60);
--ha-color-on-success-loud: var(--white-color);
/* Surfaces */
--ha-color-surface-default: var(--ha-color-neutral-10);
--ha-color-on-surface-default: var(--ha-color-neutral-95);
}
`;

View File

@@ -106,7 +106,8 @@
}
},
"area": {
"area_not_found": "Area not found."
"area_not_found": "Area not found.",
"media_playing": "Media playing"
},
"automation": {
"last_triggered": "Last triggered",
@@ -656,18 +657,6 @@
"placeholder": "Select an entity",
"create_helper": "Create a new {domain, select, \n undefined {} \n other {{domain} }\n } helper."
},
"entity-name-picker": {
"types": {
"floor": "Floor",
"area": "Area",
"device": "Device",
"entity": "Entity",
"area_missing": "No area assigned",
"floor_missing": "No floor assigned",
"device_missing": "No related device"
},
"add": "Add"
},
"entity-attribute-picker": {
"attribute": "Attribute",
"show_attributes": "Show attributes"
@@ -3036,15 +3025,6 @@
"grid_carbon_footprint": "Grid carbon footprint",
"remove_co2_signal": "Remove Electricity Maps integration",
"add_co2_signal": "Add Electricity Maps integration",
"grid_power": "Grid power",
"add_power": "Add power sensor",
"edit_power": "Edit power sensor",
"delete_power": "Delete power sensor",
"power_dialog": {
"header": "Configure grid power",
"power_stat": "Power sensor",
"power_helper": "Pick a sensor which measures grid power in either of {unit}. Positive values indicate using electricity from the grid, negative values indicate exporting electricity to the grid."
},
"flow_dialog": {
"from": {
"header": "Configure grid consumption",
@@ -3091,7 +3071,6 @@
"header": "Configure solar panels",
"entity_para": "Pick a sensor which measures solar energy production in either of {unit}.",
"solar_production_energy": "Solar production energy",
"solar_production_power": "Solar production power",
"solar_production_forecast": "Solar production forecast",
"solar_production_forecast_description": "Adding solar production forecast information will allow you to quickly see your expected production for today.",
"dont_forecast_production": "Don't forecast production",
@@ -3109,12 +3088,9 @@
"add_battery_system": "Add battery system",
"dialog": {
"header": "Configure battery system",
"energy_helper_into": "Pick a sensor that measures the electricity flowing into the battery in either of {unit}.",
"energy_helper_out": "Pick a sensor that measures the electricity flowing out of the battery in either of {unit}.",
"energy_into_battery": "Energy charged into the battery",
"energy_out_of_battery": "Energy discharged from the battery",
"power": "Battery power",
"power_helper": "Pick a sensor which measures the electricity flowing into and out of the battery in either of {unit}. Positive values indicate discharging the battery, negative values indicate charging the battery."
"entity_para": "Pick sensors which measure energy going into and coming out of the battery in either of {unit}.",
"energy_into_battery": "Energy going into the battery",
"energy_out_of_battery": "Energy coming out of the battery"
}
},
"gas": {
@@ -3176,8 +3152,7 @@
"header": "Add a device",
"display_name": "Display name",
"device_consumption_energy": "Device energy consumption",
"device_consumption_power": "Device power consumption",
"selected_stat_intro": "Select the sensor that measures the device's electricity usage in either of {unit}.",
"selected_stat_intro": "Select the energy sensor that measures the device's energy usage in either of {unit}.",
"included_in_device": "Upstream device",
"included_in_device_helper": "If this device is already counted by another device (such as a smart switch measured by a smart breaker), selecting the upstream device prevents duplicate energy tracking.",
"no_upstream_devices": "No eligible upstream devices"
@@ -5855,8 +5830,8 @@
"change_channel_initiated_text": "The channel change has been initiated and will complete in {delay} {delay, plural,\n one {minute}\n other {minutes}\n}.",
"change_channel_invalid": "Invalid channel",
"change_channel_label": "Channel",
"change_channel_multiprotocol_enabled_title": "The Thread adapter has multiprotocol enabled",
"change_channel_multiprotocol_enabled_text": "To change channel when the Thread adapter has multiprotocol enabled, please use the hardware settings menu.",
"change_channel_multiprotocol_enabled_title": "The Thread radio has multiprotocol enabled",
"change_channel_multiprotocol_enabled_text": "To change channel when the Thread radio has multiprotocol enabled, please use the hardware settings menu.",
"change_channel_range": "Channel must be in the range 11 to 26",
"change_channel_text": "Initiating a channel change for your Home Assistant Thread network should be performed with caution. Some Thread devices may not migrate to the new channel automatically and, if the new channel is congested, your Thread devices may become intermittently unavailable. Some devices may need to be manually re-joined to your Thread network before they show in Home Assistant again. This action cannot be reversed (without performing another channel change).",
"thread_network_info": "Thread network information",
@@ -5901,12 +5876,12 @@
"devices_offline": "{count} offline",
"update_button": "Update configuration",
"download_backup": "Download backup",
"migrate_radio": "Migrate adapter",
"migrate_radio": "Migrate radio",
"network_settings_title": "Network settings",
"change_channel": "Change channel",
"channel_dialog": {
"title": "Multiprotocol add-on in use",
"text": "Zigbee and Thread share the same adapter and must use the same channel. Change the channel of both networks by reconfiguring multiprotocol from the hardware menu."
"text": "Zigbee and Thread share the same radio and must use the same channel. Change the channel of both networks by reconfiguring multiprotocol from the hardware menu."
}
},
"add_device_page": {
@@ -7053,11 +7028,6 @@
"card_indicates_energy_used": "This card indicates how much of the electricity consumed by your home was generated using non-fossil fuels like solar, wind, and nuclear. The higher, the better!",
"low_carbon_energy_consumed": "Low-carbon electricity consumed",
"low_carbon_energy_not_calculated": "Consumed low-carbon electricity couldn't be calculated"
},
"power_graph": {
"grid": "Grid",
"solar": "Solar",
"battery": "Battery"
}
},
"heading": {
@@ -7899,8 +7869,7 @@
},
"picture": {
"name": "Picture",
"description": "The Picture card allows you to set an image to use for navigation to various paths in your interface or to perform an action.",
"content_id_helper": "Enter a media_source id or a URL for the image to be displayed."
"description": "The Picture card allows you to set an image to use for navigation to various paths in your interface or to perform an action."
},
"picture-elements": {
"name": "Picture elements",
@@ -7954,8 +7923,6 @@
"integration_not_loaded": "This card requires the `todo` integration to be set up.",
"hide_completed": "Hide completed items",
"hide_create": "Hide 'Add item' field",
"hide_section_headers": "Hide section headers",
"hide_section_headers_helper": "Removes the 'Active' and 'Completed' section headers and their overflow menus.",
"display_order": "Display order",
"item_tap_action": "Item tap behavior",
"actions": {

View File

@@ -9,10 +9,7 @@ import type {
HassServiceTarget,
MessageBase,
} from "home-assistant-js-websocket";
import type {
EntityNameItem,
EntityNameOptions,
} from "./common/entity/compute_entity_name_display";
import type { EntityNameType } from "./common/translations/entity-state";
import type { LocalizeFunc } from "./common/translations/localize";
import type { AreaRegistryEntry } from "./data/area_registry";
import type { DeviceRegistryEntry } from "./data/device_registry";
@@ -291,8 +288,8 @@ export interface HomeAssistant {
formatEntityAttributeName(stateObj: HassEntity, attribute: string): string;
formatEntityName(
stateObj: HassEntity,
type: EntityNameItem | EntityNameItem[],
separator?: EntityNameOptions
type: EntityNameType | EntityNameType[],
separator?: string
): string;
}

View File

@@ -0,0 +1,196 @@
import { describe, expect, it } from "vitest";
import {
computeActiveAreaMediaStates,
type MediaPlayerEntity,
} from "../../../src/data/media-player";
describe("computeActiveAreaMediaStates", () => {
it("returns playing media entities in the area", () => {
const hass = {
areas: { living_room: { area_id: "living_room" } },
entities: {
"media_player.tv": {
entity_id: "media_player.tv",
area_id: "living_room",
},
"media_player.speaker": {
entity_id: "media_player.speaker",
area_id: "living_room",
},
},
states: {
"media_player.tv": {
entity_id: "media_player.tv",
state: "playing",
} as MediaPlayerEntity,
"media_player.speaker": {
entity_id: "media_player.speaker",
state: "idle",
} as MediaPlayerEntity,
},
} as any;
const result = computeActiveAreaMediaStates(hass, "living_room");
expect(result).toHaveLength(1);
expect(result[0].entity_id).toBe("media_player.tv");
expect(result[0].state).toBe("playing");
});
it("returns empty array when no area is configured", () => {
const hass = {
areas: {},
entities: {},
states: {},
} as any;
const result = computeActiveAreaMediaStates(hass, "living_room");
expect(result).toHaveLength(0);
});
it("returns empty array when media player is not assigned to area", () => {
const hass = {
areas: { living_room: { area_id: "living_room" } },
entities: {
"media_player.bedroom": { entity_id: "media_player.bedroom" },
},
states: {
"media_player.bedroom": {
entity_id: "media_player.bedroom",
state: "playing",
} as MediaPlayerEntity,
},
} as any;
const result = computeActiveAreaMediaStates(hass, "living_room");
expect(result).toHaveLength(0);
});
it("returns playing speaker when speaker is playing", () => {
const hass = {
areas: { living_room: { area_id: "living_room" } },
entities: {
"media_player.tv": {
entity_id: "media_player.tv",
area_id: "living_room",
},
"media_player.speaker": {
entity_id: "media_player.speaker",
area_id: "living_room",
},
},
states: {
"media_player.tv": {
entity_id: "media_player.tv",
state: "idle",
} as MediaPlayerEntity,
"media_player.speaker": {
entity_id: "media_player.speaker",
state: "playing",
} as MediaPlayerEntity,
},
} as any;
const result = computeActiveAreaMediaStates(hass, "living_room");
expect(result).toHaveLength(1);
expect(result[0].entity_id).toBe("media_player.speaker");
expect(result[0].state).toBe("playing");
});
it("returns media entities that inherit area from device", () => {
const hass = {
areas: { living_room: { area_id: "living_room" } },
devices: {
device_tv: {
id: "device_tv",
area_id: "living_room",
},
},
entities: {
"media_player.tv": {
entity_id: "media_player.tv",
device_id: "device_tv", // Entity belongs to device
// No direct area_id - inherits from device
},
},
states: {
"media_player.tv": {
entity_id: "media_player.tv",
state: "playing",
} as MediaPlayerEntity,
},
} as any;
const result = computeActiveAreaMediaStates(hass, "living_room");
expect(result).toHaveLength(1);
expect(result[0].entity_id).toBe("media_player.tv");
expect(result[0].state).toBe("playing");
});
});
describe("computeActiveAreaMediaStates badge priority", () => {
it("prioritizes alert badge over media badge", () => {
const hass = {
areas: { living_room: { area_id: "living_room" } },
entities: {
"binary_sensor.door": {
entity_id: "binary_sensor.door",
area_id: "living_room",
},
"media_player.tv": {
entity_id: "media_player.tv",
area_id: "living_room",
},
},
states: {
"binary_sensor.door": {
entity_id: "binary_sensor.door",
state: "on",
} as MediaPlayerEntity,
"media_player.tv": {
entity_id: "media_player.tv",
state: "playing",
} as MediaPlayerEntity,
},
} as any;
const alertStates = hass.states["binary_sensor.door"]
? [hass.states["binary_sensor.door"]]
: [];
const mediaStates = computeActiveAreaMediaStates(hass, "living_room");
// Alert badge should take priority
expect(alertStates.length > 0).toBe(true);
expect(mediaStates.length > 0).toBe(true);
expect(alertStates.length > 0 ? "alert" : "media").toBe("alert");
});
it("shows media badge when no alerts", () => {
const hass = {
areas: { living_room: { area_id: "living_room" } },
entities: {
"media_player.tv": {
entity_id: "media_player.tv",
area_id: "living_room",
},
},
states: {
"media_player.tv": {
entity_id: "media_player.tv",
state: "playing",
} as MediaPlayerEntity,
},
} as any;
const alertStates: MediaPlayerEntity[] = [];
const mediaStates = computeActiveAreaMediaStates(hass, "living_room");
expect(alertStates.length).toBe(0);
expect(mediaStates.length).toBe(1);
expect(alertStates.length > 0 ? "alert" : "media").toBe("media");
});
});

View File

@@ -1,408 +0,0 @@
import { describe, expect, it } from "vitest";
import {
computeEntityNameDisplay,
computeEntityNameList,
} from "../../../src/common/entity/compute_entity_name_display";
import type { HomeAssistant } from "../../../src/types";
import {
mockArea,
mockDevice,
mockEntity,
mockFloor,
mockStateObj,
} from "./context/context-mock";
describe("computeEntityNameDisplay", () => {
it("returns text when all items are text", () => {
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
const hass = {
entities: {},
devices: {},
areas: {},
floors: {},
} as unknown as HomeAssistant;
const result = computeEntityNameDisplay(
stateObj,
[
{ type: "text", text: "Hello" },
{ type: "text", text: "World" },
],
hass.entities,
hass.devices,
hass.areas,
hass.floors
);
expect(result).toBe("Hello World");
});
it("uses custom separator for text items", () => {
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
const hass = {
entities: {},
devices: {},
areas: {},
floors: {},
} as unknown as HomeAssistant;
const result = computeEntityNameDisplay(
stateObj,
[
{ type: "text", text: "Hello" },
{ type: "text", text: "World" },
],
hass.entities,
hass.devices,
hass.areas,
hass.floors,
{ separator: " - " }
);
expect(result).toBe("Hello - World");
});
it("returns entity name", () => {
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
const hass = {
entities: {
"light.kitchen": mockEntity({
entity_id: "light.kitchen",
name: "Kitchen Light",
}),
},
devices: {},
areas: {},
floors: {},
} as unknown as HomeAssistant;
const result = computeEntityNameDisplay(
stateObj,
{ type: "entity" },
hass.entities,
hass.devices,
hass.areas,
hass.floors
);
expect(result).toBe("Kitchen Light");
});
it("replaces entity with device name when entity uses device name", () => {
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
const hass = {
entities: {
"light.kitchen": mockEntity({
entity_id: "light.kitchen",
name: "Kitchen Device",
device_id: "dev1",
}),
},
devices: {
dev1: mockDevice({
id: "dev1",
name: "Kitchen Device",
}),
},
areas: {},
floors: {},
} as unknown as HomeAssistant;
const result = computeEntityNameDisplay(
stateObj,
{ type: "entity" },
hass.entities,
hass.devices,
hass.areas,
hass.floors
);
expect(result).toBe("Kitchen Device");
});
it("does not replace entity with device when device is already included", () => {
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
const hass = {
entities: {
"light.kitchen": mockEntity({
entity_id: "light.kitchen",
name: "Kitchen Device",
device_id: "dev1",
}),
},
devices: {
dev1: mockDevice({
id: "dev1",
name: "Kitchen Device",
}),
},
areas: {},
floors: {},
} as unknown as HomeAssistant;
const result = computeEntityNameDisplay(
stateObj,
[{ type: "entity" }, { type: "device" }],
hass.entities,
hass.devices,
hass.areas,
hass.floors
);
// Since entity name equals device name, entity returns undefined
// So we only get the device name
expect(result).toBe("Kitchen Device");
});
it("returns combined entity and area names", () => {
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
const hass = {
entities: {
"light.kitchen": mockEntity({
entity_id: "light.kitchen",
name: "Ceiling Light",
area_id: "kitchen",
}),
},
devices: {},
areas: {
kitchen: mockArea({
area_id: "kitchen",
name: "Kitchen",
}),
},
floors: {},
} as unknown as HomeAssistant;
const result = computeEntityNameDisplay(
stateObj,
[{ type: "area" }, { type: "entity" }],
hass.entities,
hass.devices,
hass.areas,
hass.floors
);
expect(result).toBe("Kitchen Ceiling Light");
});
it("returns combined device and area names", () => {
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
const hass = {
entities: {
"light.kitchen": mockEntity({
entity_id: "light.kitchen",
name: "Light",
device_id: "dev1",
}),
},
devices: {
dev1: mockDevice({
id: "dev1",
name: "Smart Light",
area_id: "kitchen",
}),
},
areas: {
kitchen: mockArea({
area_id: "kitchen",
name: "Kitchen",
}),
},
floors: {},
} as unknown as HomeAssistant;
const result = computeEntityNameDisplay(
stateObj,
[{ type: "area" }, { type: "device" }],
hass.entities,
hass.devices,
hass.areas,
hass.floors
);
expect(result).toBe("Kitchen Smart Light");
});
it("returns floor name", () => {
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
const hass = {
entities: {
"light.kitchen": mockEntity({
entity_id: "light.kitchen",
name: "Light",
area_id: "kitchen",
}),
},
devices: {},
areas: {
kitchen: mockArea({
area_id: "kitchen",
name: "Kitchen",
floor_id: "first",
}),
},
floors: {
first: mockFloor({
floor_id: "first",
name: "First Floor",
}),
},
} as unknown as HomeAssistant;
const result = computeEntityNameDisplay(
stateObj,
{ type: "floor" },
hass.entities,
hass.devices,
hass.areas,
hass.floors
);
expect(result).toBe("First Floor");
});
it("filters out undefined names when combining", () => {
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
const hass = {
entities: {
"light.kitchen": mockEntity({
entity_id: "light.kitchen",
name: "Light",
}),
},
devices: {},
areas: {},
floors: {},
} as unknown as HomeAssistant;
const result = computeEntityNameDisplay(
stateObj,
[{ type: "area" }, { type: "entity" }, { type: "floor" }],
hass.entities,
hass.devices,
hass.areas,
hass.floors
);
// Area and floor don't exist, so only entity name is included
expect(result).toBe("Light");
});
it("mixes text with entity types", () => {
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
const hass = {
entities: {
"light.kitchen": mockEntity({
entity_id: "light.kitchen",
name: "Light",
area_id: "kitchen",
}),
},
devices: {},
areas: {
kitchen: mockArea({
area_id: "kitchen",
name: "Kitchen",
}),
},
floors: {},
} as unknown as HomeAssistant;
const result = computeEntityNameDisplay(
stateObj,
[{ type: "area" }, { type: "text", text: "-" }, { type: "entity" }],
hass.entities,
hass.devices,
hass.areas,
hass.floors
);
expect(result).toBe("Kitchen - Light");
});
});
describe("computeEntityNameList", () => {
it("returns list of names for each item type", () => {
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
const hass = {
entities: {
"light.kitchen": mockEntity({
entity_id: "light.kitchen",
name: "Light",
device_id: "dev1",
area_id: "kitchen",
}),
},
devices: {
dev1: mockDevice({
id: "dev1",
name: "Smart Device",
area_id: "kitchen",
}),
},
areas: {
kitchen: mockArea({
area_id: "kitchen",
name: "Kitchen",
floor_id: "first",
}),
},
floors: {
first: mockFloor({
floor_id: "first",
name: "First Floor",
}),
},
} as unknown as HomeAssistant;
const result = computeEntityNameList(
stateObj,
[
{ type: "floor" },
{ type: "area" },
{ type: "device" },
{ type: "entity" },
{ type: "text", text: "Custom" },
],
hass.entities,
hass.devices,
hass.areas,
hass.floors
);
expect(result).toEqual([
"First Floor",
"Kitchen",
"Smart Device",
"Light",
"Custom",
]);
});
it("returns undefined for missing context items", () => {
const stateObj = mockStateObj({ entity_id: "light.kitchen" });
const hass = {
entities: {
"light.kitchen": mockEntity({
entity_id: "light.kitchen",
name: "Light",
}),
},
devices: {},
areas: {},
floors: {},
} as unknown as HomeAssistant;
const result = computeEntityNameList(
stateObj,
[{ type: "device" }, { type: "area" }, { type: "floor" }],
hass.entities,
hass.devices,
hass.areas,
hass.floors
);
expect(result).toEqual([undefined, undefined, undefined]);
});
});

456
yarn.lock
View File

@@ -1206,14 +1206,14 @@ __metadata:
languageName: node
linkType: hard
"@bundle-stats/plugin-webpack-filter@npm:4.21.5":
version: 4.21.5
resolution: "@bundle-stats/plugin-webpack-filter@npm:4.21.5"
"@bundle-stats/plugin-webpack-filter@npm:4.21.4":
version: 4.21.4
resolution: "@bundle-stats/plugin-webpack-filter@npm:4.21.4"
dependencies:
tslib: "npm:2.8.1"
peerDependencies:
core-js: ^3.0.0
checksum: 10/a24e4fb0efdd671e8d21116045a22500494198feb451981d40db732cd890b90b7bad9a9f1d7909c8db9a1069f4c6226ae35939b4d865be437ea8e19b989ebcf1
checksum: 10/67e589755ce6702c92db3e9ca6158d0cbdce5a0c1d92508fae61c0424720121e81c6884efd86392f4906db871e4ba6f97111a55976571cdb79720a47149c027b
languageName: node
linkType: hard
@@ -1284,15 +1284,15 @@ __metadata:
languageName: node
linkType: hard
"@codemirror/view@npm:6.38.5, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0":
version: 6.38.5
resolution: "@codemirror/view@npm:6.38.5"
"@codemirror/view@npm:6.38.4, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0":
version: 6.38.4
resolution: "@codemirror/view@npm:6.38.4"
dependencies:
"@codemirror/state": "npm:^6.5.0"
crelt: "npm:^1.0.6"
style-mod: "npm:^4.1.0"
w3c-keyname: "npm:^2.2.4"
checksum: 10/2335b593770042eb3adfe369073432b07cd2d15f1e230ae4dc7be7a7b8edd74e57c13e59b92a11e7e5d59ae030aabf7f55478dfec1cf2a2fe3a1ef3f091676a4
checksum: 10/86b3894e9e7c2113aabb1db8684d0520378339c194fa56a688fc26cd7d40336bb9df1f5f19f68309d95f14b80ecf0b70c0ffe5e43f2ec11c4bab18f2d5ee4494
languageName: node
linkType: hard
@@ -1613,21 +1613,19 @@ __metadata:
languageName: node
linkType: hard
"@eslint/config-helpers@npm:^0.4.0":
version: 0.4.0
resolution: "@eslint/config-helpers@npm:0.4.0"
dependencies:
"@eslint/core": "npm:^0.16.0"
checksum: 10/d5fdbf927a77b98d2462f025f8b1a5b610609201f8d1dd47032a2937842f02bf3bdf9cb672025c83a00f3255dfd218172f989caa724853c4a8f434124a6d79ff
"@eslint/config-helpers@npm:^0.3.1":
version: 0.3.1
resolution: "@eslint/config-helpers@npm:0.3.1"
checksum: 10/fc1a90ef6180aa4b5187cee04cfc566abb2a32b77ca3e7eeb4312c7388f6898221adaf8451d9ddb22e0b8860d900fefb1eb1435e4f32f8d8732de87f14605f8f
languageName: node
linkType: hard
"@eslint/core@npm:^0.16.0":
version: 0.16.0
resolution: "@eslint/core@npm:0.16.0"
"@eslint/core@npm:^0.15.2":
version: 0.15.2
resolution: "@eslint/core@npm:0.15.2"
dependencies:
"@types/json-schema": "npm:^7.0.15"
checksum: 10/3cea45971b2d0114267b6101b673270b5d8047448cc7a8cbfdca0b0245e9d5e081cb25f13551dc7d55a090f98c13b33f0c4999f8ee8ab058537e6037629a0f71
checksum: 10/41d6273bbc6897cca34a2ca4e80a24bf6f1d43519456ebaa3c38f187da2d9e06f442c64f6e2a2813f055dce35e5cea33a21d0ac3b5b0830b7165641c640faf5d
languageName: node
linkType: hard
@@ -1648,10 +1646,10 @@ __metadata:
languageName: node
linkType: hard
"@eslint/js@npm:9.37.0":
version: 9.37.0
resolution: "@eslint/js@npm:9.37.0"
checksum: 10/2ead426ed47af0b914c7d7064eb59fede858483cf9511f78ded840708aca578138f2a6c375916d520f4f2ecf25945f4bd47b8a84e42106b4eb46f7708a36db1d
"@eslint/js@npm:9.36.0":
version: 9.36.0
resolution: "@eslint/js@npm:9.36.0"
checksum: 10/a0542f529f87b9ad69ef85c47b0c070b763591a61773b131a9d1d53934a587f0708c05a1a8f48a6805486004a4922c91d696c1e4835ff61f8750ffbded2f0c30
languageName: node
linkType: hard
@@ -1662,13 +1660,13 @@ __metadata:
languageName: node
linkType: hard
"@eslint/plugin-kit@npm:^0.4.0":
version: 0.4.0
resolution: "@eslint/plugin-kit@npm:0.4.0"
"@eslint/plugin-kit@npm:^0.3.5":
version: 0.3.5
resolution: "@eslint/plugin-kit@npm:0.3.5"
dependencies:
"@eslint/core": "npm:^0.16.0"
"@eslint/core": "npm:^0.15.2"
levn: "npm:^0.4.1"
checksum: 10/2c37ca00e352447215aeadcaff5765faead39695f1cb91cd3079a43261b234887caf38edc462811bb3401acf8c156c04882f87740df936838290c705351483be
checksum: 10/b8552d79c3091446b07d8b87a9a8ccb8cdee4d933c0ed46b8f61029c3382246fec8d04ea7d1e61656d9275263205ccaa40019fd7581bbce897eca3eda42d5dad
languageName: node
linkType: hard
@@ -1698,15 +1696,15 @@ __metadata:
languageName: node
linkType: hard
"@formatjs/ecma402-abstract@npm:2.3.6":
version: 2.3.6
resolution: "@formatjs/ecma402-abstract@npm:2.3.6"
"@formatjs/ecma402-abstract@npm:2.3.4":
version: 2.3.4
resolution: "@formatjs/ecma402-abstract@npm:2.3.4"
dependencies:
"@formatjs/fast-memoize": "npm:2.2.7"
"@formatjs/intl-localematcher": "npm:0.6.2"
"@formatjs/intl-localematcher": "npm:0.6.1"
decimal.js: "npm:^10.4.3"
tslib: "npm:^2.8.0"
checksum: 10/30b1b5cd6b62ba46245f934429936592df5500bc1b089dc92dd49c826757b873dd92c305dcfe370701e4df6b057bf007782113abb9b65db550d73be4961718bc
checksum: 10/573971ffc291096a4b9fcc80b4708124e89bf2e3ac50e0f78b41eb797e9aa1b842f4dc3665e4467a853c738386821769d9e40408a1d25bc73323a1f057a16cf2
languageName: node
linkType: hard
@@ -1719,144 +1717,144 @@ __metadata:
languageName: node
linkType: hard
"@formatjs/icu-messageformat-parser@npm:2.11.4":
version: 2.11.4
resolution: "@formatjs/icu-messageformat-parser@npm:2.11.4"
"@formatjs/icu-messageformat-parser@npm:2.11.2":
version: 2.11.2
resolution: "@formatjs/icu-messageformat-parser@npm:2.11.2"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.6"
"@formatjs/icu-skeleton-parser": "npm:1.8.16"
"@formatjs/ecma402-abstract": "npm:2.3.4"
"@formatjs/icu-skeleton-parser": "npm:1.8.14"
tslib: "npm:^2.8.0"
checksum: 10/2acb100c06c2ade666d72787fb9f9795b1ace41e8e73bfadc2b1a7b8562e81f655e484f0f33d8c39473aa17bf0ad96fb2228871806a9b3dc4f5f876754a0de3a
checksum: 10/e919eb2a132ac1d54fb1a7e3a3254007649b55196d3818090df92a4268dcddf20cbdf863c06039fbbe7a35a8a3f17bdc172dade99d1f17c1d8a95dcec444c3e3
languageName: node
linkType: hard
"@formatjs/icu-skeleton-parser@npm:1.8.16":
version: 1.8.16
resolution: "@formatjs/icu-skeleton-parser@npm:1.8.16"
"@formatjs/icu-skeleton-parser@npm:1.8.14":
version: 1.8.14
resolution: "@formatjs/icu-skeleton-parser@npm:1.8.14"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.6"
"@formatjs/ecma402-abstract": "npm:2.3.4"
tslib: "npm:^2.8.0"
checksum: 10/428001e5bed81889b276a2356a1393157af91dc59220b765a1a132f6407ac5832b7ac6ae9737674ac38e44035295c0c1c310b2630f383f2b5779ea90bf2849e6
checksum: 10/2fbe3155c310358820b118d8c9844f314eff3500a82f1c65402434a3095823e1afeaab8d1762b4a59cc5679d82dc4c8c134683565d7cdae4daace23251f46a47
languageName: node
linkType: hard
"@formatjs/intl-datetimeformat@npm:6.18.2":
version: 6.18.2
resolution: "@formatjs/intl-datetimeformat@npm:6.18.2"
"@formatjs/intl-datetimeformat@npm:6.18.0":
version: 6.18.0
resolution: "@formatjs/intl-datetimeformat@npm:6.18.0"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.6"
"@formatjs/intl-localematcher": "npm:0.6.2"
"@formatjs/ecma402-abstract": "npm:2.3.4"
"@formatjs/intl-localematcher": "npm:0.6.1"
decimal.js: "npm:^10.4.3"
tslib: "npm:^2.8.0"
checksum: 10/e6f80d0eb2049564502370839697a18858268a0dff8d199b1908137c4a229b1303131c12b8b8a8e8e259a1feba26dbc25b003b150adabea10d1c43f68086efbe
checksum: 10/b70edaa4cfa150f0a6cbeeb1488e6acdea21349abdefc4e37b923de68592c6f330a966456bf6000f233d0f715cf3b8cfce23d5a4ed574fa8ea35ccb5bea80886
languageName: node
linkType: hard
"@formatjs/intl-displaynames@npm:6.8.13":
version: 6.8.13
resolution: "@formatjs/intl-displaynames@npm:6.8.13"
"@formatjs/intl-displaynames@npm:6.8.11":
version: 6.8.11
resolution: "@formatjs/intl-displaynames@npm:6.8.11"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.6"
"@formatjs/intl-localematcher": "npm:0.6.2"
"@formatjs/ecma402-abstract": "npm:2.3.4"
"@formatjs/intl-localematcher": "npm:0.6.1"
tslib: "npm:^2.8.0"
checksum: 10/adefd25fa42266c7bc33dd3cd50f3681bdce51d18b32a03c98f8ad7587dfd8b9291345e185a4b16f31f4eee10fc799fd1b6361bdfd3a2c9fe127744e1e0f3b07
checksum: 10/05c785d9e767cc1e4d1bd40d6989c3318b6a98cb43dd6808f501f5e5538bb3a1fb8fa80f8d2282d598501d3d193a406f0127acce6b14cb7c595ab6d981437e6f
languageName: node
linkType: hard
"@formatjs/intl-durationformat@npm:0.7.6":
version: 0.7.6
resolution: "@formatjs/intl-durationformat@npm:0.7.6"
"@formatjs/intl-durationformat@npm:0.7.4":
version: 0.7.4
resolution: "@formatjs/intl-durationformat@npm:0.7.4"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.6"
"@formatjs/intl-localematcher": "npm:0.6.2"
"@formatjs/ecma402-abstract": "npm:2.3.4"
"@formatjs/intl-localematcher": "npm:0.6.1"
tslib: "npm:^2.8.0"
checksum: 10/442236ba85bcd9cb7296c43a708271fa09f110b1ca9d5899066d00812fc2965eaeaec6b5240be421b80daba62860352131088449ba0fcd2061f671cec6240f0b
checksum: 10/d62273ecd635475ca91e9b501301f3f396403fa91b584c550734b19b2d194ba1316b27303fed985c1d42ae933d54eb220da6540edfdf376b0d9371ecfd0d4e15
languageName: node
linkType: hard
"@formatjs/intl-enumerator@npm:1.8.12":
version: 1.8.12
resolution: "@formatjs/intl-enumerator@npm:1.8.12"
"@formatjs/intl-enumerator@npm:1.8.10":
version: 1.8.10
resolution: "@formatjs/intl-enumerator@npm:1.8.10"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.6"
"@formatjs/ecma402-abstract": "npm:2.3.4"
tslib: "npm:^2.8.0"
checksum: 10/8dfd7ca5383b4dca530e1df5118a72f71347f4e0daa6131b82dbf7e860a8b96bec0fed43bfa6f6e650e55fa50fcd3e9e3a5253515131b578539d8eaa84630927
checksum: 10/9e0e762143248bf91e174d3abc15261b47ac7294632d26797cf5b001707aa68ca2deeb05c95f7308aa2cffa46d61b0fac46306dea722ab210dfa012990743798
languageName: node
linkType: hard
"@formatjs/intl-getcanonicallocales@npm:2.5.6":
version: 2.5.6
resolution: "@formatjs/intl-getcanonicallocales@npm:2.5.6"
"@formatjs/intl-getcanonicallocales@npm:2.5.5":
version: 2.5.5
resolution: "@formatjs/intl-getcanonicallocales@npm:2.5.5"
dependencies:
tslib: "npm:^2.8.0"
checksum: 10/1d3d13fa1758a9bb7854f3afd844ecb70a4333a7cfbb6822b99e3b8ab6269e525a0ca23a8a47c3944e5376bc19e9e423b5cc3043db1c6de64909986c5cec6fc0
checksum: 10/2a32202765c9a4f16fc36f4e4afca7fd5f4f35885ad2ca671352a7bba1a19d5ec81933d52ab1855c8570e73247213739d9d2d95d2438bd9f02a1f0db7cb9b8a9
languageName: node
linkType: hard
"@formatjs/intl-listformat@npm:7.7.13":
version: 7.7.13
resolution: "@formatjs/intl-listformat@npm:7.7.13"
"@formatjs/intl-listformat@npm:7.7.11":
version: 7.7.11
resolution: "@formatjs/intl-listformat@npm:7.7.11"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.6"
"@formatjs/intl-localematcher": "npm:0.6.2"
"@formatjs/ecma402-abstract": "npm:2.3.4"
"@formatjs/intl-localematcher": "npm:0.6.1"
tslib: "npm:^2.8.0"
checksum: 10/476d7cffb64eb996a888b1865aa237f04088de60fa7c65b6d073bca8a3c0f4304040ef12f16eafaf6587895976b773607296951afa7f119447d8f9b2c40daa55
checksum: 10/e7de54dcbcfdd8718870501623fb1be55dbac11e2582b7961d4668fb5e1f0d1f6da0388ed49084a4527e500dbea548670659efccb690f3b4398f0f8bcd5221dd
languageName: node
linkType: hard
"@formatjs/intl-locale@npm:4.2.13":
version: 4.2.13
resolution: "@formatjs/intl-locale@npm:4.2.13"
"@formatjs/intl-locale@npm:4.2.11":
version: 4.2.11
resolution: "@formatjs/intl-locale@npm:4.2.11"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.6"
"@formatjs/intl-enumerator": "npm:1.8.12"
"@formatjs/intl-getcanonicallocales": "npm:2.5.6"
"@formatjs/ecma402-abstract": "npm:2.3.4"
"@formatjs/intl-enumerator": "npm:1.8.10"
"@formatjs/intl-getcanonicallocales": "npm:2.5.5"
tslib: "npm:^2.8.0"
checksum: 10/865615561b4bad8b8d7d93539cae7eb3ed2d46b6156486ef3ccb1b8f9f46f075c7cf2f6e5325aba1cf07150e19280858dff7dfd86d530fbf45fd31ea4fabf8d4
checksum: 10/8746af66ebd5284f189c83e0d59a4d781490ce3eadaab284bf96c4240eaf8b9422130a94a842a1ab12fa14bb2cdf02e9f78ac3b9cf955156fafeffab9d73d7a2
languageName: node
linkType: hard
"@formatjs/intl-localematcher@npm:0.6.2":
version: 0.6.2
resolution: "@formatjs/intl-localematcher@npm:0.6.2"
"@formatjs/intl-localematcher@npm:0.6.1":
version: 0.6.1
resolution: "@formatjs/intl-localematcher@npm:0.6.1"
dependencies:
tslib: "npm:^2.8.0"
checksum: 10/eb12a7f5367bbecdfafc20d7f005559ce840f420e970f425c5213d35e94e86dfe75bde03464971a26494bf8427d4961269db22ecad2834f2a19d888b5d9cc064
checksum: 10/c7b3bc8395d18670677f207b2fd107561fff5d6394a9b4273c29e0bea920300ec3a2eefead600ebb7761c04a770cada28f78ac059f84d00520bfb57a9db36998
languageName: node
linkType: hard
"@formatjs/intl-numberformat@npm:8.15.6":
version: 8.15.6
resolution: "@formatjs/intl-numberformat@npm:8.15.6"
"@formatjs/intl-numberformat@npm:8.15.4":
version: 8.15.4
resolution: "@formatjs/intl-numberformat@npm:8.15.4"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.6"
"@formatjs/intl-localematcher": "npm:0.6.2"
"@formatjs/ecma402-abstract": "npm:2.3.4"
"@formatjs/intl-localematcher": "npm:0.6.1"
decimal.js: "npm:^10.4.3"
tslib: "npm:^2.8.0"
checksum: 10/674c5fefa0b14fcd7c58d0c0e592b4887dc2563fa5a11d80a0a82328ac12b2bb82b9a5367fa0a4d80060d61d15a1821bca7085e20cad09aa93b87edb3cff68ea
checksum: 10/232740eb4992f1bf4f829f05a755f427089a70b56a8a715fa9ac8604f701691701e989247ef1537a1d7c90e315b4153b82cf2e67e7f9d5b78d471c1cf59abace
languageName: node
linkType: hard
"@formatjs/intl-pluralrules@npm:5.4.6":
version: 5.4.6
resolution: "@formatjs/intl-pluralrules@npm:5.4.6"
"@formatjs/intl-pluralrules@npm:5.4.4":
version: 5.4.4
resolution: "@formatjs/intl-pluralrules@npm:5.4.4"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.6"
"@formatjs/intl-localematcher": "npm:0.6.2"
"@formatjs/ecma402-abstract": "npm:2.3.4"
"@formatjs/intl-localematcher": "npm:0.6.1"
decimal.js: "npm:^10.4.3"
tslib: "npm:^2.8.0"
checksum: 10/88aa244e69ccfdf459899f5fa3c64df345f451ef91ce1188eab35b7e37daa225d22120f64be633f2cd8b826ea705d19831915118f555f2d17611ee842a9a86dc
checksum: 10/919f80e144283b5849014bc245626916224adc0d693e8be5531168f1c7af54bb4c8cbd77a12ceba1d13ad49171680d346d9176464fae5013e13f79d9c7baa02a
languageName: node
linkType: hard
"@formatjs/intl-relativetimeformat@npm:11.4.13":
version: 11.4.13
resolution: "@formatjs/intl-relativetimeformat@npm:11.4.13"
"@formatjs/intl-relativetimeformat@npm:11.4.11":
version: 11.4.11
resolution: "@formatjs/intl-relativetimeformat@npm:11.4.11"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.6"
"@formatjs/intl-localematcher": "npm:0.6.2"
"@formatjs/ecma402-abstract": "npm:2.3.4"
"@formatjs/intl-localematcher": "npm:0.6.1"
tslib: "npm:^2.8.0"
checksum: 10/c2058d5f29a13aa216d317d309a6ffd7d203f0fe11696b7bd524e17ac3cc22ae50ad56a26dbf18125e4c115a3e75f01e6cf2134a83df6c7916ae6d3fb21a1e9b
checksum: 10/fda4da27c0245869316c9199ed4e0521988be8b41b3e685f4abcb486f01d5b4c72f2ecf1b19b07091c15360c7691a4dd87199f81943d1ad6bda084c746fc8ec3
languageName: node
linkType: hard
@@ -1942,9 +1940,9 @@ __metadata:
languageName: node
linkType: hard
"@home-assistant/webawesome@npm:3.0.0-beta.6.ha.4":
version: 3.0.0-beta.6.ha.4
resolution: "@home-assistant/webawesome@npm:3.0.0-beta.6.ha.4"
"@home-assistant/webawesome@npm:3.0.0-beta.6.ha.1":
version: 3.0.0-beta.6.ha.1
resolution: "@home-assistant/webawesome@npm:3.0.0-beta.6.ha.1"
dependencies:
"@ctrl/tinycolor": "npm:4.1.0"
"@floating-ui/dom": "npm:^1.6.13"
@@ -1955,7 +1953,7 @@ __metadata:
lit: "npm:^3.2.1"
nanoid: "npm:^5.1.5"
qr-creator: "npm:^1.0.0"
checksum: 10/d9072b321126ef458468ed2cf040e0b04cb2aff73336c6e742c0cfb25d9fb674b7672e7c9abcf5bcb0aa0b2fe953c20186f0910f485024c827bfe4cf399f10a4
checksum: 10/c9510e0c65b682c3868b5cbbf046f62aea30e3c5d969128d9032e0d89a8943faa4c9d78c3500446ec04cffeb0ab1939b870b60d454db657faed2aa0ac6026a3e
languageName: node
linkType: hard
@@ -4945,106 +4943,106 @@ __metadata:
languageName: node
linkType: hard
"@typescript-eslint/eslint-plugin@npm:8.46.0":
version: 8.46.0
resolution: "@typescript-eslint/eslint-plugin@npm:8.46.0"
"@typescript-eslint/eslint-plugin@npm:8.45.0":
version: 8.45.0
resolution: "@typescript-eslint/eslint-plugin@npm:8.45.0"
dependencies:
"@eslint-community/regexpp": "npm:^4.10.0"
"@typescript-eslint/scope-manager": "npm:8.46.0"
"@typescript-eslint/type-utils": "npm:8.46.0"
"@typescript-eslint/utils": "npm:8.46.0"
"@typescript-eslint/visitor-keys": "npm:8.46.0"
"@typescript-eslint/scope-manager": "npm:8.45.0"
"@typescript-eslint/type-utils": "npm:8.45.0"
"@typescript-eslint/utils": "npm:8.45.0"
"@typescript-eslint/visitor-keys": "npm:8.45.0"
graphemer: "npm:^1.4.0"
ignore: "npm:^7.0.0"
natural-compare: "npm:^1.4.0"
ts-api-utils: "npm:^2.1.0"
peerDependencies:
"@typescript-eslint/parser": ^8.46.0
"@typescript-eslint/parser": ^8.45.0
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/415afd894a5fec9cfe2c327c8b26377045979cc6bdf720aeecb32af335b9e6865c70fa6a355dd16f52a36dc38f50755df3eb1466d5822c53c80465ff824c9881
checksum: 10/6d31dbd3354028b4a010af0ea2614a171b11616e6f20d36d74529b8888681ae8d15e1269122b8a8d5fae117bdd66dac4a38cfc99dc2a0ee33bd22c10075f63e4
languageName: node
linkType: hard
"@typescript-eslint/parser@npm:8.46.0":
version: 8.46.0
resolution: "@typescript-eslint/parser@npm:8.46.0"
"@typescript-eslint/parser@npm:8.45.0":
version: 8.45.0
resolution: "@typescript-eslint/parser@npm:8.45.0"
dependencies:
"@typescript-eslint/scope-manager": "npm:8.46.0"
"@typescript-eslint/types": "npm:8.46.0"
"@typescript-eslint/typescript-estree": "npm:8.46.0"
"@typescript-eslint/visitor-keys": "npm:8.46.0"
"@typescript-eslint/scope-manager": "npm:8.45.0"
"@typescript-eslint/types": "npm:8.45.0"
"@typescript-eslint/typescript-estree": "npm:8.45.0"
"@typescript-eslint/visitor-keys": "npm:8.45.0"
debug: "npm:^4.3.4"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/6838fde776fd2b2932b259a20cc89b517e0c94a2cfa363a5e8531095c23fb35d8f803196f6594026d0510bf2a8ec003c67181bb2c407904685a64c97602da65f
checksum: 10/4f8b7c73ae3b53c2adc4e981ac2ca90839a118947635481b45d29423d39b7b73cde2b185ad1084c9e19c3239444bf1be81f40b861176eec4540cb46848731991
languageName: node
linkType: hard
"@typescript-eslint/project-service@npm:8.46.0":
version: 8.46.0
resolution: "@typescript-eslint/project-service@npm:8.46.0"
"@typescript-eslint/project-service@npm:8.45.0":
version: 8.45.0
resolution: "@typescript-eslint/project-service@npm:8.45.0"
dependencies:
"@typescript-eslint/tsconfig-utils": "npm:^8.46.0"
"@typescript-eslint/types": "npm:^8.46.0"
"@typescript-eslint/tsconfig-utils": "npm:^8.45.0"
"@typescript-eslint/types": "npm:^8.45.0"
debug: "npm:^4.3.4"
peerDependencies:
typescript: ">=4.8.4 <6.0.0"
checksum: 10/de11af23ae6b82769b667e8d6e81d47ce039c7817465b99c1e29c8fbcac58af898bebe70368a274cd7b3c7232354134d53ceba0415b8d7e18317037bc4a4a2f7
checksum: 10/919c8260dae79eaec79de84a5ae66fbb09c2ab7aca8c3b7785cb011582a2864c8091e64c84013b05bce812e522fbc4a5ae1c68f86404e078fc84da0fe80247ce
languageName: node
linkType: hard
"@typescript-eslint/scope-manager@npm:8.46.0":
version: 8.46.0
resolution: "@typescript-eslint/scope-manager@npm:8.46.0"
"@typescript-eslint/scope-manager@npm:8.45.0":
version: 8.45.0
resolution: "@typescript-eslint/scope-manager@npm:8.45.0"
dependencies:
"@typescript-eslint/types": "npm:8.46.0"
"@typescript-eslint/visitor-keys": "npm:8.46.0"
checksum: 10/ed85abd08c0edf088b1b11757c658acf593cf84051bddde651304a609d3a6cd9e331149e88653676606a565c3f92c191d4af049f540f6e3bb692a4f38305fd71
"@typescript-eslint/types": "npm:8.45.0"
"@typescript-eslint/visitor-keys": "npm:8.45.0"
checksum: 10/e45d63a0109eca00f6b431d87e73eacfa03b1795905f123e9144bcacb5abb83888167d1849317c6f90ba1f3553196b2eab13e5e7cdd1050d7a84eaadb65ba801
languageName: node
linkType: hard
"@typescript-eslint/tsconfig-utils@npm:8.46.0, @typescript-eslint/tsconfig-utils@npm:^8.46.0":
version: 8.46.0
resolution: "@typescript-eslint/tsconfig-utils@npm:8.46.0"
"@typescript-eslint/tsconfig-utils@npm:8.45.0, @typescript-eslint/tsconfig-utils@npm:^8.45.0":
version: 8.45.0
resolution: "@typescript-eslint/tsconfig-utils@npm:8.45.0"
peerDependencies:
typescript: ">=4.8.4 <6.0.0"
checksum: 10/e78a66a854322423aca835070c5ee9489975c4d80d2f8ffe9cf4d6e3f67a1646ddc05b086f7156599c90ad521670ca572a4315f2b49a5922c33d6e49723558e4
checksum: 10/91696bbc34758749d3647236986bf418bacdc0de0e27c2d39cd7c2408c404c35ed18c47c2a55aea0bb9525cc7eb656586359c4e651144603f3438ce93fe80081
languageName: node
linkType: hard
"@typescript-eslint/type-utils@npm:8.46.0":
version: 8.46.0
resolution: "@typescript-eslint/type-utils@npm:8.46.0"
"@typescript-eslint/type-utils@npm:8.45.0":
version: 8.45.0
resolution: "@typescript-eslint/type-utils@npm:8.45.0"
dependencies:
"@typescript-eslint/types": "npm:8.46.0"
"@typescript-eslint/typescript-estree": "npm:8.46.0"
"@typescript-eslint/utils": "npm:8.46.0"
"@typescript-eslint/types": "npm:8.45.0"
"@typescript-eslint/typescript-estree": "npm:8.45.0"
"@typescript-eslint/utils": "npm:8.45.0"
debug: "npm:^4.3.4"
ts-api-utils: "npm:^2.1.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/5405b71b91d02ed4eac1028fc156c053953403b9f48393d92340b15a8b05bee5bf1281324c6283ac31a0e03cc1a19baf94768cb3fd70b4621f8c07a4243837db
checksum: 10/81017b3f4780a65a4e4268ab208f1cb8891c1ced9ade23d8eb4575b18aeb99fe59a0d0ddbb4eea9c086567a1b4515d3466e850d4c81ec0d2d88658c43877a6cf
languageName: node
linkType: hard
"@typescript-eslint/types@npm:8.46.0, @typescript-eslint/types@npm:^8.46.0":
version: 8.46.0
resolution: "@typescript-eslint/types@npm:8.46.0"
checksum: 10/0118b0dd592bf4beaf41e8c6be812980dd0adea44d48c90d8b0272777b58d4cfd6326b8bc363efa3c640be476a6bf3632aee2d97052d5e34071e6576b9c28264
"@typescript-eslint/types@npm:8.45.0, @typescript-eslint/types@npm:^8.45.0":
version: 8.45.0
resolution: "@typescript-eslint/types@npm:8.45.0"
checksum: 10/889ded2b9bf376c876611b2a37f89051fdc8ec501314a4b97832caefa4305bffc4b752548941ce2e7f9659a81336d096d439d4c2ed236c99fefdf60b715593dd
languageName: node
linkType: hard
"@typescript-eslint/typescript-estree@npm:8.46.0":
version: 8.46.0
resolution: "@typescript-eslint/typescript-estree@npm:8.46.0"
"@typescript-eslint/typescript-estree@npm:8.45.0":
version: 8.45.0
resolution: "@typescript-eslint/typescript-estree@npm:8.45.0"
dependencies:
"@typescript-eslint/project-service": "npm:8.46.0"
"@typescript-eslint/tsconfig-utils": "npm:8.46.0"
"@typescript-eslint/types": "npm:8.46.0"
"@typescript-eslint/visitor-keys": "npm:8.46.0"
"@typescript-eslint/project-service": "npm:8.45.0"
"@typescript-eslint/tsconfig-utils": "npm:8.45.0"
"@typescript-eslint/types": "npm:8.45.0"
"@typescript-eslint/visitor-keys": "npm:8.45.0"
debug: "npm:^4.3.4"
fast-glob: "npm:^3.3.2"
is-glob: "npm:^4.0.3"
@@ -5053,32 +5051,32 @@ __metadata:
ts-api-utils: "npm:^2.1.0"
peerDependencies:
typescript: ">=4.8.4 <6.0.0"
checksum: 10/61053bd0c35a1fe5c82aef00cb70dbe0878ab28e55550cc1e2d6e7d4a0520c81947eb7505227c85a742a93db905d7e7376aed7d958dc257507b9bdda1daf0b00
checksum: 10/2fb4e63ad6128afbada8eabaabfe7d5a8f1a1f387bb13d7d3209103493ba974b518bf47b17e9a853beba10ec81efd5582ebf628c2eb77a924cf67d4d85466e5e
languageName: node
linkType: hard
"@typescript-eslint/utils@npm:8.46.0":
version: 8.46.0
resolution: "@typescript-eslint/utils@npm:8.46.0"
"@typescript-eslint/utils@npm:8.45.0":
version: 8.45.0
resolution: "@typescript-eslint/utils@npm:8.45.0"
dependencies:
"@eslint-community/eslint-utils": "npm:^4.7.0"
"@typescript-eslint/scope-manager": "npm:8.46.0"
"@typescript-eslint/types": "npm:8.46.0"
"@typescript-eslint/typescript-estree": "npm:8.46.0"
"@typescript-eslint/scope-manager": "npm:8.45.0"
"@typescript-eslint/types": "npm:8.45.0"
"@typescript-eslint/typescript-estree": "npm:8.45.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/4e0da60de389799afdd36249fd4bcf9e085a4d6f119e241e436a701b45cdf10becc3f1e3cdef29ebbf147a81f40d9a4800d428cb4a66799d3e4aa80b879c9ee2
checksum: 10/9e675a0da4434bd434901f9ba3e1e91d4d7ad542d7fcf8c23534a67f2f9039a569da20929e67a6562e3a263be226ad424cd0c1ac80f7828f4285f7f34e361926
languageName: node
linkType: hard
"@typescript-eslint/visitor-keys@npm:8.46.0":
version: 8.46.0
resolution: "@typescript-eslint/visitor-keys@npm:8.46.0"
"@typescript-eslint/visitor-keys@npm:8.45.0":
version: 8.45.0
resolution: "@typescript-eslint/visitor-keys@npm:8.45.0"
dependencies:
"@typescript-eslint/types": "npm:8.46.0"
"@typescript-eslint/types": "npm:8.45.0"
eslint-visitor-keys: "npm:^4.2.1"
checksum: 10/37e6145b6a5e960c59777d7fc86f722ff696e76c627106ac4577b945ca35744a5f96525d77bde50fe8c328503e9392e21e3adb7cf9899ae0efc054d63f4c3916
checksum: 10/8ae7e19c69c1f67fa8f952c18a09ad42a8cba492545d6e1dca6750e760893773f69ec6b1a96d0997e833c82aecc5ff7fb9546c5abd6c4427d91206670cf8ff37
languageName: node
linkType: hard
@@ -6925,10 +6923,10 @@ __metadata:
languageName: node
linkType: hard
"core-js@npm:3.46.0":
version: 3.46.0
resolution: "core-js@npm:3.46.0"
checksum: 10/82993ca487c6cbbf8bbf00e45eeb9705eb63dc2f9c90d7f35696733efbc3f4b52426e1f8dbef0f0b68ea16caa21e4f44cc5490e08120e1cad4a72b031ed8adaa
"core-js@npm:3.45.1":
version: 3.45.1
resolution: "core-js@npm:3.45.1"
checksum: 10/b9dca79b1af8bb4f0d4af0752ea98d694fe157abaf55513fd4084df32dfd4398f0fc57898b32cdb643c1cecb87b9231c2a2ce535797c80ae328eac6d6078ee61
languageName: node
linkType: hard
@@ -8032,18 +8030,18 @@ __metadata:
languageName: node
linkType: hard
"eslint@npm:9.37.0":
version: 9.37.0
resolution: "eslint@npm:9.37.0"
"eslint@npm:9.36.0":
version: 9.36.0
resolution: "eslint@npm:9.36.0"
dependencies:
"@eslint-community/eslint-utils": "npm:^4.8.0"
"@eslint-community/regexpp": "npm:^4.12.1"
"@eslint/config-array": "npm:^0.21.0"
"@eslint/config-helpers": "npm:^0.4.0"
"@eslint/core": "npm:^0.16.0"
"@eslint/config-helpers": "npm:^0.3.1"
"@eslint/core": "npm:^0.15.2"
"@eslint/eslintrc": "npm:^3.3.1"
"@eslint/js": "npm:9.37.0"
"@eslint/plugin-kit": "npm:^0.4.0"
"@eslint/js": "npm:9.36.0"
"@eslint/plugin-kit": "npm:^0.3.5"
"@humanfs/node": "npm:^0.16.6"
"@humanwhocodes/module-importer": "npm:^1.0.1"
"@humanwhocodes/retry": "npm:^0.4.2"
@@ -8078,7 +8076,7 @@ __metadata:
optional: true
bin:
eslint: bin/eslint.js
checksum: 10/c7530470c9cafe9a7f768477f7894d9b9d28e92995186223e99fbd9edeb391119e2a70678a2e98e213ae37cbb41de89403b510f5f33df2340aa65dd6f2a3c0bb
checksum: 10/6e512a82e680e6cdc554e97c7e232b83171f24a52fb46f89c2df74bcb80fe59b6e0a989485c9fe7e9ca81556b1953dd8604ace4ed37f651eded9a37816c06b7c
languageName: node
linkType: hard
@@ -9182,32 +9180,32 @@ __metadata:
"@babel/preset-env": "npm:7.28.3"
"@babel/runtime": "npm:7.28.4"
"@braintree/sanitize-url": "npm:7.1.1"
"@bundle-stats/plugin-webpack-filter": "npm:4.21.5"
"@bundle-stats/plugin-webpack-filter": "npm:4.21.4"
"@codemirror/autocomplete": "npm:6.19.0"
"@codemirror/commands": "npm:6.9.0"
"@codemirror/language": "npm:6.11.3"
"@codemirror/legacy-modes": "npm:6.5.2"
"@codemirror/search": "npm:6.5.11"
"@codemirror/state": "npm:6.5.2"
"@codemirror/view": "npm:6.38.5"
"@codemirror/view": "npm:6.38.4"
"@date-fns/tz": "npm:1.4.1"
"@egjs/hammerjs": "npm:2.0.17"
"@formatjs/intl-datetimeformat": "npm:6.18.2"
"@formatjs/intl-displaynames": "npm:6.8.13"
"@formatjs/intl-durationformat": "npm:0.7.6"
"@formatjs/intl-getcanonicallocales": "npm:2.5.6"
"@formatjs/intl-listformat": "npm:7.7.13"
"@formatjs/intl-locale": "npm:4.2.13"
"@formatjs/intl-numberformat": "npm:8.15.6"
"@formatjs/intl-pluralrules": "npm:5.4.6"
"@formatjs/intl-relativetimeformat": "npm:11.4.13"
"@formatjs/intl-datetimeformat": "npm:6.18.0"
"@formatjs/intl-displaynames": "npm:6.8.11"
"@formatjs/intl-durationformat": "npm:0.7.4"
"@formatjs/intl-getcanonicallocales": "npm:2.5.5"
"@formatjs/intl-listformat": "npm:7.7.11"
"@formatjs/intl-locale": "npm:4.2.11"
"@formatjs/intl-numberformat": "npm:8.15.4"
"@formatjs/intl-pluralrules": "npm:5.4.4"
"@formatjs/intl-relativetimeformat": "npm:11.4.11"
"@fullcalendar/core": "npm:6.1.19"
"@fullcalendar/daygrid": "npm:6.1.19"
"@fullcalendar/interaction": "npm:6.1.19"
"@fullcalendar/list": "npm:6.1.19"
"@fullcalendar/luxon3": "npm:6.1.19"
"@fullcalendar/timegrid": "npm:6.1.19"
"@home-assistant/webawesome": "npm:3.0.0-beta.6.ha.4"
"@home-assistant/webawesome": "npm:3.0.0-beta.6.ha.1"
"@lezer/highlight": "npm:1.2.1"
"@lit-labs/motion": "npm:1.0.9"
"@lit-labs/observers": "npm:2.0.6"
@@ -9283,7 +9281,7 @@ __metadata:
browserslist-useragent-regexp: "npm:4.1.3"
color-name: "npm:2.0.2"
comlink: "npm:4.4.2"
core-js: "npm:3.46.0"
core-js: "npm:3.45.1"
cropperjs: "npm:1.6.2"
culori: "npm:4.0.2"
date-fns: "npm:4.1.0"
@@ -9293,7 +9291,7 @@ __metadata:
dialog-polyfill: "npm:0.5.6"
echarts: "npm:6.0.0"
element-internals-polyfill: "npm:3.0.2"
eslint: "npm:9.37.0"
eslint: "npm:9.36.0"
eslint-config-airbnb-base: "npm:15.0.0"
eslint-config-prettier: "npm:10.1.8"
eslint-import-resolver-webpack: "npm:0.13.10"
@@ -9317,7 +9315,7 @@ __metadata:
html-minifier-terser: "npm:7.2.0"
husky: "npm:9.1.7"
idb-keyval: "npm:6.2.2"
intl-messageformat: "npm:10.7.18"
intl-messageformat: "npm:10.7.16"
js-yaml: "npm:4.1.0"
jsdom: "npm:27.0.0"
jszip: "npm:3.10.1"
@@ -9332,7 +9330,7 @@ __metadata:
lodash.template: "npm:4.5.0"
luxon: "npm:3.7.2"
map-stream: "npm:0.0.7"
marked: "npm:16.4.0"
marked: "npm:16.3.0"
memoize-one: "npm:6.0.0"
node-vibrant: "npm:4.0.3"
object-hash: "npm:3.0.0"
@@ -9354,8 +9352,8 @@ __metadata:
tinykeys: "npm:3.0.0"
ts-lit-plugin: "npm:2.0.2"
typescript: "npm:5.9.3"
typescript-eslint: "npm:8.46.0"
ua-parser-js: "npm:2.0.6"
typescript-eslint: "npm:8.45.0"
ua-parser-js: "npm:2.0.5"
vite-tsconfig-paths: "npm:5.1.4"
vitest: "npm:3.2.4"
vue: "npm:2.7.16"
@@ -9712,15 +9710,15 @@ __metadata:
languageName: node
linkType: hard
"intl-messageformat@npm:10.7.18":
version: 10.7.18
resolution: "intl-messageformat@npm:10.7.18"
"intl-messageformat@npm:10.7.16":
version: 10.7.16
resolution: "intl-messageformat@npm:10.7.16"
dependencies:
"@formatjs/ecma402-abstract": "npm:2.3.6"
"@formatjs/ecma402-abstract": "npm:2.3.4"
"@formatjs/fast-memoize": "npm:2.2.7"
"@formatjs/icu-messageformat-parser": "npm:2.11.4"
"@formatjs/icu-messageformat-parser": "npm:2.11.2"
tslib: "npm:^2.8.0"
checksum: 10/96650d673912763d21bbfa14b50749b992d45f1901092a020e3155961e3c70f4644dd1731c3ecb1207a1eb94d84bedf4c34b1ac8127c29ad6b015b6a2a4045cb
checksum: 10/c19b77c5e495ce8b0d1aa0d95444bf3a4f73886805f1e08d7159b364abcf2f63686b2ccf202eaafb0e39a0e9fde61848b8dd2db1679efd4f6ec8f6a3d0e77928
languageName: node
linkType: hard
@@ -10974,12 +10972,12 @@ __metadata:
languageName: node
linkType: hard
"marked@npm:16.4.0":
version: 16.4.0
resolution: "marked@npm:16.4.0"
"marked@npm:16.3.0":
version: 16.3.0
resolution: "marked@npm:16.3.0"
bin:
marked: bin/marked.js
checksum: 10/5174b345ccc61e2030c2eb8abb3e5cbebeb6697a6d2b609f64ffa2ff6e482f5f1e1fda1912db19c747f43971b1fa54ae53c1ab1ce5d2f58566d6db4bc3016833
checksum: 10/60497834b9acfb3b3994222509d359ecb9a197c885dfeb77e2050a287cd2f4ab19f00d5597172b47f9e0c54d9e1e13d8b2dd73322b7838599e1f16d1d6283f5b
languageName: node
linkType: hard
@@ -14317,18 +14315,18 @@ __metadata:
languageName: node
linkType: hard
"typescript-eslint@npm:8.46.0":
version: 8.46.0
resolution: "typescript-eslint@npm:8.46.0"
"typescript-eslint@npm:8.45.0":
version: 8.45.0
resolution: "typescript-eslint@npm:8.45.0"
dependencies:
"@typescript-eslint/eslint-plugin": "npm:8.46.0"
"@typescript-eslint/parser": "npm:8.46.0"
"@typescript-eslint/typescript-estree": "npm:8.46.0"
"@typescript-eslint/utils": "npm:8.46.0"
"@typescript-eslint/eslint-plugin": "npm:8.45.0"
"@typescript-eslint/parser": "npm:8.45.0"
"@typescript-eslint/typescript-estree": "npm:8.45.0"
"@typescript-eslint/utils": "npm:8.45.0"
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: ">=4.8.4 <6.0.0"
checksum: 10/fd74aab1d21d661299a64107236b5c3515d6d955eb1764b56c5c9505b8cef5f2600e8290d251f1379138333573df94a1fe1fd7fef23952b5ab9f12ff2b774f92
checksum: 10/1c17ebb5bcbea418c8f372d71b5c2df8c9b8c6897d1bda8196ea17bac8fabeffe1814bc4f7a28d40f404fb811c97fcda0d69c4375b4f010d9bf44d19d8401706
languageName: node
linkType: hard
@@ -14379,16 +14377,17 @@ __metadata:
languageName: node
linkType: hard
"ua-parser-js@npm:2.0.6":
version: 2.0.6
resolution: "ua-parser-js@npm:2.0.6"
"ua-parser-js@npm:2.0.5":
version: 2.0.5
resolution: "ua-parser-js@npm:2.0.5"
dependencies:
detect-europe-js: "npm:^0.1.2"
is-standalone-pwa: "npm:^0.1.1"
ua-is-frozen: "npm:^0.1.2"
undici: "npm:^7.12.0"
bin:
ua-parser-js: script/cli.js
checksum: 10/b0049d3b272979049c7df6af2ec2ce032e4351316b10c33699f6e3f0bec701336f67530cc3ccb363c554b1bb5047b75d2f46575699afacd6e541762ca3861f4d
checksum: 10/e946cb1c85bfcd0f2d30c7d5e1b605e340bb458432e7e87fc4aa1b2f90117e4220521d4e0bc7dd8c2a5cadd0935dedb5ac434b70efdc0007221288c1d98b3cd5
languageName: node
linkType: hard
@@ -14451,6 +14450,13 @@ __metadata:
languageName: node
linkType: hard
"undici@npm:^7.12.0":
version: 7.16.0
resolution: "undici@npm:7.16.0"
checksum: 10/2bb71672b23d3dc0f56f1b7fb6c936e4487a350db46eaafc03f2f9107f99cdf8e51ecdd32e589e2381ef47a64b6369cfb31f328b2c3ea663023aa47bc5258b9e
languageName: node
linkType: hard
"unicode-canonical-property-names-ecmascript@npm:^2.0.0":
version: 2.0.1
resolution: "unicode-canonical-property-names-ecmascript@npm:2.0.1"