mirror of
https://github.com/home-assistant/frontend.git
synced 2026-11-13 16:33:02 +00:00
Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
692338459d | ||
|
|
a8ee519fcf | ||
|
|
fdbab9046c | ||
|
|
9f19a66a51 | ||
|
|
7b5d02bdba | ||
|
|
3a3228bf79 | ||
|
|
a299f5f9a7 | ||
|
|
27902ef586 | ||
|
|
2d0ab5349b | ||
|
|
77170b447f | ||
|
|
e2bb744202 | ||
|
|
0eec83448a | ||
|
|
1864f6f802 | ||
|
|
9a19cc3bf6 | ||
|
|
ec51fb4f8d | ||
|
|
6a1a9bfb1b | ||
|
|
16807d2765 | ||
|
|
9caad65c91 | ||
|
|
af523f1087 | ||
|
|
349d5f7125 |
No files matched your search
@@ -157,6 +157,99 @@ async function pollProcess(lokaliseApi, projectId, processId) {
|
||||
}
|
||||
/* eslint-enable no-await-in-loop */
|
||||
|
||||
const DRAFTS_ACCEPTED_KEYS = [
|
||||
// Written per language rather than translated from English, so an English edit doesn't make them outdated
|
||||
"ui.components.rooms.keywords",
|
||||
];
|
||||
|
||||
async function exportTranslations(lokaliseApi, projectId) {
|
||||
const exportProcess = await lokaliseApi.files().async_download(projectId, {
|
||||
format: "json",
|
||||
original_filenames: false,
|
||||
replace_breaks: false,
|
||||
json_unescaped_slashes: true,
|
||||
export_empty_as: "skip",
|
||||
filter_data: ["verified"],
|
||||
});
|
||||
|
||||
const finishedProcess = await pollProcess(
|
||||
lokaliseApi,
|
||||
projectId,
|
||||
exportProcess.process_id
|
||||
);
|
||||
|
||||
const bundleUrl = finishedProcess.details.download_url;
|
||||
|
||||
console.log(`Downloading translations from: ${bundleUrl}`);
|
||||
|
||||
const response = await fetch(bundleUrl);
|
||||
|
||||
if (response.status !== 200 && response.status !== 0) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
console.log(`Extracting translations...`);
|
||||
|
||||
const contents = await JSZip.loadAsync(await response.arrayBuffer());
|
||||
|
||||
const files = {};
|
||||
await Promise.all(
|
||||
Object.keys(contents.files).map(async (filename) => {
|
||||
const file = contents.file(filename);
|
||||
if (!file) {
|
||||
// no file, probably a directory
|
||||
return;
|
||||
}
|
||||
files[filename.split("/").splice(-1)[0]] = await file.async("string");
|
||||
})
|
||||
);
|
||||
return files;
|
||||
}
|
||||
|
||||
async function fetchDrafts(lokaliseApi, projectId) {
|
||||
const english = JSON.parse(
|
||||
await fs.readFile("src/translations/en.json", { encoding })
|
||||
);
|
||||
const keyNames = DRAFTS_ACCEPTED_KEYS.flatMap((key) => {
|
||||
const keys = key.split(".");
|
||||
const drafts = keys.reduce((obj, part) => obj?.[part], english) ?? {};
|
||||
return Object.keys(drafts).map((name) => [...keys, name].join("::"));
|
||||
});
|
||||
if (!keyNames.length) {
|
||||
return [];
|
||||
}
|
||||
const { items } = await lokaliseApi.keys().list({
|
||||
project_id: projectId,
|
||||
filter_keys: keyNames.join(","),
|
||||
include_translations: 1,
|
||||
limit: keyNames.length,
|
||||
});
|
||||
return items.flatMap((key) =>
|
||||
key.translations
|
||||
.filter(({ translation }) => translation)
|
||||
.map(({ language_iso, translation }) => ({
|
||||
filename: `${language_iso}.json`,
|
||||
path: key.key_name.web.split("::"),
|
||||
translation,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
function acceptDrafts(files, drafts) {
|
||||
const data = {};
|
||||
drafts.forEach(({ filename, path: keyPath, translation }) => {
|
||||
data[filename] ??= files[filename] ? JSON.parse(files[filename]) : {};
|
||||
const parent = keyPath.slice(0, -1).reduce((obj, part) => {
|
||||
obj[part] ??= {};
|
||||
return obj[part];
|
||||
}, data[filename]);
|
||||
parent[keyPath.at(-1)] = translation;
|
||||
});
|
||||
Object.entries(data).forEach(([filename, content]) => {
|
||||
files[filename] = JSON.stringify(content, null, 2);
|
||||
});
|
||||
}
|
||||
|
||||
gulp.task("fetch-lokalise", async function () {
|
||||
let apiKey;
|
||||
try {
|
||||
@@ -193,52 +286,24 @@ gulp.task("fetch-lokalise", async function () {
|
||||
await Promise.all(
|
||||
projects.map(async ([project, projectId]) => {
|
||||
try {
|
||||
const exportProcess = await lokaliseApi
|
||||
.files()
|
||||
.async_download(projectId, {
|
||||
format: "json",
|
||||
original_filenames: false,
|
||||
replace_breaks: false,
|
||||
json_unescaped_slashes: true,
|
||||
export_empty_as: "skip",
|
||||
filter_data: ["verified"],
|
||||
});
|
||||
|
||||
const finishedProcess = await pollProcess(
|
||||
lokaliseApi,
|
||||
projectId,
|
||||
exportProcess.process_id
|
||||
);
|
||||
|
||||
const bundleUrl = finishedProcess.details.download_url;
|
||||
|
||||
console.log(`Downloading translations from: ${bundleUrl}`);
|
||||
|
||||
const response = await fetch(bundleUrl);
|
||||
|
||||
if (response.status !== 200 && response.status !== 0) {
|
||||
throw new Error(response.statusText);
|
||||
const [files, drafts] = await Promise.all([
|
||||
exportTranslations(lokaliseApi, projectId),
|
||||
project === "frontend"
|
||||
? fetchDrafts(lokaliseApi, projectId)
|
||||
: undefined,
|
||||
]);
|
||||
if (drafts) {
|
||||
acceptDrafts(files, drafts);
|
||||
}
|
||||
|
||||
console.log(`Extracting translations...`);
|
||||
|
||||
const contents = await JSZip.loadAsync(await response.arrayBuffer());
|
||||
|
||||
await mkdirPromise;
|
||||
await Promise.all(
|
||||
Object.keys(contents.files).map(async (filename) => {
|
||||
const file = contents.file(filename);
|
||||
if (!file) {
|
||||
// no file, probably a directory
|
||||
return;
|
||||
}
|
||||
const content = await file.async("nodebuffer");
|
||||
await fs.writeFile(
|
||||
path.join(inDir, project, filename.split("/").splice(-1)[0]),
|
||||
content,
|
||||
{ flag: "w", encoding }
|
||||
);
|
||||
})
|
||||
Object.entries(files).map(([filename, content]) =>
|
||||
fs.writeFile(path.join(inDir, project, filename), content, {
|
||||
flag: "w",
|
||||
encoding,
|
||||
})
|
||||
)
|
||||
);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import type { ReactiveElement } from "lit";
|
||||
import { ContextSubscriptionController } from "../../../src/common/decorators/consume";
|
||||
import { uiContext } from "../../../src/data/context";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
// A demo that provides its own hass hides the gallery theme from its children,
|
||||
// so pass the gallery light or dark mode on to it.
|
||||
export const followGalleryTheme = (
|
||||
host: ReactiveElement,
|
||||
hass: MockHomeAssistant
|
||||
) =>
|
||||
new ContextSubscriptionController(host, uiContext, (ui) => {
|
||||
if (ui) {
|
||||
hass.updateHass({
|
||||
themes: ui.themes,
|
||||
selectedTheme: ui.selectedTheme,
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -117,11 +117,17 @@ shared slots/attributes below.
|
||||
|
||||
**Attributes**
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| ----------------- | ------- | ------- | --------------------------------------- |
|
||||
| `headline` | String | — | Primary text. Overridden by the slot. |
|
||||
| `supporting-text` | String | — | Secondary text. Overridden by the slot. |
|
||||
| `disabled` | Boolean | `false` | Dims the row and blocks pointer events. |
|
||||
| Name | Type | Default | Description |
|
||||
| ---------------------- | ------- | ------- | -------------------------------------------------- |
|
||||
| `headline` | String | — | Primary text. Overridden by the slot. |
|
||||
| `supporting-text` | String | — | Secondary text. Overridden by the slot. |
|
||||
| `wrap-headline` | Boolean | `false` | Wrap the headline instead of truncating it. |
|
||||
| `wrap-supporting-text` | Boolean | `false` | Wrap the supporting text instead of truncating it. |
|
||||
| `disabled` | Boolean | `false` | Dims the row and blocks pointer events. |
|
||||
|
||||
The headline and the supporting text stay on one line and are truncated with an
|
||||
ellipsis by default. Set `wrap-headline` or `wrap-supporting-text` on rows whose
|
||||
text has to stay readable in full, like settings with a description.
|
||||
|
||||
**CSS parts**
|
||||
|
||||
|
||||
@@ -191,6 +191,20 @@ export class DemoHaList extends LitElement {
|
||||
<ha-list-item-base headline="Disabled row" disabled>
|
||||
<ha-svg-icon slot="start" .path=${mdiHome}></ha-svg-icon>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base
|
||||
headline="Truncated by default, a long headline stays on one line and ends with an ellipsis"
|
||||
supporting-text="Long supporting text is truncated as well, so every row in the list keeps the same height"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiHome}></ha-svg-icon>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base
|
||||
wrap-headline
|
||||
wrap-supporting-text
|
||||
headline="With wrap-headline, a long headline wraps onto as many lines as it needs"
|
||||
supporting-text="With wrap-supporting-text, long supporting text wraps too, which suits settings with a description"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiHome}></ha-svg-icon>
|
||||
</ha-list-item-base>
|
||||
</ha-list-base>
|
||||
</ha-card>
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import type {
|
||||
} from "../../../../src/data/history";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { followGalleryTheme } from "../../data/follow-gallery-theme";
|
||||
|
||||
const HOUR = 60 * 60 * 1000;
|
||||
const HOURS = 48;
|
||||
@@ -166,6 +167,7 @@ export class DemoStateHistoryCharts extends LitElement {
|
||||
super.firstUpdated(changedProps);
|
||||
const hass = provideHass(this);
|
||||
hass.updateTranslations(null, "en");
|
||||
followGalleryTheme(this, hass);
|
||||
hass.addEntities(ENTITIES);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { ChartSelectMode } from "../../../../src/components/chart/ha-chart-base";
|
||||
import "../../../../src/components/chart/statistics-chart";
|
||||
import "../../../../src/components/ha-button";
|
||||
import "../../../../src/components/ha-card";
|
||||
import type {
|
||||
Statistics,
|
||||
@@ -11,7 +13,9 @@ import type {
|
||||
} from "../../../../src/data/recorder";
|
||||
import { StatisticMeanType } from "../../../../src/data/recorder";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
import type { HASSDomEvent } from "../../../../src/common/dom/fire_event";
|
||||
import type { HomeAssistant } from "../../../../src/types";
|
||||
import { followGalleryTheme } from "../../data/follow-gallery-theme";
|
||||
|
||||
const HOUR = 60 * 60 * 1000;
|
||||
const HOURS = 48;
|
||||
@@ -108,6 +112,64 @@ interface Example {
|
||||
height?: string;
|
||||
}
|
||||
|
||||
interface TimeRange {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
interface RangeExample {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
statistics: Statistics;
|
||||
statTypes: StatisticType[];
|
||||
chartType: "line" | "bar";
|
||||
unit: string;
|
||||
selectMode: ChartSelectMode;
|
||||
hint: string;
|
||||
}
|
||||
|
||||
const RANGE_EXAMPLES: RangeExample[] = [
|
||||
{
|
||||
id: "point",
|
||||
title: "Point selection",
|
||||
description:
|
||||
"With select-mode set to point, a click selects the hour that contains it, and everything outside fades. Dragging still zooms.",
|
||||
statistics: { [HEAT_PUMP_ID]: HEAT_PUMP },
|
||||
statTypes: ["change"],
|
||||
chartType: "bar",
|
||||
unit: "kWh",
|
||||
selectMode: "point",
|
||||
hint: "Click on a bar to select its hour.",
|
||||
},
|
||||
{
|
||||
id: "range-bar",
|
||||
title: "Range selection on bars",
|
||||
description:
|
||||
"With select-mode set to range, dragging selects a time range instead of zooming, so several bars can be selected at once. A click still selects one bar.",
|
||||
statistics: { [HEAT_PUMP_ID]: HEAT_PUMP },
|
||||
statTypes: ["change"],
|
||||
chartType: "bar",
|
||||
unit: "kWh",
|
||||
selectMode: "range",
|
||||
hint: "Click or drag on the chart to select one or more bars.",
|
||||
},
|
||||
{
|
||||
id: "range",
|
||||
title: "Range selection on lines",
|
||||
description: "The same on a line chart.",
|
||||
statistics: { [LIVING_ROOM_ID]: LIVING_ROOM },
|
||||
statTypes: ["mean", "min", "max"],
|
||||
chartType: "line",
|
||||
unit: "°C",
|
||||
selectMode: "range",
|
||||
hint: "Click or drag on the chart to select a period.",
|
||||
},
|
||||
];
|
||||
|
||||
const formatRange = (range: TimeRange) =>
|
||||
`${new Date(range.start).toLocaleString()} to ${new Date(range.end).toLocaleString()}`;
|
||||
|
||||
const EXAMPLES: Example[] = [
|
||||
{
|
||||
title: "Sum as bars",
|
||||
@@ -159,17 +221,20 @@ const EXAMPLES: Example[] = [
|
||||
export class DemoStatisticsChart extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@state() private _selections: Record<string, TimeRange | undefined> = {};
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
const hass = provideHass(this);
|
||||
hass.updateTranslations(null, "en");
|
||||
followGalleryTheme(this, hass);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
return EXAMPLES.map(
|
||||
return html`${EXAMPLES.map(
|
||||
(example) => html`
|
||||
<ha-card header=${example.title}>
|
||||
<div class="card-content">
|
||||
@@ -191,7 +256,66 @@ export class DemoStatisticsChart extends LitElement {
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
);
|
||||
)}
|
||||
${RANGE_EXAMPLES.map((example) => this._renderRangeExample(example))}`;
|
||||
}
|
||||
|
||||
private _renderRangeExample(example: RangeExample) {
|
||||
const selection = this._selections[example.id];
|
||||
return html`
|
||||
<ha-card header=${example.title}>
|
||||
<div class="card-content">
|
||||
<p>${example.description}</p>
|
||||
<statistics-chart
|
||||
.statisticsData=${example.statistics}
|
||||
.metadata=${METADATA}
|
||||
.names=${NAMES}
|
||||
.statTypes=${example.statTypes}
|
||||
.chartType=${example.chartType}
|
||||
period="hour"
|
||||
.unit=${example.unit}
|
||||
.startTime=${START}
|
||||
.endTime=${END}
|
||||
.highlight=${selection}
|
||||
.clickForMoreInfo=${false}
|
||||
data-example=${example.id}
|
||||
.selectMode=${example.selectMode}
|
||||
hide-legend
|
||||
@chart-select=${this._rangeSelected}
|
||||
></statistics-chart>
|
||||
<div class="selection">
|
||||
${
|
||||
selection
|
||||
? html`
|
||||
<span>${formatRange(selection)}</span>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="s"
|
||||
data-example=${example.id}
|
||||
@click=${this._clearSelection}
|
||||
>
|
||||
Clear
|
||||
</ha-button>
|
||||
`
|
||||
: html`<span class="hint">${example.hint}</span>`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _rangeSelected(ev: HASSDomEvent<HASSDomEvents["chart-select"]>) {
|
||||
const id = (ev.currentTarget as HTMLElement).dataset.example!;
|
||||
this._selections = {
|
||||
...this._selections,
|
||||
[id]: ev.detail,
|
||||
};
|
||||
}
|
||||
|
||||
private _clearSelection(ev: Event) {
|
||||
const id = (ev.currentTarget as HTMLElement).dataset.example!;
|
||||
this._selections = { ...this._selections, [id]: undefined };
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
@@ -202,6 +326,20 @@ export class DemoStatisticsChart extends LitElement {
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
.selection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--ha-space-2);
|
||||
min-height: 48px;
|
||||
margin-top: var(--ha-space-2);
|
||||
padding: 0 var(--ha-space-2) 0 var(--ha-space-4);
|
||||
border-radius: var(--ha-border-radius-lg);
|
||||
background-color: var(--secondary-background-color);
|
||||
}
|
||||
.hint {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
+8
-8
@@ -55,14 +55,14 @@
|
||||
"@date-fns/tz": "1.5.0",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "8.0.3",
|
||||
"@formatjs/intl-displaynames": "8.0.3",
|
||||
"@formatjs/intl-displaynames": "8.0.2",
|
||||
"@formatjs/intl-durationformat": "0.12.4",
|
||||
"@formatjs/intl-getcanonicallocales": "3.3.1",
|
||||
"@formatjs/intl-listformat": "9.0.3",
|
||||
"@formatjs/intl-listformat": "9.0.2",
|
||||
"@formatjs/intl-locale": "5.4.3",
|
||||
"@formatjs/intl-numberformat": "10.0.4",
|
||||
"@formatjs/intl-pluralrules": "7.0.4",
|
||||
"@formatjs/intl-relativetimeformat": "13.0.3",
|
||||
"@formatjs/intl-numberformat": "10.0.3",
|
||||
"@formatjs/intl-pluralrules": "7.0.3",
|
||||
"@formatjs/intl-relativetimeformat": "13.0.2",
|
||||
"@home-assistant/webawesome": "3.14.0-ha.2",
|
||||
"@lezer/common": "1.5.5",
|
||||
"@lezer/highlight": "1.2.5",
|
||||
@@ -102,7 +102,7 @@
|
||||
"hls.js": "1.7.4",
|
||||
"home-assistant-js-websocket": "9.8.1",
|
||||
"idb-keyval": "6.3.0",
|
||||
"intl-messageformat": "12.1.4",
|
||||
"intl-messageformat": "12.1.5",
|
||||
"js-yaml": "5.5.0",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet.markercluster": "1.5.3",
|
||||
@@ -180,7 +180,7 @@
|
||||
"eslint": "10.14.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-webpack": "0.13.11",
|
||||
"eslint-plugin-import-x": "4.18.0",
|
||||
"eslint-plugin-import-x": "4.18.1",
|
||||
"eslint-plugin-lit": "2.3.1",
|
||||
"eslint-plugin-lit-a11y": "5.1.3",
|
||||
"eslint-plugin-unused-imports": "4.4.1",
|
||||
@@ -215,7 +215,7 @@
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.72.1",
|
||||
"vite": "8.3.6",
|
||||
"vite": "8.3.7",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "5.1.0",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
|
||||
Generated
+122
-122
@@ -225,8 +225,8 @@ importers:
|
||||
specifier: 8.0.3
|
||||
version: 8.0.3
|
||||
'@formatjs/intl-displaynames':
|
||||
specifier: 8.0.3
|
||||
version: 8.0.3
|
||||
specifier: 8.0.2
|
||||
version: 8.0.2
|
||||
'@formatjs/intl-durationformat':
|
||||
specifier: 0.12.4
|
||||
version: 0.12.4
|
||||
@@ -234,20 +234,20 @@ importers:
|
||||
specifier: 3.3.1
|
||||
version: 3.3.1
|
||||
'@formatjs/intl-listformat':
|
||||
specifier: 9.0.3
|
||||
version: 9.0.3
|
||||
specifier: 9.0.2
|
||||
version: 9.0.2
|
||||
'@formatjs/intl-locale':
|
||||
specifier: 5.4.3
|
||||
version: 5.4.3
|
||||
'@formatjs/intl-numberformat':
|
||||
specifier: 10.0.4
|
||||
version: 10.0.4
|
||||
specifier: 10.0.3
|
||||
version: 10.0.3
|
||||
'@formatjs/intl-pluralrules':
|
||||
specifier: 7.0.4
|
||||
version: 7.0.4
|
||||
specifier: 7.0.3
|
||||
version: 7.0.3
|
||||
'@formatjs/intl-relativetimeformat':
|
||||
specifier: 13.0.3
|
||||
version: 13.0.3
|
||||
specifier: 13.0.2
|
||||
version: 13.0.2
|
||||
'@home-assistant/webawesome':
|
||||
specifier: 3.14.0-ha.2
|
||||
version: 3.14.0-ha.2(@floating-ui/[email protected])(@types/[email protected])
|
||||
@@ -366,8 +366,8 @@ importers:
|
||||
specifier: 6.3.0
|
||||
version: 6.3.0
|
||||
intl-messageformat:
|
||||
specifier: 12.1.4
|
||||
version: 12.1.4
|
||||
specifier: 12.1.5
|
||||
version: 12.1.5
|
||||
js-yaml:
|
||||
specifier: 5.5.0
|
||||
version: 5.5.0
|
||||
@@ -595,8 +595,8 @@ importers:
|
||||
specifier: 0.13.11
|
||||
version: 0.13.11([email protected])
|
||||
eslint-plugin-import-x:
|
||||
specifier: 4.18.0
|
||||
version: 4.18.0(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected]))([email protected])
|
||||
specifier: 4.18.1
|
||||
version: 4.18.1(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected]))([email protected])
|
||||
eslint-plugin-lit:
|
||||
specifier: 2.3.1
|
||||
version: 2.3.1([email protected]([email protected]))
|
||||
@@ -700,14 +700,14 @@ importers:
|
||||
specifier: 8.72.1
|
||||
version: 8.72.1([email protected]([email protected]))([email protected])([email protected])
|
||||
vite:
|
||||
specifier: 8.3.6
|
||||
version: 8.3.6(@types/[email protected])([email protected])([email protected])
|
||||
specifier: 8.3.7
|
||||
version: 8.3.7(@types/[email protected])([email protected])([email protected])
|
||||
vite-tsconfig-paths:
|
||||
specifier: 6.1.1
|
||||
version: 6.1.1([email protected])([email protected])([email protected].6(@types/[email protected])([email protected])([email protected]))
|
||||
version: 6.1.1([email protected])([email protected])([email protected].7(@types/[email protected])([email protected])([email protected]))
|
||||
vitest:
|
||||
specifier: 5.1.0
|
||||
version: 5.1.0(@types/[email protected])(@vitest/[email protected])([email protected])([email protected].6(@types/[email protected])([email protected])([email protected]))
|
||||
version: 5.1.0(@types/[email protected])(@vitest/[email protected])([email protected])([email protected].7(@types/[email protected])([email protected])([email protected]))
|
||||
webpack-stats-plugin:
|
||||
specifier: 1.1.3
|
||||
version: 1.1.3
|
||||
@@ -2021,20 +2021,20 @@ packages:
|
||||
'@formatjs/[email protected]':
|
||||
resolution: {integrity: sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==}
|
||||
|
||||
'@formatjs/[email protected]2':
|
||||
resolution: {integrity: sha512-YuwN+dDKgssfuYwtOyGQD0CSn7K46g9cv1Fp0VgKpjZDDFvv8b8crzP990IcJhRPWHhKzCGsbUSJVIguyyFA5A==}
|
||||
'@formatjs/[email protected]3':
|
||||
resolution: {integrity: sha512-iB1fYfhtSGd8uH9A8b1m01Ujcv6QtQnJasefP3jPApmz72L/zNipzf3BdoinTMd1xuviVChjUJWL60iTwjUQ+w==}
|
||||
|
||||
'@formatjs/[email protected]':
|
||||
resolution: {integrity: sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==}
|
||||
|
||||
'@formatjs/[email protected]2':
|
||||
resolution: {integrity: sha512-ca7tZMLpfHX3sUD7hP6gE/qr6Iq89EDd3ZLeRhbsB7r0UD55DGJq+tmFLzrGrnx5StahUevQmQQVtbDRHsfDHA==}
|
||||
'@formatjs/[email protected]3':
|
||||
resolution: {integrity: sha512-IdAJ9NrHn7AF0zqncD9UpxQozTYyJtbwwrZnJMEKVxTcOOXtVkUb2XYTpNOGTAv58NCsk0rjRB9G+i2SvBmGAQ==}
|
||||
|
||||
'@formatjs/[email protected]':
|
||||
resolution: {integrity: sha512-LQed+WhJBlceMJ4Zbe7dDe7Wn15le9t5HXfOFGdJYM/MGDyn56LTHlSzPzFjsJA0rvyp4FjN28jRQiMwtsji6A==}
|
||||
|
||||
'@formatjs/[email protected].3':
|
||||
resolution: {integrity: sha512-UhKG1OwrbQzZNKIcmZu+/JR5SOxE25RWmUzxaNfQ04AJ4OwxNkTvAbVX0ojnkm0i8Qs4E8ElIoBdA6F8XTkJXw==}
|
||||
'@formatjs/[email protected].2':
|
||||
resolution: {integrity: sha512-9gayLdxgPhzw2UT2nbWBhrRN+7T2xy/cvvrEVFClRLzg1DSJJdXtdYRcbbQ8cdCRlC1O3dQag+bUeW+Kt9bRWA==}
|
||||
|
||||
'@formatjs/[email protected]':
|
||||
resolution: {integrity: sha512-EjUHL8isAQOqXuLxGllgsfIQdLrLNo8iCOQHMqUNY4Fvl52hw6TEP0n9tPnOmQWb5wTUa92NeqyGmwhjQ2lvHA==}
|
||||
@@ -2042,8 +2042,8 @@ packages:
|
||||
'@formatjs/[email protected]':
|
||||
resolution: {integrity: sha512-GkXdVwb/4Y9qJaZ19BguPv2i9Vr+z5MrlmFa/9HR7WK2tMcCWZathuGhn7W/V3sFqUdAVB3uvmGuOxXwc61DJQ==}
|
||||
|
||||
'@formatjs/[email protected].3':
|
||||
resolution: {integrity: sha512-XamK+7ie16gbBp51AH+K97NfwCoECk36jEbBO9m7HaInoLbG1F3P8M22enKtkifsi/cH9gjp4igFaYtY23h0uA==}
|
||||
'@formatjs/[email protected].2':
|
||||
resolution: {integrity: sha512-Qfsm6eyPyD6y7AgRyiOTJVaq0IBClDXQCm0/6vT3CRiwn89npdoUv3kTuwDykGDs1igLoTPWLBdMR81HIP4qQg==}
|
||||
|
||||
'@formatjs/[email protected]':
|
||||
resolution: {integrity: sha512-YdxSbfDifRIew7mukcwITPlhkomfHCcSzzbjEJqOGbod4wrfdUN7SmiZQU2Lr8zs/q/15ntZsthJ8NsW3P30mA==}
|
||||
@@ -2054,14 +2054,14 @@ packages:
|
||||
'@formatjs/[email protected]':
|
||||
resolution: {integrity: sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==}
|
||||
|
||||
'@formatjs/[email protected].4':
|
||||
resolution: {integrity: sha512-rQzbAgJcG/RPMR4xbFxMA33HnvXJtStYR0gGY1vcb/6JGmhZcauGE7g5Vch+EOtZkiitloSkHgU5JXmQpTTggg==}
|
||||
'@formatjs/[email protected].3':
|
||||
resolution: {integrity: sha512-Hi/3S+Iru5RIYNkiTJwqCmERsNwNacu3k+anfCcp7Nzn9hxhCpAuhjr6I9Y6CVUs/1D3fGSgfnJTXGukot5aBg==}
|
||||
|
||||
'@formatjs/[email protected].4':
|
||||
resolution: {integrity: sha512-EeQ8YUs1QOpYLUiNHkHfR1rsOMtQUBQXEQwR9WmJRWfzvVXeYZ7vBeMARtlb2KuHUgdmfXjoin5oNn68hjyrEQ==}
|
||||
'@formatjs/[email protected].3':
|
||||
resolution: {integrity: sha512-moK0Ee0ddD8d8tZnoZ1JDUdNLBURXT5NMG4cnvYQY2bTYLphPhZk2KxgD78y6+lL69J4ihpQJleI+K5KlE39gw==}
|
||||
|
||||
'@formatjs/[email protected].3':
|
||||
resolution: {integrity: sha512-5nyIp4F/59A5hcfU6lZphSslpildVSirqudWfygBENGdGSHmggZVjhoad458XWEgtUd6d0JoiwsiUBFLfbqJ9Q==}
|
||||
'@formatjs/[email protected].2':
|
||||
resolution: {integrity: sha512-g7AVCP/0r49+h4R1qnQcNpq78JoEIthwp+QtHv9VkHw1c0bEegmhuz1t03DEr2u5nqP4HE//yAFjQtrv66b67A==}
|
||||
|
||||
'@formatjs/[email protected]':
|
||||
resolution: {integrity: sha512-+9TU+8tqsUqobFzQJEvfQ8Yukt1PYgL5WxYi/yW+hWVNx422wEo/TlVRLvIfdYqlVS048jfyu8piNagfhOV+2A==}
|
||||
@@ -2694,98 +2694,98 @@ packages:
|
||||
'@codemirror/state': ^6.0.0
|
||||
'@codemirror/view': ^6.0.0
|
||||
|
||||
'@rolldown/[email protected]6':
|
||||
resolution: {integrity: sha512-ia3DwAs0OsMGS7+IConptSiyk9pEXkOx/AbJ2n/lAGxH3QkWQus1t8d7gnAT38FCU2sQu8lUbeR6AiZFv4JxaA==}
|
||||
'@rolldown/[email protected]8':
|
||||
resolution: {integrity: sha512-ubTG+4KlidvioxjOFdsdEa2F6JVGZb8m2ByJUqQ7fYzUeT1cEn194POcsQ10PKH4p//q0YQn4SB3YpSuzcA1Yg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@rolldown/[email protected]6':
|
||||
resolution: {integrity: sha512-kx5wBiMB6E7TEvpl2B9gjLU004BkvEm2Dr941BFjx/SontPwSzNuQUIoVKxHHoSjpFIfHMmcguyLOtyn0hObRg==}
|
||||
'@rolldown/[email protected]8':
|
||||
resolution: {integrity: sha512-2Xa1t/JeFGTZn5TbzgPppcCftF8sCVnTNvrIvoSdcZhoFzAuVy1L3xxQUQ4sfWDtwVzg1u6Yi1R3SlJugRNV2Q==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@rolldown/[email protected]6':
|
||||
resolution: {integrity: sha512-qslZrXnKJU4cUpjoAmtAo/KQM9e2IRMlmpig6Zpwj+8KWxW8v+nkAf9216Bsek9lOC1jvg2yCjygCFo2f83msQ==}
|
||||
'@rolldown/[email protected]8':
|
||||
resolution: {integrity: sha512-iUzw9HshVvrWci9gVXjxWS7/M/IcLvNo67vu2c/w9zKBz4hIvPRixdUpIW/iCfhldLaq9ER2hecsI9UY5z0/yA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@rolldown/[email protected]6':
|
||||
resolution: {integrity: sha512-FdF+y48ND13eAyM2mQn+85kvAqNlvRWROy2rhgA463X/0Gv4vGtazZtfaF1mRBLscYvtOlh4QnLjOWiFzX69OQ==}
|
||||
'@rolldown/[email protected]8':
|
||||
resolution: {integrity: sha512-XuDHFzbFEtZl0QGJi92+fmG67tfnp1iZ9VOeDkwasGyTq2VG3/5PnJnvAr+MTzFh/rNScJ3VhN+XpZyyGsN7Jw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@rolldown/[email protected]6':
|
||||
resolution: {integrity: sha512-ddjqFM5e95AgLbUPtKIS7V1KTVlA0RUgCVr5mn+/IEhsdy3pr+Vuj/HZ7nlMN2FyxIwUktOH1LEPIQXO1v9y1Q==}
|
||||
'@rolldown/[email protected]8':
|
||||
resolution: {integrity: sha512-MM8opLfsLK15oEXDz6iBc0t3ahu+ew9jsAmha+ovZ33hSwYgqo7YmpRITqFTfW7xA71BLEQ3jJdTsaCmhB1c9w==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@rolldown/[email protected]6':
|
||||
resolution: {integrity: sha512-tHdKWbFkFBI687iim3LIyIvyL7j66VvQYkDPss5Stn/qyE2tWkR1YD0dXmXPFKKqT/o5ZEJVsaRsWS2meQwGOA==}
|
||||
'@rolldown/[email protected]8':
|
||||
resolution: {integrity: sha512-J6EoCWiy5RnFQzHrKyHXXevZ9lukQfGpnchX/zG/k/WC2nc9O9XyS1JgA8kI9VxMFggMbCPjpv7LcikriXfehw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@rolldown/[email protected]6':
|
||||
resolution: {integrity: sha512-FJiLGAblPBMlBEdbOqw6kW6QcXDWAf/oJu7gYs9ZJZwmg+K+/zjFLxPTIY9izJUzxzc24FHvmseyA0IvJSl17w==}
|
||||
'@rolldown/[email protected]8':
|
||||
resolution: {integrity: sha512-HIkFuLF7CdDeki+1aCZLLbLq0hk/PHntb+z4/YCTo47JbksTD6jJEELIz92tYROmDmvMrNqUMqBi71BkJLDR+A==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/[email protected]6':
|
||||
resolution: {integrity: sha512-hrJpblMsrQ58U9veWpbXzkhV5zStunds6HWLprqq1qauvgBr1E6P+24kLj6lSLI9vjRn/RYpjprhkHQPKGWpNg==}
|
||||
'@rolldown/[email protected]8':
|
||||
resolution: {integrity: sha512-4oqWItLLYt7IceOnElCct8T2ek5p7PmD7LN04TRJRXGo5f3HtKTgGH6PhYRFK4C2ICkq/LMUU0BVPvvlAKhm8g==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/[email protected]6':
|
||||
resolution: {integrity: sha512-qJd/dC6j6CwLG9/s/RP2nb1RZ0GIgj7Ioj7x/SYa3RzjVrlzrmH0fhIOpfvqQDeIsdP0loi+kHaMhJzY1PBIaA==}
|
||||
'@rolldown/[email protected]8':
|
||||
resolution: {integrity: sha512-+AkRly5SrCEgnPhGRfub7pucASYEC2fTsxZg7VCcnozpA2+Wn/gCYy1t7LuJS67/xyJu/7noYBJncDUkbwgw2A==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/[email protected]6':
|
||||
resolution: {integrity: sha512-Vx7Xg5uUJuNcujJjI70OICaH0EYj80BtOLZTSN7uOYg8QOLlyKnaG0IW7Fn/J0IpJsaKzmDKMhOQv1JBM9m9vg==}
|
||||
'@rolldown/[email protected]8':
|
||||
resolution: {integrity: sha512-Ttg1r9vVykMnQrE/GwYO9icKtXiuiMfRe/w74xJkeoVuvhRG+WtT6LEMOf58Yl63yd77lhHzO8Y55R8CC3XALA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/[email protected]6':
|
||||
resolution: {integrity: sha512-TNKhgE3uJTsTzatUSSYt0Kgo+Oa2uSlSjz4r1aEIea7HX6ULBym4fAlYUhG1i6pjNf2njfCUAfKwr6C3E9Xg1w==}
|
||||
'@rolldown/[email protected]8':
|
||||
resolution: {integrity: sha512-xX1Tt4o5jUMma0vJsXp5eRZA9nOuyigb2dKJMMzZRmzMxLYGE9PaKKSqHPzPvSQgM8ZOEacSTQwDQcwWfh0xmQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rolldown/[email protected]6':
|
||||
resolution: {integrity: sha512-7chXdpjXZ8qG0+mIxECN3cT9kvhZ3/oZhMX6Pp876P7CDrIy8nJswva6Mq357JR7eYXfuskwZxMeV1SzoZdHrA==}
|
||||
'@rolldown/[email protected]8':
|
||||
resolution: {integrity: sha512-4VVlGI5K2kociZlW3lSN8VjG3TW5yQIlAcfCdBXMgGjJxZll15Fa4ocRHXHvIVVnwlibCw/NQajJrNDiqqpiiw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rolldown/[email protected]6':
|
||||
resolution: {integrity: sha512-Kwjt+7uNTJK5/MDkGWGVO2KQPnj8yAV75dMpfwyHZrJSXWHrniOB0aUEWlVYf3H1hOqFLIStLdM5QXq6bnKmOQ==}
|
||||
'@rolldown/[email protected]8':
|
||||
resolution: {integrity: sha512-PApTsgH1jLYuyy3tSCz9De82Gq5qXwi7vImVZhEzTgX1C4150Cp5M2ZfL3pLvbCvbEHL0oJeC6pd6podtv8Y5w==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [openharmony]
|
||||
|
||||
'@rolldown/[email protected]6':
|
||||
resolution: {integrity: sha512-/LH6WfjDRXXJz/p5xl4TmU+jpcnwcxbKEQBW/GZE0JHfN0lGIyTtsUEGaQ/q/GikkGJ2yLKe6pwm7AnEWO8pdw==}
|
||||
'@rolldown/[email protected]8':
|
||||
resolution: {integrity: sha512-J2XjWjx/SP+4x0H4r6dz3Vzb5WVkg7re40Do9K7HJ3sK2/xcmZrWwakXlSwaMvOkvruLJCD4CL/3wqsIR/thgg==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@rolldown/[email protected]6':
|
||||
resolution: {integrity: sha512-1lv1KvX0Z871zfk7vw+dnXMMnHNyDwMzWG8Kv3HxDXMOhzCX4SHc7HK319fInP0i+0p9D8brkfzL7pJOaGl+tw==}
|
||||
'@rolldown/[email protected]8':
|
||||
resolution: {integrity: sha512-XwRJxRQvkuEtS0hIIt29QrqPJ+JzMJSNYHJw4ae/mc6uEarfRG1ceyg4HmmWiBdL/YCb6g7N5BLbSvmRJRYZHw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
@@ -4994,8 +4994,8 @@ packages:
|
||||
eslint-plugin-import: '>=1.4.0'
|
||||
webpack: '>=1.11.0'
|
||||
|
||||
[email protected].0:
|
||||
resolution: {integrity: sha512-UfxHFmV+f9vwybrNn6LQ1rPzfeDMPGi/dRbqQnCFO/jBzYrujSOyX7jxqBSYr/HwXs/V4XJdEGPvC3Re46nzKg==}
|
||||
[email protected].1:
|
||||
resolution: {integrity: sha512-D1h++38NS2Z40zf8jbd2OHqDrnIFRKpdp8sL0jP4xSDGZj6MNwEQua8F+r3tOWjcHrsWuhHOJ7wASWioJ31tfg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
'@typescript-eslint/utils': ^8.56.0
|
||||
@@ -5602,8 +5602,8 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==}
|
||||
|
||||
[email protected].4:
|
||||
resolution: {integrity: sha512-JUkmv3cAEyijN3oiRiV3ctel7yVMuUCgfh7Pu8pj1/z6L1DSEPRyhuMaLjVw3pGmGJ5RDH54WVFxfMHZWTwyHw==}
|
||||
[email protected].5:
|
||||
resolution: {integrity: sha512-quhcDFAxy4wr/GrgWgmMjuegoedNUIxGNLORfXgDgdjuv8cBM88ClLTLXmfYC1roMSR1PPc55yUEkpJ5m67W2g==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w==}
|
||||
@@ -6945,8 +6945,8 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-OlwfYEgA2RdboZohpldlvJ1xngOins5d7ejqnIBWr9KaMxsnBqotpptRXTyfNRLnFpqzX6sTDt+X+a+6udnU8g==}
|
||||
|
||||
[email protected]6:
|
||||
resolution: {integrity: sha512-bWvlMpFKiTfDKfnK7FC2MPgPeWCf/Kpwsq49ApdW+FQaBZb6O6dZ4MAPbPUjp7hYQrw/RobqCzn64BRrQecGHw==}
|
||||
[email protected]8:
|
||||
resolution: {integrity: sha512-C1uuT3UR60SShuCaPQFAAZyefJCWZlAKB02BV3BuPE2Rs4fevRQltEKoFO2tV5W9/eJhU5RieV6vJj3euulfNQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
|
||||
@@ -7754,8 +7754,8 @@ packages:
|
||||
peerDependencies:
|
||||
vite: '*'
|
||||
|
||||
[email protected].6:
|
||||
resolution: {integrity: sha512-UI7dK29NnS54Fk+mmo7o+jI6U8Fp7hDl+X5Q1k45WH+d4dSlbLVUETGQ+hfkNgvLQGyoElPj837nZyv4MBc0VA==}
|
||||
[email protected].7:
|
||||
resolution: {integrity: sha512-oxh0dMxrayBr33ScJvvBJ2HBQ2jc1TBrXKR2NyL/MJ9WPuX19mN2DXWUmywKdyw5RFVQBo8hmtb2FMC0LHrnng==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
@@ -9781,23 +9781,23 @@ snapshots:
|
||||
'@formatjs/icu-skeleton-parser': 1.8.14
|
||||
tslib: 2.9.1
|
||||
|
||||
'@formatjs/[email protected]2':
|
||||
'@formatjs/[email protected]3':
|
||||
dependencies:
|
||||
'@formatjs/icu-skeleton-parser': 2.1.12
|
||||
'@formatjs/icu-skeleton-parser': 2.1.13
|
||||
|
||||
'@formatjs/[email protected]':
|
||||
dependencies:
|
||||
'@formatjs/ecma402-abstract': 2.3.4
|
||||
tslib: 2.9.1
|
||||
|
||||
'@formatjs/[email protected]2': {}
|
||||
'@formatjs/[email protected]3': {}
|
||||
|
||||
'@formatjs/[email protected]':
|
||||
dependencies:
|
||||
'@formatjs/bigdecimal': 0.2.10
|
||||
'@formatjs/intl-localematcher': 0.10.1
|
||||
|
||||
'@formatjs/[email protected].3':
|
||||
'@formatjs/[email protected].2':
|
||||
dependencies:
|
||||
'@formatjs/intl-localematcher': 0.10.1
|
||||
|
||||
@@ -9808,7 +9808,7 @@ snapshots:
|
||||
|
||||
'@formatjs/[email protected]': {}
|
||||
|
||||
'@formatjs/[email protected].3':
|
||||
'@formatjs/[email protected].2':
|
||||
dependencies:
|
||||
'@formatjs/intl-localematcher': 0.10.1
|
||||
|
||||
@@ -9825,17 +9825,17 @@ snapshots:
|
||||
dependencies:
|
||||
tslib: 2.9.1
|
||||
|
||||
'@formatjs/[email protected].4':
|
||||
'@formatjs/[email protected].3':
|
||||
dependencies:
|
||||
'@formatjs/bigdecimal': 0.2.10
|
||||
'@formatjs/intl-localematcher': 0.10.1
|
||||
|
||||
'@formatjs/[email protected].4':
|
||||
'@formatjs/[email protected].3':
|
||||
dependencies:
|
||||
'@formatjs/bigdecimal': 0.2.10
|
||||
'@formatjs/intl-localematcher': 0.10.1
|
||||
|
||||
'@formatjs/[email protected].3':
|
||||
'@formatjs/[email protected].2':
|
||||
dependencies:
|
||||
'@formatjs/intl-localematcher': 0.10.1
|
||||
|
||||
@@ -10658,49 +10658,49 @@ snapshots:
|
||||
'@codemirror/state': 6.7.6
|
||||
'@codemirror/view': 6.43.15
|
||||
|
||||
'@rolldown/[email protected]6':
|
||||
'@rolldown/[email protected]8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]6':
|
||||
'@rolldown/[email protected]8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]6':
|
||||
'@rolldown/[email protected]8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]6':
|
||||
'@rolldown/[email protected]8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]6':
|
||||
'@rolldown/[email protected]8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]6':
|
||||
'@rolldown/[email protected]8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]6':
|
||||
'@rolldown/[email protected]8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]6':
|
||||
'@rolldown/[email protected]8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]6':
|
||||
'@rolldown/[email protected]8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]6':
|
||||
'@rolldown/[email protected]8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]6':
|
||||
'@rolldown/[email protected]8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]6':
|
||||
'@rolldown/[email protected]8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]6':
|
||||
'@rolldown/[email protected]8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]6':
|
||||
'@rolldown/[email protected]8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]6':
|
||||
'@rolldown/[email protected]8':
|
||||
optional: true
|
||||
|
||||
'@rolldown/[email protected]': {}
|
||||
@@ -11697,7 +11697,7 @@ snapshots:
|
||||
obug: 3.0.0
|
||||
std-env: 4.3.0
|
||||
tinyrainbow: 3.2.0
|
||||
vitest: 5.1.0(@types/[email protected])(@vitest/[email protected])([email protected])([email protected].6(@types/[email protected])([email protected])([email protected]))
|
||||
vitest: 5.1.0(@types/[email protected])(@vitest/[email protected])([email protected])([email protected].7(@types/[email protected])([email protected])([email protected]))
|
||||
|
||||
'@vitest/[email protected]': {}
|
||||
|
||||
@@ -11705,14 +11705,14 @@ snapshots:
|
||||
dependencies:
|
||||
'@vitest/istanbul-lib-coverage': 2.1.0
|
||||
|
||||
'@vitest/[email protected]([email protected].6(@types/[email protected])([email protected])([email protected]))':
|
||||
'@vitest/[email protected]([email protected].7(@types/[email protected])([email protected])([email protected]))':
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
'@vitest/spy': 5.1.0
|
||||
estree-walker: 3.0.3
|
||||
magic-string: 1.4.3
|
||||
optionalDependencies:
|
||||
vite: 8.3.6(@types/[email protected])([email protected])([email protected])
|
||||
vite: 8.3.7(@types/[email protected])([email protected])([email protected])
|
||||
|
||||
'@vitest/[email protected]': {}
|
||||
|
||||
@@ -12823,7 +12823,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
[email protected].0(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected]))([email protected]):
|
||||
[email protected].1(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected]))([email protected]):
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.72.1
|
||||
cjs-module-lexer: 2.3.0
|
||||
@@ -13543,10 +13543,10 @@ snapshots:
|
||||
'@formatjs/icu-messageformat-parser': 2.11.2
|
||||
tslib: 2.9.1
|
||||
|
||||
[email protected].4:
|
||||
[email protected].5:
|
||||
dependencies:
|
||||
'@formatjs/fast-memoize': 3.1.7
|
||||
'@formatjs/icu-messageformat-parser': 3.5.22
|
||||
'@formatjs/icu-messageformat-parser': 3.5.23
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
@@ -14942,26 +14942,26 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]6:
|
||||
[email protected]8:
|
||||
dependencies:
|
||||
'@oxc-project/types': 0.156.0
|
||||
'@rolldown/pluginutils': 1.0.1
|
||||
optionalDependencies:
|
||||
'@rolldown/binding-android-arm-eabi': 1.2.16
|
||||
'@rolldown/binding-android-arm64': 1.2.16
|
||||
'@rolldown/binding-darwin-arm64': 1.2.16
|
||||
'@rolldown/binding-darwin-x64': 1.2.16
|
||||
'@rolldown/binding-freebsd-x64': 1.2.16
|
||||
'@rolldown/binding-linux-arm-gnueabihf': 1.2.16
|
||||
'@rolldown/binding-linux-arm64-gnu': 1.2.16
|
||||
'@rolldown/binding-linux-arm64-musl': 1.2.16
|
||||
'@rolldown/binding-linux-ppc64-gnu': 1.2.16
|
||||
'@rolldown/binding-linux-s390x-gnu': 1.2.16
|
||||
'@rolldown/binding-linux-x64-gnu': 1.2.16
|
||||
'@rolldown/binding-linux-x64-musl': 1.2.16
|
||||
'@rolldown/binding-openharmony-arm64': 1.2.16
|
||||
'@rolldown/binding-win32-arm64-msvc': 1.2.16
|
||||
'@rolldown/binding-win32-x64-msvc': 1.2.16
|
||||
'@rolldown/binding-android-arm-eabi': 1.2.18
|
||||
'@rolldown/binding-android-arm64': 1.2.18
|
||||
'@rolldown/binding-darwin-arm64': 1.2.18
|
||||
'@rolldown/binding-darwin-x64': 1.2.18
|
||||
'@rolldown/binding-freebsd-x64': 1.2.18
|
||||
'@rolldown/binding-linux-arm-gnueabihf': 1.2.18
|
||||
'@rolldown/binding-linux-arm64-gnu': 1.2.18
|
||||
'@rolldown/binding-linux-arm64-musl': 1.2.18
|
||||
'@rolldown/binding-linux-ppc64-gnu': 1.2.18
|
||||
'@rolldown/binding-linux-s390x-gnu': 1.2.18
|
||||
'@rolldown/binding-linux-x64-gnu': 1.2.18
|
||||
'@rolldown/binding-linux-x64-musl': 1.2.18
|
||||
'@rolldown/binding-openharmony-arm64': 1.2.18
|
||||
'@rolldown/binding-win32-arm64-msvc': 1.2.18
|
||||
'@rolldown/binding-win32-x64-msvc': 1.2.18
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
@@ -15909,22 +15909,22 @@ snapshots:
|
||||
- bare-abort-controller
|
||||
- react-native-b4a
|
||||
|
||||
[email protected]([email protected])([email protected])([email protected].6(@types/[email protected])([email protected])([email protected])):
|
||||
[email protected]([email protected])([email protected])([email protected].7(@types/[email protected])([email protected])([email protected])):
|
||||
dependencies:
|
||||
debug: 4.4.3([email protected])
|
||||
globrex: 0.1.2
|
||||
tsconfck: 3.1.6([email protected])
|
||||
vite: 8.3.6(@types/[email protected])([email protected])([email protected])
|
||||
vite: 8.3.7(@types/[email protected])([email protected])([email protected])
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
[email protected].6(@types/[email protected])([email protected])([email protected]):
|
||||
[email protected].7(@types/[email protected])([email protected])([email protected]):
|
||||
dependencies:
|
||||
lightningcss: 1.33.0
|
||||
picomatch: 4.0.7
|
||||
postcss: 8.5.32
|
||||
rolldown: 1.2.16
|
||||
rolldown: 1.2.18
|
||||
tinyglobby: 0.2.17
|
||||
optionalDependencies:
|
||||
'@types/node': 24.19.2
|
||||
@@ -15932,10 +15932,10 @@ snapshots:
|
||||
terser: 5.50.0
|
||||
yaml: 2.9.1
|
||||
|
||||
[email protected](@types/[email protected])(@vitest/[email protected])([email protected])([email protected].6(@types/[email protected])([email protected])([email protected])):
|
||||
[email protected](@types/[email protected])(@vitest/[email protected])([email protected])([email protected].7(@types/[email protected])([email protected])([email protected])):
|
||||
dependencies:
|
||||
'@types/chai': 5.2.3
|
||||
'@vitest/mocker': 5.1.0([email protected].6(@types/[email protected])([email protected])([email protected]))
|
||||
'@vitest/mocker': 5.1.0([email protected].7(@types/[email protected])([email protected])([email protected]))
|
||||
chai: 6.3.0
|
||||
es-module-lexer: 2.3.2
|
||||
expect-type: 1.4.0
|
||||
@@ -15946,7 +15946,7 @@ snapshots:
|
||||
tinybench: 6.2.2
|
||||
tinyexec: 1.3.1
|
||||
tinyglobby: 0.2.17
|
||||
vite: 8.3.6(@types/[email protected])([email protected])([email protected])
|
||||
vite: 8.3.7(@types/[email protected])([email protected])([email protected])
|
||||
why-is-node-running: 3.2.1
|
||||
optionalDependencies:
|
||||
'@types/node': 24.19.2
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import type { IcePath } from "./limit-ice-servers";
|
||||
|
||||
// The fields of the transport, candidate pair and local candidate stats
|
||||
interface IceStats {
|
||||
id: string;
|
||||
type: string;
|
||||
selectedCandidatePairId?: string;
|
||||
selected?: boolean;
|
||||
localCandidateId?: string;
|
||||
candidateType?: string;
|
||||
relayProtocol?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the network path a connected peer connection uses. Returns
|
||||
* `undefined` if the browser doesn't report it.
|
||||
*/
|
||||
export const getIcePath = async (
|
||||
peerConnection: RTCPeerConnection
|
||||
): Promise<IcePath | undefined> => {
|
||||
const stats: Record<string, IceStats> = {};
|
||||
try {
|
||||
(await peerConnection.getStats()).forEach((report: IceStats) => {
|
||||
stats[report.id] = report;
|
||||
});
|
||||
} catch (_err: unknown) {
|
||||
return undefined;
|
||||
}
|
||||
const reports = Object.values(stats);
|
||||
const pairId = reports.find(
|
||||
(report) => report.type === "transport" && report.selectedCandidatePairId
|
||||
)?.selectedCandidatePairId;
|
||||
const pair = pairId
|
||||
? stats[pairId]
|
||||
: // Firefox has no transport stats
|
||||
reports.find(
|
||||
(report) => report.type === "candidate-pair" && report.selected
|
||||
);
|
||||
const candidate = pair?.localCandidateId
|
||||
? stats[pair.localCandidateId]
|
||||
: undefined;
|
||||
if (!candidate?.candidateType) {
|
||||
return undefined;
|
||||
}
|
||||
if (candidate.url?.startsWith("turn")) {
|
||||
return { relayUrl: candidate.url };
|
||||
}
|
||||
if (candidate.candidateType === "relay" || candidate.relayProtocol) {
|
||||
// Not all browsers report which TURN URL a relayed candidate came from
|
||||
return undefined;
|
||||
}
|
||||
return {};
|
||||
};
|
||||
@@ -1,103 +0,0 @@
|
||||
import { ensureArray } from "../array/ensure-array";
|
||||
|
||||
const isTurn = (url: string) => url.startsWith("turn");
|
||||
|
||||
const isTcpTurn = (url: string) =>
|
||||
url.startsWith("turns:") || url.includes("transport=tcp");
|
||||
|
||||
const isTls443 = (url: string) =>
|
||||
url.startsWith("turns:") && /:443(\?|$)/.test(url);
|
||||
|
||||
// "turn:host:3478?transport=udp" -> "host"
|
||||
const getHost = (url: string) =>
|
||||
url
|
||||
.replace(/^[a-z]+:/i, "")
|
||||
.replace(/\?.*$/, "")
|
||||
.replace(/:\d+$/, "")
|
||||
.toLowerCase();
|
||||
|
||||
/**
|
||||
* Reduces the URLs of every ICE server to one STUN URL, one UDP TURN URL and
|
||||
* one TCP TURN URL (preferring TLS on port 443, which passes most firewalls)
|
||||
* per host.
|
||||
*
|
||||
* The browser gathers a candidate for every URL on every network interface and
|
||||
* checks each of them against every remote candidate. Redundant URLs of the
|
||||
* same host do not add a new network path, but multiply the number of checks
|
||||
* and delay the connection by many seconds.
|
||||
*/
|
||||
export const limitIceServers = (
|
||||
iceServers: RTCIceServer[] | undefined
|
||||
): RTCIceServer[] | undefined =>
|
||||
iceServers?.map((server) => {
|
||||
const urls = ensureArray(server.urls);
|
||||
if (urls.length < 2) {
|
||||
return server;
|
||||
}
|
||||
const hosts = [...new Set(urls.map(getHost))];
|
||||
return {
|
||||
...server,
|
||||
urls: hosts.flatMap((host) => {
|
||||
const hostUrls = urls.filter((url) => getHost(url) === host);
|
||||
const turnUrls = hostUrls.filter(isTurn);
|
||||
const tcpTurnUrls = turnUrls.filter(isTcpTurn);
|
||||
return [
|
||||
hostUrls.find((url) => !isTurn(url)),
|
||||
turnUrls.find((url) => !isTcpTurn(url)),
|
||||
tcpTurnUrls.find(isTls443) ??
|
||||
tcpTurnUrls.find((url) => url.startsWith("turns:")) ??
|
||||
tcpTurnUrls[0],
|
||||
].filter((url) => url !== undefined);
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
// "turns:host" -> "turns:host:5349?transport=tcp", browsers report the URL of
|
||||
// a candidate with the port and transport that were left out
|
||||
const normalizeUrl = (url: string) => {
|
||||
const scheme = url.slice(0, url.indexOf(":")).toLowerCase();
|
||||
const port =
|
||||
/:(\d+)(\?|$)/.exec(url)?.[1] ?? (scheme.endsWith("s") ? "5349" : "3478");
|
||||
const transport =
|
||||
/[?&]transport=(\w+)/i.exec(url)?.[1].toLowerCase() ??
|
||||
(scheme === "turns" ? "tcp" : "udp");
|
||||
return `${scheme}:${getHost(url)}:${port}?transport=${transport}`;
|
||||
};
|
||||
|
||||
/** The network path a connection uses, without `relayUrl` it is direct. */
|
||||
export interface IcePath {
|
||||
relayUrl?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces the ICE servers to what a new connection needs to take the same
|
||||
* path as a connection that is already running: only the TURN URL it relays
|
||||
* over, or no TCP TURN URLs when it is direct. The UDP TURN URLs stay, as
|
||||
* browsers also use them to find their public address.
|
||||
*
|
||||
* Returns `undefined` if the relay is not part of the ICE servers.
|
||||
*/
|
||||
export const limitIceServersToPath = (
|
||||
iceServers: RTCIceServer[] | undefined,
|
||||
path: IcePath
|
||||
): RTCIceServer[] | undefined => {
|
||||
const { relayUrl } = path;
|
||||
if (relayUrl) {
|
||||
const normalizedUrl = normalizeUrl(relayUrl);
|
||||
for (const server of iceServers ?? []) {
|
||||
const url = ensureArray(server.urls).find(
|
||||
(serverUrl) => normalizeUrl(serverUrl) === normalizedUrl
|
||||
);
|
||||
if (url) {
|
||||
return [{ ...server, urls: [url] }];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
return limitIceServers(iceServers)
|
||||
?.map((server) => ({
|
||||
...server,
|
||||
urls: ensureArray(server.urls).filter((url) => !isTcpTurn(url)),
|
||||
}))
|
||||
.filter((server) => server.urls.length);
|
||||
};
|
||||
@@ -7,7 +7,10 @@ import {
|
||||
mdiRestart,
|
||||
} from "@mdi/js";
|
||||
import { differenceInMinutes } from "date-fns";
|
||||
import type { DataZoomComponentOption } from "echarts/components";
|
||||
import type {
|
||||
DataZoomComponentOption,
|
||||
MarkAreaComponentOption,
|
||||
} from "echarts/components";
|
||||
import type { EChartsType } from "echarts/core";
|
||||
import type {
|
||||
ECElementEvent,
|
||||
@@ -90,14 +93,29 @@ export const DEFAULT_CHART_WIDTH = 500;
|
||||
// is short enough for a whole screenful; on a desktop that would cover the page.
|
||||
const VISIBILITY_ROOT_MARGIN_NARROW = "100%";
|
||||
const VISIBILITY_ROOT_MARGIN = "300px";
|
||||
const CLICK_DRAG_TOLERANCE = 4;
|
||||
// The selection is shown by the mask, not by the brush
|
||||
const BRUSH_STYLE = {
|
||||
color: "rgba(0, 0, 0, 0)",
|
||||
borderColor: "rgba(0, 0, 0, 0)",
|
||||
};
|
||||
|
||||
export type ZoomSliderMode = "auto" | "always" | "never";
|
||||
|
||||
export type ChartSelectMode = "none" | "point" | "range";
|
||||
const DEFERRED_PROPS = [
|
||||
"options",
|
||||
"data",
|
||||
"_hiddenDatasets",
|
||||
"_isZoomed",
|
||||
"highlight",
|
||||
] as const;
|
||||
const MASK_OPACITY = 0.7;
|
||||
|
||||
export interface ChartTimeRange {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
type RawSeriesOption = Exclude<
|
||||
NonNullable<ECOption["series"]>,
|
||||
@@ -176,6 +194,18 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
@property({ attribute: "grow-for-zoom-slider", type: Boolean })
|
||||
public growForZoomSlider = false;
|
||||
|
||||
// "point" selects on click, "range" also selects on drag instead of zooming
|
||||
@property({ attribute: "select-mode" })
|
||||
public selectMode: ChartSelectMode = "none";
|
||||
|
||||
// Everything outside this time range fades
|
||||
@property({ attribute: false }) public highlight?: ChartTimeRange;
|
||||
|
||||
// Rounds a selection to the periods of the data, or drops it
|
||||
@property({ attribute: false }) public snapSelection?: (
|
||||
range: ChartTimeRange
|
||||
) => ChartTimeRange | undefined;
|
||||
|
||||
// extraComponents is not reactive and should not trigger updates
|
||||
public extraComponents?: any[];
|
||||
|
||||
@@ -201,6 +231,14 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
|
||||
@state() private _isZoomed = false;
|
||||
|
||||
private _pointerDownX?: number;
|
||||
|
||||
private _selectingRange?: ChartTimeRange;
|
||||
|
||||
private _maskColor = "";
|
||||
|
||||
private _maskShown = false;
|
||||
|
||||
// Separate from _isZoomed, whose touch handling reruns graph layouts
|
||||
@state() private _isGraphRoamed = false;
|
||||
|
||||
@@ -454,6 +492,9 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
if (changedProps.has("selectMode")) {
|
||||
this._updateSelectMode();
|
||||
}
|
||||
const themeChanged = changedProps.has("_themes") && this.hasUpdated;
|
||||
if (!themeChanged && !DEFERRED_PROPS.some((p) => changedProps.has(p))) {
|
||||
return;
|
||||
@@ -499,6 +540,8 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
) {
|
||||
this._disposeSonification();
|
||||
}
|
||||
} else if (changedProps.has("highlight")) {
|
||||
this._updateMask();
|
||||
}
|
||||
if (changedProps.has("options")) {
|
||||
chartOptions = { ...chartOptions, ...this._createOptions() };
|
||||
@@ -888,6 +931,9 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
|
||||
const style = getComputedStyle(this);
|
||||
echarts.registerTheme("custom", this._createTheme(style));
|
||||
this._maskColor =
|
||||
style.getPropertyValue("--card-background-color").trim() ||
|
||||
style.getPropertyValue("--primary-background-color").trim();
|
||||
|
||||
this.chart = echarts.init(this._chartContainer!, "custom");
|
||||
this._dragToZoom = false;
|
||||
@@ -970,6 +1016,9 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
this._legendItemCount = this._getLegendItems()?.length;
|
||||
this._updateSankeyRoam();
|
||||
this._updateDragToZoom();
|
||||
if (this.selectMode !== "none") {
|
||||
this._updateSelectMode();
|
||||
}
|
||||
if (zoomStart !== 0 || zoomEnd !== 100) {
|
||||
this.zoom(zoomStart, zoomEnd, true);
|
||||
}
|
||||
@@ -1021,7 +1070,10 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
|
||||
// A chart can switch type (e.g. pie and bar) without being rebuilt
|
||||
private _updateDragToZoom() {
|
||||
const active = !this._touchInput && this._hasZoomableXAxis();
|
||||
const active =
|
||||
!this._touchInput &&
|
||||
this._hasZoomableXAxis() &&
|
||||
this.selectMode !== "range";
|
||||
if (!this.chart || active === this._dragToZoom) {
|
||||
return;
|
||||
}
|
||||
@@ -1033,6 +1085,127 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private _snapRange(range: ChartTimeRange) {
|
||||
return this.snapSelection ? this.snapSelection(range) : range;
|
||||
}
|
||||
|
||||
private _getMaskArea(): MarkAreaComponentOption | undefined {
|
||||
const range = this._selectingRange ?? this.highlight;
|
||||
if (!range) {
|
||||
return undefined;
|
||||
}
|
||||
this._maskShown = true;
|
||||
return {
|
||||
silent: true,
|
||||
z: 10,
|
||||
itemStyle: {
|
||||
color: this._maskColor,
|
||||
opacity: MASK_OPACITY,
|
||||
},
|
||||
data: [
|
||||
[{ xAxis: -Infinity }, { xAxis: range.start }],
|
||||
[{ xAxis: range.end }, { xAxis: Infinity }],
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Only the first series carries the mask, merged in without resending data
|
||||
private _updateMask() {
|
||||
this.chart?.setOption({
|
||||
series: [{ markArea: this._getMaskArea() ?? { data: [] } }],
|
||||
});
|
||||
}
|
||||
|
||||
private _handleSelectPointerDown = (e: ECElementEvent) => {
|
||||
this._pointerDownX = e.offsetX;
|
||||
};
|
||||
|
||||
private _handleSelectClick = (e: ECElementEvent) => {
|
||||
const point = [e.offsetX, e.offsetY];
|
||||
// A drag that ends on the plot is not a click
|
||||
const dragged =
|
||||
this._pointerDownX !== undefined &&
|
||||
Math.abs(e.offsetX - this._pointerDownX) > CLICK_DRAG_TOLERANCE;
|
||||
if (dragged || !this.chart?.containPixel("grid", point)) {
|
||||
return;
|
||||
}
|
||||
const [x] = this.chart.convertFromPixel("grid", point) as number[];
|
||||
const selection = this._snapRange({ start: x, end: x });
|
||||
if (selection) {
|
||||
fireEvent(this, "chart-select", selection);
|
||||
}
|
||||
};
|
||||
|
||||
private _handleBrush = (e: any) => {
|
||||
const range = e.areas?.[0]?.coordRange as number[] | undefined;
|
||||
if (range) {
|
||||
this._selectingRange = this._snapRange({
|
||||
start: range[0],
|
||||
end: range[1],
|
||||
});
|
||||
this._updateMask();
|
||||
}
|
||||
};
|
||||
|
||||
private _handleBrushEnd = (e: any) => {
|
||||
const range = e.areas?.[0]?.coordRange as number[] | undefined;
|
||||
this.chart?.dispatchAction({ type: "brush", areas: [] });
|
||||
this._selectingRange = undefined;
|
||||
this._updateMask();
|
||||
const selection =
|
||||
range && this._snapRange({ start: range[0], end: range[1] });
|
||||
if (selection) {
|
||||
fireEvent(this, "chart-select", selection);
|
||||
}
|
||||
};
|
||||
|
||||
private _updateSelectMode() {
|
||||
if (!this.chart) {
|
||||
return;
|
||||
}
|
||||
const zr = this.chart.getZr();
|
||||
zr.off("mousedown", this._handleSelectPointerDown);
|
||||
zr.off("click", this._handleSelectClick);
|
||||
this.chart.off("brush", this._handleBrush);
|
||||
this.chart.off("brushEnd", this._handleBrushEnd);
|
||||
if (this.selectMode !== "none") {
|
||||
zr.on("mousedown", this._handleSelectPointerDown);
|
||||
zr.on("click", this._handleSelectClick);
|
||||
}
|
||||
const selectRange = this.selectMode === "range";
|
||||
if (selectRange) {
|
||||
this.chart.on("brush", this._handleBrush);
|
||||
this.chart.on("brushEnd", this._handleBrushEnd);
|
||||
this.chart.setOption({
|
||||
brush: {
|
||||
xAxisIndex: 0,
|
||||
// Only measures the range, the mask fades what is outside
|
||||
seriesIndex: [],
|
||||
transformable: false,
|
||||
brushStyle: BRUSH_STYLE,
|
||||
},
|
||||
});
|
||||
}
|
||||
const brushCursor = {
|
||||
type: "takeGlobalCursor",
|
||||
key: "brush",
|
||||
brushOption: {
|
||||
brushType: selectRange ? "lineX" : false,
|
||||
brushMode: "single",
|
||||
brushStyle: BRUSH_STYLE,
|
||||
},
|
||||
};
|
||||
// Brush and drag to zoom share the global cursor, the last one taken wins
|
||||
if (selectRange) {
|
||||
this._updateDragToZoom();
|
||||
this.chart.dispatchAction(brushCursor);
|
||||
} else {
|
||||
this.chart.dispatchAction(brushCursor);
|
||||
this._dragToZoom = false;
|
||||
this._updateDragToZoom();
|
||||
}
|
||||
}
|
||||
|
||||
private _hasZoomableXAxis() {
|
||||
const xAxis = ensureArray(this.options?.xAxis)?.[0] as
|
||||
XAXisOption | undefined;
|
||||
@@ -1475,7 +1648,8 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
private _getSeries() {
|
||||
const xAxis = (this.options?.xAxis?.[0] ?? this.options?.xAxis) as
|
||||
XAXisOption | undefined;
|
||||
const series = ensureArray(this.data).map((s) => {
|
||||
const maskArea = this._getMaskArea();
|
||||
const series = ensureArray(this.data).map((s, index) => {
|
||||
const data = this._hiddenDatasets.has(String(s.id ?? s.name))
|
||||
? undefined
|
||||
: s.data;
|
||||
@@ -1513,6 +1687,16 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
} as HaECSeriesItem;
|
||||
}
|
||||
}
|
||||
// ECharts merges series by id and would keep a mask left out here
|
||||
if (this._maskShown && (s.type === "line" || s.type === "bar")) {
|
||||
result = {
|
||||
...result,
|
||||
markArea:
|
||||
index === 0 && maskArea
|
||||
? maskArea
|
||||
: ((s as LineSeriesOption).markArea ?? { data: [] }),
|
||||
} as HaECSeriesItem;
|
||||
}
|
||||
return processSeriesTooltipFormatter(result);
|
||||
});
|
||||
return series as ECOption["series"];
|
||||
@@ -1762,7 +1946,7 @@ export class HaChartBase extends MobileAwareMixin(LitElement) {
|
||||
// it, so a quick drag out would lose the release and its selection
|
||||
if (
|
||||
ev.type === "pointerdown" &&
|
||||
this._dragToZoom &&
|
||||
(this._dragToZoom || this.selectMode === "range") &&
|
||||
this._isZoomPress(ev)
|
||||
) {
|
||||
this.chart
|
||||
@@ -2337,6 +2521,7 @@ declare global {
|
||||
"dataset-hidden": { id: string };
|
||||
"dataset-unhidden": { id: string };
|
||||
"chart-click": ECElementEvent;
|
||||
"chart-select": ChartTimeRange;
|
||||
"legend-label-click": { id: string };
|
||||
"chart-zoom": {
|
||||
start: number;
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
import type { HassConfig, HassEntities } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues, TemplateResult } 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 { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
||||
@@ -49,7 +49,13 @@ import type {
|
||||
HomeAssistantUI,
|
||||
} from "../../types";
|
||||
import { getPeriodicAxisLabelConfig } from "./axis-label";
|
||||
import type { CustomLegendOption } from "./ha-chart-base";
|
||||
import type {
|
||||
ChartSelectMode,
|
||||
ChartTimeRange,
|
||||
CustomLegendOption,
|
||||
HaChartBase,
|
||||
ZoomSliderMode,
|
||||
} from "./ha-chart-base";
|
||||
import "./ha-chart-base";
|
||||
import { sideTooltipPosition } from "./chart-tooltip-position";
|
||||
import "./ha-chart-tooltip-marker";
|
||||
@@ -128,6 +134,16 @@ export class StatisticsChart extends LitElement {
|
||||
@property({ attribute: "expand-legend", type: Boolean })
|
||||
public expandLegend?: boolean;
|
||||
|
||||
@property({ attribute: "select-mode" })
|
||||
public selectMode: ChartSelectMode = "none";
|
||||
|
||||
@property({ attribute: "zoom-slider" })
|
||||
public zoomSlider: ZoomSliderMode = "auto";
|
||||
|
||||
@property({ attribute: false }) public highlight?: ChartTimeRange;
|
||||
|
||||
@query("ha-chart-base") private _chartBase?: HaChartBase;
|
||||
|
||||
@state() private _chartData: (LineSeriesOption | BarSeriesOption)[] = [];
|
||||
|
||||
@state() private _legendData: CustomLegendOption["data"];
|
||||
@@ -244,8 +260,12 @@ export class StatisticsChart extends LitElement {
|
||||
return html`
|
||||
<ha-chart-base
|
||||
.data=${this._chartData}
|
||||
.highlight=${this.highlight}
|
||||
.snapSelection=${this._snapSelection}
|
||||
.options=${this._chartOptions}
|
||||
.height=${this.height}
|
||||
.selectMode=${this.selectMode}
|
||||
.zoomSlider=${this.zoomSlider}
|
||||
style=${styleMap({ height: this.height })}
|
||||
@dataset-hidden=${this._datasetHidden}
|
||||
@dataset-unhidden=${this._datasetUnhidden}
|
||||
@@ -259,6 +279,34 @@ export class StatisticsChart extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
public resetZoom() {
|
||||
this._chartBase?.zoom(0, 100);
|
||||
}
|
||||
|
||||
// A click selects the period that contains it, a drag every period it
|
||||
// touches
|
||||
private _snapSelection = (
|
||||
range: ChartTimeRange
|
||||
): ChartTimeRange | undefined => {
|
||||
let start: number | undefined;
|
||||
let end: number | undefined;
|
||||
Object.values(this.statisticsData ?? {}).forEach((values) =>
|
||||
values.forEach((value) => {
|
||||
const touched =
|
||||
range.start === range.end
|
||||
? value.start <= range.start && range.start < value.end
|
||||
: value.start < range.end && value.end > range.start;
|
||||
if (touched) {
|
||||
start = Math.min(start ?? value.start, value.start);
|
||||
end = Math.max(end ?? value.end, value.end);
|
||||
}
|
||||
})
|
||||
);
|
||||
return start === undefined || end === undefined
|
||||
? undefined
|
||||
: { start, end };
|
||||
};
|
||||
|
||||
private _datasetHidden(ev: HASSDomEvent<HASSDomEvents["dataset-hidden"]>) {
|
||||
this._hiddenStats.add(ev.detail.id);
|
||||
this.requestUpdate("_hiddenStats");
|
||||
|
||||
@@ -19,10 +19,8 @@ import {
|
||||
type CameraEntity,
|
||||
} from "../data/camera";
|
||||
import {
|
||||
hasWebRtcWorked,
|
||||
MJPEG_STREAM,
|
||||
selectCameraStreams,
|
||||
setWebRtcWorked,
|
||||
type CameraStream,
|
||||
type CameraStreamTracks,
|
||||
type CameraStreamType,
|
||||
@@ -39,9 +37,6 @@ import type { HaWebRtcPlayer } from "./ha-web-rtc-player";
|
||||
|
||||
export type { CameraStreamType } from "../data/camera-streams";
|
||||
|
||||
// How long WebRTC gets on its own before HLS is started as well
|
||||
const WEB_RTC_FIRST_TIMEOUT = 12000;
|
||||
|
||||
@customElement("ha-camera-stream")
|
||||
export class HaCameraStream extends LitElement {
|
||||
@state()
|
||||
@@ -86,22 +81,12 @@ export class HaCameraStream extends LitElement {
|
||||
|
||||
@state() private _webRtcStreams?: CameraStreamTracks;
|
||||
|
||||
// WebRTC worked before for this camera, so HLS is not started with it
|
||||
@state() private _webRtcFirst = false;
|
||||
|
||||
@query("ha-web-rtc-player") private _webRtcPlayer?: HaWebRtcPlayer;
|
||||
|
||||
private _visibleStreamType?: CameraStreamType;
|
||||
|
||||
private _webRtcFirstTimeout?: number;
|
||||
|
||||
private _capabilitiesRequest = 0;
|
||||
|
||||
private _streamsStartedAt = 0;
|
||||
|
||||
// If WebRTC loaded fast enough to be worth waiting for without HLS
|
||||
private _webRtcLoadedInTime?: boolean;
|
||||
|
||||
private _thumbnailApi = memoizeOne(
|
||||
(
|
||||
api: ContextType<typeof apiContext>,
|
||||
@@ -148,16 +133,15 @@ export class HaCameraStream extends LitElement {
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._connected = true;
|
||||
if (this.hasUpdated && this._capabilities && this.stateObj) {
|
||||
if (this.hasUpdated) {
|
||||
// The players start again when reattached, so their results do as well
|
||||
this._startStreams(this.stateObj.entity_id, this._capabilities);
|
||||
this._resetStreams();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._connected = false;
|
||||
this._stopWebRtcFirst();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues<this>): void {
|
||||
@@ -173,28 +157,6 @@ export class HaCameraStream extends LitElement {
|
||||
this._visibleStreamType = visibleStreamType;
|
||||
fireEvent(this, "stream-type-changed", { type: visibleStreamType });
|
||||
}
|
||||
this._rememberWebRtc(streams);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remembers per camera if WebRTC is the stream that ends up playing in this
|
||||
* browser, to start it without HLS the next time.
|
||||
*/
|
||||
private _rememberWebRtc(streams: CameraStream[]) {
|
||||
if (
|
||||
!this.stateObj ||
|
||||
!this._webRtcStreams ||
|
||||
(this._capabilities?.frontend_stream_types.length ?? 0) < 2 ||
|
||||
streams.length !== 1
|
||||
) {
|
||||
// No choice between streams, or the choice is not final yet
|
||||
return;
|
||||
}
|
||||
setWebRtcWorked(
|
||||
this.stateObj.entity_id,
|
||||
streams[0].type === STREAM_TYPE_WEB_RTC &&
|
||||
this._webRtcLoadedInTime === true
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -271,8 +233,7 @@ export class HaCameraStream extends LitElement {
|
||||
return this._streams(
|
||||
this._capabilities?.frontend_stream_types,
|
||||
this._hlsStreams,
|
||||
this._webRtcStreams,
|
||||
this._webRtcFirst
|
||||
this._webRtcStreams
|
||||
);
|
||||
}
|
||||
|
||||
@@ -287,62 +248,19 @@ export class HaCameraStream extends LitElement {
|
||||
};
|
||||
return;
|
||||
}
|
||||
const entityId = this.stateObj!.entity_id;
|
||||
const capabilities = await fetchCameraCapabilities(this._api, entityId);
|
||||
const capabilities = await fetchCameraCapabilities(
|
||||
this._api,
|
||||
this.stateObj!.entity_id
|
||||
);
|
||||
if (request !== this._capabilitiesRequest) {
|
||||
return;
|
||||
}
|
||||
if (this.isConnected) {
|
||||
// Otherwise started when connected
|
||||
this._startStreams(entityId, capabilities);
|
||||
}
|
||||
this._capabilities = capabilities;
|
||||
}
|
||||
|
||||
private _resetStreams() {
|
||||
this._hlsStreams = undefined;
|
||||
this._webRtcStreams = undefined;
|
||||
this._webRtcLoadedInTime = undefined;
|
||||
this._stopWebRtcFirst();
|
||||
}
|
||||
|
||||
private _startStreams(entityId: string, capabilities: CameraCapabilities) {
|
||||
this._resetStreams();
|
||||
this._streamsStartedAt = performance.now();
|
||||
if (
|
||||
capabilities.frontend_stream_types.length > 1 &&
|
||||
hasWebRtcWorked(entityId)
|
||||
) {
|
||||
this._webRtcFirst = true;
|
||||
this._webRtcFirstTimeout = window.setTimeout(
|
||||
this._webRtcFirstTimedOut,
|
||||
WEB_RTC_FIRST_TIMEOUT
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _webRtcFirstTimedOut = () => {
|
||||
if (document.hidden) {
|
||||
// A player started in the background is not stopped again, so wait
|
||||
this._webRtcFirstTimeout = window.setTimeout(
|
||||
this._webRtcFirstTimedOut,
|
||||
WEB_RTC_FIRST_TIMEOUT
|
||||
);
|
||||
return;
|
||||
}
|
||||
this._webRtcFirstTimeout = undefined;
|
||||
// Start HLS as well, WebRTC keeps connecting and takes over when it loads
|
||||
this._webRtcFirst = false;
|
||||
if (this.stateObj) {
|
||||
// Don't wait for it the next time, also when it never reports a result
|
||||
setWebRtcWorked(this.stateObj.entity_id, false);
|
||||
}
|
||||
};
|
||||
|
||||
private _stopWebRtcFirst() {
|
||||
clearTimeout(this._webRtcFirstTimeout);
|
||||
this._webRtcFirstTimeout = undefined;
|
||||
this._webRtcFirst = false;
|
||||
}
|
||||
|
||||
private async _getPosterUrl(): Promise<void> {
|
||||
@@ -368,8 +286,6 @@ export class HaCameraStream extends LitElement {
|
||||
}
|
||||
|
||||
private _handleWebRtcStreams(ev: HASSDomEvent<HASSDomEvents["streams"]>) {
|
||||
clearTimeout(this._webRtcFirstTimeout);
|
||||
this._webRtcFirstTimeout = undefined;
|
||||
if (
|
||||
!ev.detail.hasVideo &&
|
||||
!ev.detail.hasAudio &&
|
||||
@@ -380,10 +296,6 @@ export class HaCameraStream extends LitElement {
|
||||
// Only a browser without WebRTC falls back to MJPEG.
|
||||
return;
|
||||
}
|
||||
if (ev.detail.hasVideo && this._webRtcLoadedInTime === undefined) {
|
||||
this._webRtcLoadedInTime =
|
||||
performance.now() - this._streamsStartedAt <= WEB_RTC_FIRST_TIMEOUT;
|
||||
}
|
||||
this._webRtcStreams = ev.detail;
|
||||
}
|
||||
|
||||
@@ -391,8 +303,7 @@ export class HaCameraStream extends LitElement {
|
||||
(
|
||||
supportedTypes?: CameraCapabilities["frontend_stream_types"],
|
||||
hlsStreams?: CameraStreamTracks,
|
||||
webRtcStreams?: CameraStreamTracks,
|
||||
webRtcFirst?: boolean
|
||||
webRtcStreams?: CameraStreamTracks
|
||||
): CameraStream[] => {
|
||||
if (__DEMO__) {
|
||||
return [{ type: MJPEG_STREAM, visible: true }];
|
||||
@@ -401,7 +312,6 @@ export class HaCameraStream extends LitElement {
|
||||
supportedTypes,
|
||||
hlsStreams,
|
||||
webRtcStreams,
|
||||
webRtcFirst,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -112,6 +112,9 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
|
||||
@property({ type: Boolean, attribute: "no-sort" }) public noSort = false;
|
||||
|
||||
@property({ attribute: "pin-block" })
|
||||
public pinBlock?: "start" | "center" | "end" | "nearest";
|
||||
|
||||
// Skip the "unknown value" highlight and note for a value that is not in the
|
||||
// list but that the value renderer presents on its own.
|
||||
@property({ type: Boolean, attribute: "no-unknown-state" })
|
||||
@@ -347,6 +350,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
.searchKeys=${this.searchKeys}
|
||||
.customValueLabel=${this.customValueLabel}
|
||||
.noSort=${this.noSort}
|
||||
.pinBlock=${this.pinBlock}
|
||||
></ha-picker-combo-box>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -180,13 +180,14 @@ class HaHLSPlayer extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.entityid) {
|
||||
const entityId = this.entityid;
|
||||
if (!entityId) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { url } = await fetchStreamUrl(
|
||||
{ callWS: this._api.callWS, hassUrl: this._connection.hassUrl },
|
||||
this.entityid
|
||||
entityId
|
||||
);
|
||||
|
||||
this._url = this._connection.hassUrl(url);
|
||||
@@ -198,6 +199,10 @@ class HaHLSPlayer extends LitElement {
|
||||
// eslint-disable-next-line
|
||||
console.error(err);
|
||||
|
||||
if (!this.isConnected || entityId !== this.entityid) {
|
||||
// Removed or switched camera while fetching, the result is outdated
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "streams", { hasAudio: false, hasVideo: false });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import type { TemplateResult } from "lit";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { customIcons } from "../data/custom_icons";
|
||||
import type { ValueChangedEvent } from "../types";
|
||||
import "./ha-combo-box-item";
|
||||
import "./ha-generic-picker";
|
||||
import type { HaGenericPicker } from "./ha-generic-picker";
|
||||
import "./ha-icon";
|
||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
|
||||
|
||||
@@ -50,16 +54,17 @@ const createIconItem = (icon: IconData, prefix: string): PickerComboBoxItem => {
|
||||
const loadIcons = async () => {
|
||||
ICONS_LOADED = true;
|
||||
|
||||
const iconList = await import("../../build/mdi/iconList.json");
|
||||
ICONS = iconList.default.map((icon) => createIconItem(icon, "mdi"));
|
||||
const sortIcons = (icons: PickerComboBoxItem[]) =>
|
||||
icons.sort((a, b) => caseInsensitiveStringCompare(a.id, b.id));
|
||||
|
||||
const customIconLoads: Promise<PickerComboBoxItem[]>[] = [];
|
||||
Object.keys(customIcons).forEach((iconSet) => {
|
||||
customIconLoads.push(loadCustomIconItems(iconSet));
|
||||
});
|
||||
(await Promise.all(customIconLoads)).forEach((customIconItems) => {
|
||||
ICONS.push(...customIconItems);
|
||||
});
|
||||
const iconList = await import("../../build/mdi/iconList.json");
|
||||
const mdiIcons = iconList.default.map((icon) => createIconItem(icon, "mdi"));
|
||||
ICONS = sortIcons([...mdiIcons]);
|
||||
|
||||
const customIconItems = await Promise.all(
|
||||
Object.keys(customIcons).map(loadCustomIconItems)
|
||||
);
|
||||
ICONS = sortIcons([...mdiIcons, ...customIconItems.flat()]);
|
||||
};
|
||||
|
||||
const loadCustomIconItems = async (
|
||||
@@ -81,8 +86,8 @@ const loadCustomIconItems = async (
|
||||
|
||||
const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
|
||||
<ha-combo-box-item>
|
||||
<ha-icon .icon=${item.id} slot="start"></ha-icon>
|
||||
${item.id}
|
||||
<ha-icon .icon=${item.icon ?? item.id} slot="start"></ha-icon>
|
||||
${item.icon ?? item.id}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
|
||||
@@ -104,12 +109,32 @@ export class HaIconPicker extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public invalid = false;
|
||||
|
||||
private _getIconPickerItems = (): PickerComboBoxItem[] => ICONS;
|
||||
@property({ attribute: false }) public suggestedIcons?: string[];
|
||||
|
||||
@state() @consumeLocalize() private _localize!: LocalizeFunc;
|
||||
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
private _getIconPickerItems = () =>
|
||||
this._itemsWithSuggestions(ICONS, this.suggestedIcons, this._localize);
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("suggestedIcons") || changedProps.has("_localize")) {
|
||||
this._picker?.refreshItems();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
allow-custom-value
|
||||
no-sort
|
||||
.pinBlock=${
|
||||
this.value && this.suggestedIcons?.includes(this.value)
|
||||
? "end"
|
||||
: undefined
|
||||
}
|
||||
.getItems=${this._getIconPickerItems}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
@@ -128,15 +153,35 @@ export class HaIconPicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _itemsWithSuggestions = memoizeOne(
|
||||
(
|
||||
icons: PickerComboBoxItem[],
|
||||
suggestedIcons: string[] | undefined,
|
||||
localize: LocalizeFunc
|
||||
): (PickerComboBoxItem | string)[] => {
|
||||
if (!suggestedIcons?.length) {
|
||||
return icons;
|
||||
}
|
||||
const suggested = new Set(suggestedIcons);
|
||||
return [
|
||||
localize("ui.components.icon_picker.suggested"),
|
||||
...suggestedIcons.map((icon) => ({ id: icon, primary: icon, icon })),
|
||||
localize("ui.components.icon_picker.all_icons"),
|
||||
...icons.filter((item) => !suggested.has(item.id)),
|
||||
];
|
||||
}
|
||||
);
|
||||
|
||||
// Filter can take a significant chunk of frame (up to 3-5 ms)
|
||||
private _filterIcons = memoizeOne(
|
||||
(
|
||||
filter: string,
|
||||
filteredItems: PickerComboBoxItem[],
|
||||
allItems: PickerComboBoxItem[]
|
||||
_allItems: PickerComboBoxItem[]
|
||||
): PickerComboBoxItem[] => {
|
||||
const normalizedFilter = filter.toLowerCase().replace(/\s+/g, "-");
|
||||
const iconItems = allItems?.length ? allItems : filteredItems;
|
||||
// The full list also holds the suggestions and their section titles
|
||||
const iconItems = ICONS.length ? ICONS : filteredItems;
|
||||
|
||||
if (!normalizedFilter.length) {
|
||||
return iconItems;
|
||||
@@ -175,8 +220,7 @@ export class HaIconPicker extends LitElement {
|
||||
protected firstUpdated() {
|
||||
if (!ICONS_LOADED) {
|
||||
loadIcons().then(() => {
|
||||
this._getIconPickerItems = (): PickerComboBoxItem[] => ICONS;
|
||||
this.requestUpdate();
|
||||
this._picker?.refreshItems();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,6 +201,9 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
@property({ type: Boolean, attribute: "no-sort" }) public noSort = false;
|
||||
|
||||
@property({ attribute: "pin-block" })
|
||||
public pinBlock?: "start" | "center" | "end" | "nearest";
|
||||
|
||||
@query(".list") private _list?: HaListBase;
|
||||
|
||||
@query("ha-input-search") private _searchFieldElement?: HaInputSearch;
|
||||
@@ -453,6 +456,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
this.value
|
||||
)}
|
||||
.pinIndex=${this.value ? this._getInitialSelectedIndex() : undefined}
|
||||
.pinBlock=${this.pinBlock ?? "center"}
|
||||
@mousedown=${this._preventBlur}
|
||||
@focus=${this._focusList}
|
||||
@blur=${this._resetSelectedItem}
|
||||
@@ -685,6 +689,11 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
fireEvent(this, "index-selected", { index, item, newTab });
|
||||
}
|
||||
|
||||
// Fuse indexes every row as text when the first one is a section title
|
||||
private _searchableItems = memoizeOne((items: PickerComboBoxItem[]) =>
|
||||
items.filter((item) => typeof item !== "string")
|
||||
);
|
||||
|
||||
private _fuseIndex = memoizeOne(
|
||||
(states: PickerComboBoxItem[], searchKeys?: FuseWeightedKey[]) =>
|
||||
Fuse.createIndex(searchKeys || DEFAULT_SEARCH_KEYS, states)
|
||||
@@ -708,10 +717,11 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = this._fuseIndex(this._allItems, this.searchKeys);
|
||||
const searchableItems = this._searchableItems(this._allItems);
|
||||
const index = this._fuseIndex(searchableItems, this.searchKeys);
|
||||
|
||||
let filteredItems = multiTermSortedSearch<PickerComboBoxItem>(
|
||||
this._allItems,
|
||||
searchableItems,
|
||||
searchString,
|
||||
(item) => item.id,
|
||||
index
|
||||
|
||||
@@ -9,12 +9,6 @@ import { consume } from "../common/decorators/consume";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { getIcePath } from "../common/webrtc/get-ice-path";
|
||||
import type { IcePath } from "../common/webrtc/limit-ice-servers";
|
||||
import {
|
||||
limitIceServers,
|
||||
limitIceServersToPath,
|
||||
} from "../common/webrtc/limit-ice-servers";
|
||||
import {
|
||||
addWebRtcCandidate,
|
||||
fetchWebRtcClientConfiguration,
|
||||
@@ -37,8 +31,10 @@ const ANSWER_TIMEOUT = 20000;
|
||||
// minutes to give up on their own, or never do.
|
||||
const ICE_TIMEOUT = 30000;
|
||||
|
||||
// How often a stream that lost or never got its connection is started again
|
||||
const MAX_RETRIES = 2;
|
||||
// How often a stream that lost its connection or got a backend error is
|
||||
// started again. A new connection that gets no answer or finds no path is not
|
||||
// retried, as a retry would most likely end the same way.
|
||||
const MAX_RETRIES = 1;
|
||||
|
||||
// Gives the backend time to tear down the failed session before the next one
|
||||
const RETRY_DELAY = 1000;
|
||||
@@ -192,16 +188,8 @@ export class HaWebRtcPlayer extends LitElement {
|
||||
// Renegotiating the existing connection is not supported by all providers
|
||||
// (go2rtc answers with a new DTLS fingerprint, which Firefox rejects), so
|
||||
// start a new connection and session with the microphone track instead.
|
||||
// The new connection only looks for the path the running stream uses
|
||||
const icePath = this._peerConnection
|
||||
? await getIcePath(this._peerConnection)
|
||||
: undefined;
|
||||
if (request !== this._microphoneRequest || !this.isConnected) {
|
||||
track.stop();
|
||||
return;
|
||||
}
|
||||
this._captureReconnectFrame();
|
||||
await this._startWebRtc(track, icePath);
|
||||
await this._startWebRtc(track);
|
||||
}
|
||||
|
||||
private _captureReconnectFrame() {
|
||||
@@ -238,8 +226,9 @@ export class HaWebRtcPlayer extends LitElement {
|
||||
|
||||
private _retries = 0;
|
||||
|
||||
// The stream only loaded with all ICE servers, so keep using them
|
||||
private _allIceServers = false;
|
||||
// The stream showed video since it was started, so a lost connection is
|
||||
// worth a retry
|
||||
private _hasPlayed = false;
|
||||
|
||||
private _handleVisibilityChange = () => {
|
||||
if (document.pictureInPictureElement) {
|
||||
@@ -256,8 +245,7 @@ export class HaWebRtcPlayer extends LitElement {
|
||||
clearTimeout(this._hiddenCleanupTimeout);
|
||||
this._hiddenCleanupTimeout = undefined;
|
||||
} else {
|
||||
this._retries = 0;
|
||||
this._startWebRtc();
|
||||
this._restartWebRtc();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -298,8 +286,7 @@ export class HaWebRtcPlayer extends LitElement {
|
||||
public override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
if (this.hasUpdated && this.entityid) {
|
||||
this._retries = 0;
|
||||
this._startWebRtc();
|
||||
this._restartWebRtc();
|
||||
}
|
||||
document.addEventListener("visibilitychange", this._handleVisibilityChange);
|
||||
}
|
||||
@@ -320,14 +307,18 @@ export class HaWebRtcPlayer extends LitElement {
|
||||
if (!changedProperties.has("entityid")) {
|
||||
return;
|
||||
}
|
||||
this._restartWebRtc();
|
||||
}
|
||||
|
||||
/** Starts a new stream, not a retry of the current one. */
|
||||
private _restartWebRtc() {
|
||||
this._retries = 0;
|
||||
this._allIceServers = false;
|
||||
this._hasPlayed = false;
|
||||
this._startWebRtc();
|
||||
}
|
||||
|
||||
private async _startWebRtc(
|
||||
localReturnAudioTrack?: MediaStreamTrack,
|
||||
icePath?: IcePath
|
||||
localReturnAudioTrack?: MediaStreamTrack
|
||||
): Promise<void> {
|
||||
this._cleanUp();
|
||||
|
||||
@@ -382,21 +373,9 @@ export class HaWebRtcPlayer extends LitElement {
|
||||
|
||||
this._logEvent("end clientConfig", this._clientConfig);
|
||||
|
||||
const { iceServers } = this._clientConfig.configuration;
|
||||
const allIceServers = this._allIceServers || this._retries >= MAX_RETRIES;
|
||||
|
||||
this._peerConnection = new RTCPeerConnection({
|
||||
...this._clientConfig.configuration,
|
||||
// The last attempt uses all ICE servers, in case a removed URL is the
|
||||
// only one that works in this network
|
||||
// A known relay is taken from all of them. A direct path is not, as
|
||||
// the URL it needs can be one that is removed.
|
||||
iceServers:
|
||||
(icePath &&
|
||||
(icePath.relayUrl || !allIceServers) &&
|
||||
limitIceServersToPath(iceServers, icePath)) ||
|
||||
(allIceServers ? iceServers : limitIceServers(iceServers)),
|
||||
});
|
||||
this._peerConnection = new RTCPeerConnection(
|
||||
this._clientConfig.configuration
|
||||
);
|
||||
|
||||
if (this._clientConfig.dataChannel) {
|
||||
// Some cameras (such as nest) require a data channel to establish a stream
|
||||
@@ -529,6 +508,9 @@ export class HaWebRtcPlayer extends LitElement {
|
||||
private _retryOrFail(error: WebRtcPlayerError) {
|
||||
if (
|
||||
this._retries >= MAX_RETRIES ||
|
||||
// No answer or no path for a stream that never played, a retry would
|
||||
// most likely end the same way
|
||||
(error.type === "no_connection" && !this._hasPlayed) ||
|
||||
// A microphone connect the user started is handled by starting again
|
||||
// without the microphone. A retry keeps the microphone of the stream.
|
||||
(this._localReturnAudioTrack &&
|
||||
@@ -799,10 +781,8 @@ export class HaWebRtcPlayer extends LitElement {
|
||||
|
||||
this._logEvent("loadedData", data);
|
||||
this._stopTimer();
|
||||
if (this._retries >= MAX_RETRIES) {
|
||||
this._allIceServers = true;
|
||||
}
|
||||
this._retries = 0;
|
||||
this._hasPlayed = true;
|
||||
|
||||
// Without a track the microphone is still waiting for permission
|
||||
if (this._microphoneConnecting && this._localReturnAudioTrack) {
|
||||
|
||||
@@ -34,6 +34,8 @@ import type { HASSDomTargetEvent } from "../../common/dom/fire_event";
|
||||
*
|
||||
* @attr {string} headline - Primary text. Overridden by the `headline` slot.
|
||||
* @attr {string} supporting-text - Secondary text. Overridden by the `supporting-text` slot.
|
||||
* @attr {boolean} wrap-headline - Wraps the headline onto multiple lines instead of truncating it.
|
||||
* @attr {boolean} wrap-supporting-text - Wraps the supporting text onto multiple lines instead of truncating it.
|
||||
* @attr {boolean} disabled - Dims the row and blocks pointer events.
|
||||
*/
|
||||
@customElement("ha-row-item")
|
||||
@@ -43,6 +45,12 @@ export class HaRowItem extends LitElement {
|
||||
@property({ type: String, attribute: "supporting-text" })
|
||||
public supportingText?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "wrap-headline" })
|
||||
public wrapHeadline = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "wrap-supporting-text" })
|
||||
public wrapSupportingText = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
|
||||
protected readonly _slotController = new HasSlotController(
|
||||
@@ -192,6 +200,10 @@ export class HaRowItem extends LitElement {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
:host([wrap-headline]) .headline,
|
||||
:host([wrap-supporting-text]) .supporting {
|
||||
white-space: normal;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { HaListItemButton } from "../item/ha-list-item-button";
|
||||
import "./ha-media-browser-thumbnail";
|
||||
|
||||
/**
|
||||
* @element ha-media-player-browse-list-item
|
||||
* @extends {HaListItemButton}
|
||||
*
|
||||
* @summary
|
||||
* Row of the media browser list. Always reserves a graphic next to the title
|
||||
* so titles line up, and shows a thumbnail with an optional play button over
|
||||
* it. Without `interactive` the row is plain text, used for the "not shown"
|
||||
* row. The row is rendered inside the shadow root of `ha-list-virtualized`, so
|
||||
* it carries its own styles.
|
||||
*
|
||||
* @slot start - Play or pick button, shown over the thumbnail.
|
||||
* @slot headline - Title of the row.
|
||||
*
|
||||
* @csspart start - The graphic container.
|
||||
*
|
||||
* @attr {string} thumbnail - URL of the thumbnail. Shown inside the graphic.
|
||||
*/
|
||||
@customElement("ha-media-player-browse-list-item")
|
||||
export class HaMediaPlayerBrowseListItem extends HaListItemButton {
|
||||
@property() public thumbnail?: string;
|
||||
|
||||
// The "not shown" row is plain text, not a button
|
||||
protected override _renderControl(content: TemplateResult): TemplateResult {
|
||||
if (!this.interactive) {
|
||||
return content;
|
||||
}
|
||||
return super._renderControl(content);
|
||||
}
|
||||
|
||||
// Always reserve the graphic, so titles line up across rows
|
||||
protected override _renderStart(): TemplateResult {
|
||||
return html`<div part="start" class="start">
|
||||
<div class="graphic">
|
||||
${
|
||||
this.thumbnail
|
||||
? html`<ha-media-browser-thumbnail
|
||||
.url=${this.thumbnail}
|
||||
></ha-media-browser-thumbnail>`
|
||||
: nothing
|
||||
}
|
||||
<slot name="start"></slot>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = [
|
||||
HaListItemButton.styles,
|
||||
css`
|
||||
:host {
|
||||
direction: var(--direction);
|
||||
--ha-row-item-padding-block: 0;
|
||||
--ha-row-item-min-height: 48px;
|
||||
}
|
||||
|
||||
:host(.show-images) {
|
||||
--ha-row-item-padding-block: var(--ha-space-1);
|
||||
--ha-row-item-min-height: 64px;
|
||||
}
|
||||
|
||||
:host(.not-shown) {
|
||||
font-style: italic;
|
||||
color: var(--secondary-text-color);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
::slotted(ha-svg-icon) {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.graphic {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:host(.show-images) .graphic {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
ha-media-browser-thumbnail {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
::slotted(.play) {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
margin: auto;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
opacity: 0;
|
||||
transition: all 0.5s;
|
||||
background-color: rgba(var(--rgb-card-background-color), 0.5);
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
--ha-icon-button-size: 40px;
|
||||
}
|
||||
|
||||
:host(:hover) ::slotted(.play) {
|
||||
opacity: 1;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
::slotted(.play.show) {
|
||||
opacity: 1;
|
||||
background-color: transparent;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-media-player-browse-list-item": HaMediaPlayerBrowseListItem;
|
||||
}
|
||||
}
|
||||
@@ -77,7 +77,6 @@ import "../ha-svg-icon";
|
||||
import "../ha-tooltip";
|
||||
import "../input/ha-input-search";
|
||||
import type { HaInputSearch } from "../input/ha-input-search";
|
||||
import { HaListItemButton } from "../item/ha-list-item-button";
|
||||
import "../list/ha-list-virtualized";
|
||||
import type {
|
||||
HaListVirtualized,
|
||||
@@ -88,6 +87,7 @@ import type { ManualMediaPickedEvent } from "./ha-browse-media-manual";
|
||||
import "./ha-browse-media-tts";
|
||||
import type { TtsMediaPickedEvent } from "./ha-browse-media-tts";
|
||||
import "./ha-media-browser-thumbnail";
|
||||
import "./ha-media-player-browse-list-item";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -120,105 +120,6 @@ type MediaBrowseListRow =
|
||||
count: number;
|
||||
});
|
||||
|
||||
@customElement("ha-media-player-browse-list-item")
|
||||
export class HaMediaPlayerBrowseListItem extends HaListItemButton {
|
||||
@property() public thumbnail?: string;
|
||||
|
||||
// The "not shown" row is plain text, not a button
|
||||
protected override _renderControl(content: TemplateResult): TemplateResult {
|
||||
if (!this.interactive) {
|
||||
return content;
|
||||
}
|
||||
return super._renderControl(content);
|
||||
}
|
||||
|
||||
// Always reserve the graphic, so titles line up across rows
|
||||
protected override _renderStart(): TemplateResult {
|
||||
return html`<div part="start" class="start">
|
||||
<div class="graphic">
|
||||
${
|
||||
this.thumbnail
|
||||
? html`<ha-media-browser-thumbnail
|
||||
.url=${this.thumbnail}
|
||||
></ha-media-browser-thumbnail>`
|
||||
: nothing
|
||||
}
|
||||
<slot name="start"></slot>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = [
|
||||
HaListItemButton.styles,
|
||||
css`
|
||||
:host {
|
||||
direction: var(--direction);
|
||||
--ha-row-item-padding-block: 0;
|
||||
--ha-row-item-min-height: 48px;
|
||||
}
|
||||
|
||||
:host(.show-images) {
|
||||
--ha-row-item-padding-block: var(--ha-space-1);
|
||||
--ha-row-item-min-height: 64px;
|
||||
}
|
||||
|
||||
:host(.not-shown) {
|
||||
font-style: italic;
|
||||
color: var(--secondary-text-color);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
::slotted(ha-svg-icon) {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.graphic {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:host(.show-images) .graphic {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
ha-media-browser-thumbnail {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
::slotted(.play) {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
margin: auto;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
opacity: 0;
|
||||
transition: all 0.5s;
|
||||
background-color: rgba(var(--rgb-card-background-color), 0.5);
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
--ha-icon-button-size: 40px;
|
||||
}
|
||||
|
||||
:host(:hover) ::slotted(.play) {
|
||||
opacity: 1;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
::slotted(.play.show) {
|
||||
opacity: 1;
|
||||
background-color: transparent;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
/** Folders with at least this many items get a filter field when their source cannot search. */
|
||||
const FILTER_MIN_CHILDREN = 10;
|
||||
|
||||
@@ -1945,6 +1846,5 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-media-player-browse": HaMediaPlayerBrowse;
|
||||
"ha-media-player-browse-list-item": HaMediaPlayerBrowseListItem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
|
||||
export const ROOM_ICONS = {
|
||||
living_room: [
|
||||
"mdi:sofa",
|
||||
"mdi:sofa-single",
|
||||
"mdi:television",
|
||||
"mdi:fireplace",
|
||||
"mdi:floor-lamp",
|
||||
],
|
||||
bedroom: ["mdi:bed", "mdi:bed-king", "mdi:bed-double", "mdi:lamp"],
|
||||
kids_room: ["mdi:teddy-bear", "mdi:toy-brick", "mdi:bunk-bed", "mdi:cradle"],
|
||||
kitchen: [
|
||||
"mdi:stove",
|
||||
"mdi:fridge",
|
||||
"mdi:countertop",
|
||||
"mdi:chef-hat",
|
||||
"mdi:pot-steam",
|
||||
"mdi:coffee-maker",
|
||||
],
|
||||
dining_room: [
|
||||
"mdi:silverware-fork-knife",
|
||||
"mdi:table-chair",
|
||||
"mdi:table-furniture",
|
||||
"mdi:glass-wine",
|
||||
],
|
||||
bathroom: [
|
||||
"mdi:shower",
|
||||
"mdi:bathtub",
|
||||
"mdi:shower-head",
|
||||
"mdi:toothbrush",
|
||||
"mdi:hair-dryer",
|
||||
],
|
||||
toilet: ["mdi:toilet", "mdi:paper-roll"],
|
||||
office: [
|
||||
"mdi:desk",
|
||||
"mdi:laptop",
|
||||
"mdi:monitor",
|
||||
"mdi:chair-rolling",
|
||||
"mdi:bookshelf",
|
||||
],
|
||||
laundry_room: [
|
||||
"mdi:washing-machine",
|
||||
"mdi:tumble-dryer",
|
||||
"mdi:iron",
|
||||
"mdi:hanger",
|
||||
],
|
||||
hallway: ["mdi:door", "mdi:door-open", "mdi:coat-rack", "mdi:key"],
|
||||
stairs: ["mdi:stairs", "mdi:stairs-up", "mdi:stairs-down"],
|
||||
garage: ["mdi:garage", "mdi:garage-variant", "mdi:car", "mdi:tools"],
|
||||
garden: [
|
||||
"mdi:flower",
|
||||
"mdi:tree",
|
||||
"mdi:grass",
|
||||
"mdi:sprout",
|
||||
"mdi:watering-can",
|
||||
"mdi:greenhouse",
|
||||
],
|
||||
terrace: [
|
||||
"mdi:balcony",
|
||||
"mdi:grill",
|
||||
"mdi:umbrella-outline",
|
||||
"mdi:coach-lamp",
|
||||
"mdi:patio-heater",
|
||||
],
|
||||
basement: [
|
||||
"mdi:home-floor-negative-1",
|
||||
"mdi:stairs-down",
|
||||
"mdi:water-boiler",
|
||||
],
|
||||
attic: ["mdi:home-roof", "mdi:package-variant-closed"],
|
||||
storage: ["mdi:archive", "mdi:package-variant-closed", "mdi:warehouse"],
|
||||
utility_room: [
|
||||
"mdi:water-boiler",
|
||||
"mdi:heat-pump",
|
||||
"mdi:server-network",
|
||||
"mdi:flash",
|
||||
],
|
||||
pool: ["mdi:pool", "mdi:swim"],
|
||||
media_room: [
|
||||
"mdi:theater",
|
||||
"mdi:projector",
|
||||
"mdi:television",
|
||||
"mdi:movie-open",
|
||||
],
|
||||
game_room: [
|
||||
"mdi:gamepad-variant",
|
||||
"mdi:billiards",
|
||||
"mdi:cards-playing",
|
||||
"mdi:dice-multiple",
|
||||
],
|
||||
music_room: ["mdi:piano", "mdi:guitar-acoustic", "mdi:music"],
|
||||
library: ["mdi:bookshelf", "mdi:book-open-variant", "mdi:library"],
|
||||
bar: [
|
||||
"mdi:glass-cocktail",
|
||||
"mdi:glass-mug-variant",
|
||||
"mdi:bottle-wine",
|
||||
"mdi:beer",
|
||||
],
|
||||
dressing_room: [
|
||||
"mdi:hanger",
|
||||
"mdi:wardrobe",
|
||||
"mdi:tshirt-crew",
|
||||
"mdi:shoe-heel",
|
||||
],
|
||||
workshop: ["mdi:tools", "mdi:hammer-wrench", "mdi:toolbox", "mdi:saw-blade"],
|
||||
server_room: ["mdi:server-network", "mdi:server", "mdi:router-network"],
|
||||
safe_room: ["mdi:shield-home", "mdi:shield-lock", "mdi:safe"],
|
||||
prayer_room: ["mdi:hands-pray", "mdi:meditation", "mdi:candle"],
|
||||
sauna: ["mdi:spa", "mdi:hot-tub", "mdi:thermometer-high"],
|
||||
outdoor_kitchen: ["mdi:grill", "mdi:grill-outline", "mdi:pizza"],
|
||||
driveway: ["mdi:road-variant", "mdi:car", "mdi:gate"],
|
||||
} as const satisfies Record<string, readonly string[]>;
|
||||
|
||||
export type RoomType = keyof typeof ROOM_ICONS;
|
||||
|
||||
type RoomKeywords = Partial<Record<RoomType, string[]>>;
|
||||
|
||||
export const ROOM_TYPES = Object.keys(ROOM_ICONS) as RoomType[];
|
||||
|
||||
export const isRoomType = (value: string): value is RoomType =>
|
||||
(ROOM_TYPES as string[]).includes(value);
|
||||
|
||||
// Languages written without spaces between words
|
||||
const NO_WORD_SPACING =
|
||||
/[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Thai}]/u;
|
||||
|
||||
const normalizeRoomText = (text: string): string =>
|
||||
text
|
||||
.normalize("NFD")
|
||||
.replace(/\p{M}/gu, "")
|
||||
.toLowerCase()
|
||||
.replace(/['’]/g, "")
|
||||
.replace(/[^\p{L}\p{N}]+/gu, " ")
|
||||
.trim();
|
||||
|
||||
interface RoomMatch {
|
||||
room: RoomType;
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
const findMatches = (name: string, keywords: RoomKeywords): RoomMatch[] => {
|
||||
const text = ` ${normalizeRoomText(name)} `;
|
||||
const candidates: RoomMatch[] = [];
|
||||
for (const room of ROOM_TYPES) {
|
||||
for (const keyword of keywords[room] ?? []) {
|
||||
const phrase = normalizeRoomText(keyword);
|
||||
if (!phrase) {
|
||||
continue;
|
||||
}
|
||||
const needle = NO_WORD_SPACING.test(phrase) ? phrase : ` ${phrase} `;
|
||||
const start = text.indexOf(needle);
|
||||
if (start !== -1) {
|
||||
candidates.push({ room, start, end: start + needle.length });
|
||||
}
|
||||
}
|
||||
}
|
||||
// The longest phrase wins, so "guest bedroom" is not also read as "bedroom"
|
||||
candidates.sort((a, b) => b.end - b.start - (a.end - a.start));
|
||||
const kept: RoomMatch[] = [];
|
||||
for (const match of candidates) {
|
||||
const overlaps = kept.some(
|
||||
(other) => match.start < other.end - 1 && other.start < match.end - 1
|
||||
);
|
||||
if (!overlaps) {
|
||||
kept.push(match);
|
||||
}
|
||||
}
|
||||
return kept.sort((a, b) => a.start - b.start);
|
||||
};
|
||||
|
||||
const matchRoomTypes = (
|
||||
names: string[],
|
||||
keywords: RoomKeywords
|
||||
): RoomType[] => {
|
||||
const rooms = new Set<RoomType>();
|
||||
for (const name of names) {
|
||||
for (const match of findMatches(name, keywords)) {
|
||||
rooms.add(match.room);
|
||||
}
|
||||
}
|
||||
return [...rooms];
|
||||
};
|
||||
|
||||
const roomIconSuggestions = (rooms: RoomType[]): string[] => [
|
||||
...new Set(rooms.flatMap((room) => ROOM_ICONS[room])),
|
||||
];
|
||||
|
||||
export const getAreaIconSuggestions = (
|
||||
names: string[],
|
||||
localize: LocalizeFunc
|
||||
): string[] => {
|
||||
const keywords: RoomKeywords = Object.fromEntries(
|
||||
ROOM_TYPES.map((room) => [
|
||||
room,
|
||||
localize(`ui.components.rooms.keywords.${room}`).split("|"),
|
||||
])
|
||||
);
|
||||
return roomIconSuggestions(matchRoomTypes(names, keywords));
|
||||
};
|
||||
@@ -1,6 +1,3 @@
|
||||
import { addRecent } from "../common/array/add-recent";
|
||||
import { getStorage } from "../common/decorators/storage";
|
||||
|
||||
export const STREAM_TYPE_HLS = "hls";
|
||||
export const STREAM_TYPE_WEB_RTC = "web_rtc";
|
||||
|
||||
@@ -25,8 +22,6 @@ export interface CameraStreamSelectionOptions {
|
||||
supportedTypes?: StreamType[];
|
||||
hlsStreams?: CameraStreamTracks;
|
||||
webRtcStreams?: CameraStreamTracks;
|
||||
/** Only start WebRTC and keep HLS off until WebRTC fails or takes too long. */
|
||||
webRtcFirst?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,7 +32,6 @@ export const selectCameraStreams = ({
|
||||
supportedTypes,
|
||||
hlsStreams,
|
||||
webRtcStreams,
|
||||
webRtcFirst,
|
||||
}: CameraStreamSelectionOptions): CameraStream[] => {
|
||||
if (!supportedTypes) {
|
||||
return [];
|
||||
@@ -76,7 +70,7 @@ export const selectCameraStreams = ({
|
||||
}
|
||||
|
||||
// WebRTC is still loading
|
||||
if (hlsFailed || (webRtcFirst && !hlsStreams)) {
|
||||
if (hlsFailed) {
|
||||
return [{ type: STREAM_TYPE_WEB_RTC, visible: true }];
|
||||
}
|
||||
return [
|
||||
@@ -84,32 +78,3 @@ export const selectCameraStreams = ({
|
||||
{ type: STREAM_TYPE_WEB_RTC, visible: false },
|
||||
];
|
||||
};
|
||||
|
||||
const WEB_RTC_ENTITIES_KEY = "cameraWebRtcEntities";
|
||||
|
||||
const MAX_WEB_RTC_ENTITIES = 100;
|
||||
|
||||
const getWebRtcEntities = (): string[] => {
|
||||
const entities = getStorage().getValue<string[]>(WEB_RTC_ENTITIES_KEY);
|
||||
return Array.isArray(entities) ? entities : [];
|
||||
};
|
||||
|
||||
/**
|
||||
* If WebRTC was the stream that ended up playing for this camera in this
|
||||
* browser. Stored per browser, as it depends on the network of the client.
|
||||
*/
|
||||
export const hasWebRtcWorked = (entityId: string): boolean =>
|
||||
getWebRtcEntities().includes(entityId);
|
||||
|
||||
export const setWebRtcWorked = (entityId: string, worked: boolean): void => {
|
||||
const entities = getWebRtcEntities();
|
||||
if (entities.includes(entityId) === worked) {
|
||||
return;
|
||||
}
|
||||
getStorage().setValue(
|
||||
WEB_RTC_ENTITIES_KEY,
|
||||
worked
|
||||
? addRecent(entities, entityId, MAX_WEB_RTC_ENTITIES)
|
||||
: entities.filter((entity) => entity !== entityId)
|
||||
);
|
||||
};
|
||||
+11
-7
@@ -209,7 +209,15 @@ export const watchForNewMatterDevice = (
|
||||
callback: (device: DeviceRegistryEntry) => void
|
||||
): UnsubscribeFunc => {
|
||||
let curMatterDevices: Set<string> | undefined;
|
||||
const unsubDeviceReg = subscribeDeviceRegistry(hass.connection, (entries) => {
|
||||
let unsubDeviceReg: UnsubscribeFunc | undefined;
|
||||
// Callers also unsubscribe after a find. The registry collection counts its
|
||||
// subscribers, so a second unsubscribe would stop its updates for everyone.
|
||||
const unsubscribe = () => {
|
||||
unsubDeviceReg?.();
|
||||
unsubDeviceReg = undefined;
|
||||
curMatterDevices = undefined;
|
||||
};
|
||||
unsubDeviceReg = subscribeDeviceRegistry(hass.connection, (entries) => {
|
||||
if (!curMatterDevices) {
|
||||
curMatterDevices = new Set(
|
||||
Object.values(entries)
|
||||
@@ -226,15 +234,11 @@ export const watchForNewMatterDevice = (
|
||||
!curMatterDevices!.has(device.id)
|
||||
);
|
||||
if (newMatterDevices.length) {
|
||||
unsubDeviceReg();
|
||||
curMatterDevices = undefined;
|
||||
unsubscribe();
|
||||
callback(newMatterDevices[0]);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
unsubDeviceReg();
|
||||
curMatterDevices = undefined;
|
||||
};
|
||||
return unsubscribe;
|
||||
};
|
||||
|
||||
export const redirectOnNewMatterDevice = (
|
||||
|
||||
@@ -167,16 +167,12 @@ export class HaMoreInfoFavorites extends LitElement {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: calc(var(--ha-space-2) * -1);
|
||||
gap: var(--ha-space-4);
|
||||
flex-wrap: wrap;
|
||||
max-width: var(--favorite-items-max-width, 250px);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.container > * {
|
||||
margin: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.favorite {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -60,14 +60,25 @@
|
||||
/* Margins, not flex `gap` (Safari 14.1). */
|
||||
#ha-launch-screen .ha-launch-screen-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
margin: -4px 16px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 0 16px;
|
||||
}
|
||||
#ha-launch-screen .ha-launch-screen-actions label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#ha-launch-screen .ha-launch-screen-actions input {
|
||||
margin: 0 8px 0 0;
|
||||
accent-color: #009ac7;
|
||||
}
|
||||
#ha-launch-screen .ha-launch-screen-actions button {
|
||||
margin: 4px;
|
||||
padding: 8px 16px;
|
||||
border: 1px solid transparent;
|
||||
border: 1px solid currentColor;
|
||||
border-radius: 18px;
|
||||
background: none;
|
||||
color: #009ac7;
|
||||
@@ -76,9 +87,6 @@
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
#ha-launch-screen .ha-launch-screen-actions .ha-launch-screen-continue {
|
||||
border-color: currentColor;
|
||||
}
|
||||
.ohf-logo {
|
||||
margin: 48px 0;
|
||||
display: flex;
|
||||
|
||||
@@ -116,30 +116,33 @@
|
||||
actions.className = "ha-launch-screen-actions";
|
||||
box.appendChild(actions);
|
||||
|
||||
function addButton(label, value) {
|
||||
var button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.textContent = label;
|
||||
button.addEventListener("click", function () {
|
||||
try {
|
||||
localStorage.setItem(ACCEPTED_KEY, value());
|
||||
} catch (_err) {
|
||||
// Continue this time only.
|
||||
}
|
||||
box.removeChild(actions);
|
||||
loadApp();
|
||||
});
|
||||
actions.appendChild(button);
|
||||
return button;
|
||||
}
|
||||
var always = null;
|
||||
var button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.textContent = "Continue anyway";
|
||||
button.addEventListener("click", function () {
|
||||
try {
|
||||
localStorage.setItem(
|
||||
ACCEPTED_KEY,
|
||||
always && always.checked ? "always" : String(Date.now())
|
||||
);
|
||||
} catch (_err) {
|
||||
// Continue this time only.
|
||||
}
|
||||
box.removeChild(actions);
|
||||
loadApp();
|
||||
});
|
||||
actions.appendChild(button);
|
||||
|
||||
addButton("Continue anyway", function () {
|
||||
return String(Date.now());
|
||||
}).className = "ha-launch-screen-continue";
|
||||
// From the first reminder on. A checkbox, so it is not chosen by
|
||||
// accident.
|
||||
if (acceptedAt) {
|
||||
addButton("Don't show again", function () {
|
||||
return "always";
|
||||
});
|
||||
var label = document.createElement("label");
|
||||
always = document.createElement("input");
|
||||
always.type = "checkbox";
|
||||
label.appendChild(always);
|
||||
label.appendChild(document.createTextNode("Don't show again"));
|
||||
actions.appendChild(label);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
@@ -816,7 +816,7 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
|
||||
this._currentAddon.version
|
||||
? html`
|
||||
<wa-divider></wa-divider>
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.i18n.localize(
|
||||
"ui.panel.config.apps.dashboard.option.boot.title"
|
||||
@@ -842,7 +842,7 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
|
||||
${
|
||||
this._currentAddon.startup !== "once"
|
||||
? html`
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.i18n.localize(
|
||||
"ui.panel.config.apps.dashboard.option.watchdog.title"
|
||||
@@ -867,7 +867,7 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.i18n.localize(
|
||||
"ui.panel.config.apps.dashboard.option.auto_update.title"
|
||||
@@ -893,7 +893,7 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
|
||||
!this._computeCannotIngressSidebar &&
|
||||
this._currentAddon.ingress
|
||||
? html`
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.i18n.localize(
|
||||
"ui.panel.config.apps.dashboard.option.ingress_panel.title"
|
||||
@@ -923,7 +923,7 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
|
||||
${
|
||||
this._computeUsesProtectedOptions
|
||||
? html`
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.i18n.localize(
|
||||
"ui.panel.config.apps.dashboard.option.protected.title"
|
||||
@@ -954,7 +954,7 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
|
||||
${
|
||||
this._currentAddon.version && this._currentAddon.state === "started"
|
||||
? html`<wa-divider></wa-divider>
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-supporting-text>
|
||||
<span slot="supporting-text">
|
||||
${this.i18n.localize(
|
||||
"ui.panel.config.apps.dashboard.hostname"
|
||||
@@ -1630,10 +1630,6 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
|
||||
color: var(--ha-color-text-secondary);
|
||||
}
|
||||
|
||||
.controls ha-row-item::part(supporting-text) {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.controls .content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
@@ -29,6 +29,10 @@ import type {
|
||||
AreaRegistryEntryMutableParams,
|
||||
} from "../../../data/area/area_registry";
|
||||
import { deleteAreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import {
|
||||
getAreaIconSuggestions,
|
||||
ROOM_ICONS,
|
||||
} from "../../../data/area_icon_suggestions";
|
||||
import {
|
||||
SENSOR_DEVICE_CLASS_HUMIDITY,
|
||||
SENSOR_DEVICE_CLASS_TEMPERATURE,
|
||||
@@ -96,6 +100,10 @@ class DialogAreaDetail
|
||||
|
||||
@state() private _submitting = false;
|
||||
|
||||
@state() private _suggestedIcons?: string[];
|
||||
|
||||
private _iconFollowsName = false;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _suggestionInclude: MetadataSuggestionInclude = {
|
||||
@@ -128,6 +136,7 @@ class DialogAreaDetail
|
||||
this._temperatureEntity = null;
|
||||
this._humidityEntity = null;
|
||||
}
|
||||
this._iconFollowsName = !this._params.entry;
|
||||
this._open = true;
|
||||
this._initDirtyTracking({ type: "deep" }, this._currentState());
|
||||
await this.updateComplete;
|
||||
@@ -151,6 +160,30 @@ class DialogAreaDetail
|
||||
return true;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("_name") || changedProps.has("_aliases")) {
|
||||
this._updateSuggestedIcons();
|
||||
}
|
||||
}
|
||||
|
||||
private _updateSuggestedIcons() {
|
||||
const icons = getAreaIconSuggestions(
|
||||
[this._name ?? "", ...(this._aliases ?? [])],
|
||||
this.hass.localize
|
||||
);
|
||||
this._suggestedIcons = icons;
|
||||
if (this._iconFollowsName) {
|
||||
const untouched = !this.isDirtyState;
|
||||
this._icon = icons[0] ?? null;
|
||||
if (untouched) {
|
||||
this._initDirtyTracking({ type: "deep" }, this._currentState());
|
||||
} else {
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _entityName(entityId: string | null): string | null {
|
||||
const stateObj = entityId ? this.hass.states[entityId] : undefined;
|
||||
return stateObj ? this.hass.formatEntityName(stateObj) : null;
|
||||
@@ -193,6 +226,7 @@ class DialogAreaDetail
|
||||
.value=${this._icon}
|
||||
@value-changed=${this._iconChanged}
|
||||
.label=${this.hass.localize("ui.panel.config.areas.editor.icon")}
|
||||
.suggestedIcons=${this._suggestedIcons}
|
||||
></ha-icon-picker>
|
||||
|
||||
<ha-floor-picker
|
||||
@@ -300,6 +334,7 @@ class DialogAreaDetail
|
||||
this._suggestionInclude = {
|
||||
...this._suggestionInclude,
|
||||
name: this._name.trim() === "",
|
||||
room: !this._icon || this._iconFollowsName,
|
||||
};
|
||||
|
||||
return generateMetadataSuggestionTask<{
|
||||
@@ -349,6 +384,11 @@ class DialogAreaDetail
|
||||
this._floor = processed.floor;
|
||||
}
|
||||
|
||||
if (processed.room && (!this._icon || this._iconFollowsName)) {
|
||||
this._icon = ROOM_ICONS[processed.room][0];
|
||||
this._iconFollowsName = false;
|
||||
}
|
||||
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
|
||||
@@ -464,6 +504,7 @@ class DialogAreaDetail
|
||||
private _iconChanged(ev) {
|
||||
this._error = undefined;
|
||||
this._icon = ev.detail.value;
|
||||
this._iconFollowsName = false;
|
||||
this._updateDirtyState(this._currentState());
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ class HaBackupConfigAddon extends LitElement {
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-list-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.backup.schedule.update_preference.label`
|
||||
@@ -55,7 +55,7 @@ class HaBackupConfigAddon extends LitElement {
|
||||
]}
|
||||
></ha-select>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(`ui.panel.config.backup.schedule.retention`)}
|
||||
</span>
|
||||
@@ -113,10 +113,6 @@ class HaBackupConfigAddon extends LitElement {
|
||||
ha-list-base {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
}
|
||||
ha-list-item-base::part(headline),
|
||||
ha-list-item-base::part(supporting-text) {
|
||||
white-space: wrap;
|
||||
}
|
||||
ha-select {
|
||||
min-width: 210px;
|
||||
}
|
||||
|
||||
@@ -197,7 +197,7 @@ class HaBackupConfigAgents extends LitElement {
|
||||
!this.cloudStatus.active_subscription;
|
||||
|
||||
return html`
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
${this._renderAgentIcon(agentId)}
|
||||
<div slot="headline" class="name">${name}</div>
|
||||
${
|
||||
@@ -248,7 +248,7 @@ class HaBackupConfigAgents extends LitElement {
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline>
|
||||
${this._renderAgentIcon(agentId)}
|
||||
<div slot="headline" class="name">${name}</div>
|
||||
<ha-icon-button
|
||||
@@ -317,10 +317,6 @@ class HaBackupConfigAgents extends LitElement {
|
||||
--mdc-icon-size: 48px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
ha-list-item-base::part(headline),
|
||||
ha-list-item-base::part(supporting-text) {
|
||||
white-space: wrap;
|
||||
}
|
||||
ha-list-item-base::part(end) {
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
|
||||
@@ -239,7 +239,7 @@ class HaBackupConfigData extends LitElement {
|
||||
return html`
|
||||
${this._renderSizeEstimate()}
|
||||
<ha-list-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon>
|
||||
<span slot="headline">
|
||||
${this.hass.localize("ui.panel.config.backup.data.ha_settings")}
|
||||
@@ -266,7 +266,7 @@ class HaBackupConfigData extends LitElement {
|
||||
|
||||
${
|
||||
this._showDbOption
|
||||
? html`<ha-list-item-base>
|
||||
? html`<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<ha-svg-icon slot="start" .path=${mdiChartBox}></ha-svg-icon>
|
||||
<span slot="headline">
|
||||
${this.hass.localize("ui.panel.config.backup.data.history")}
|
||||
@@ -288,7 +288,7 @@ class HaBackupConfigData extends LitElement {
|
||||
${
|
||||
isHassio
|
||||
? html`
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiPlayBoxMultiple}
|
||||
@@ -309,7 +309,7 @@ class HaBackupConfigData extends LitElement {
|
||||
></ha-switch>
|
||||
</ha-list-item-base>
|
||||
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<ha-svg-icon slot="start" .path=${mdiFolder}></ha-svg-icon>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
@@ -332,7 +332,7 @@ class HaBackupConfigData extends LitElement {
|
||||
${
|
||||
this._hasLocalAddons(this._addons)
|
||||
? html`
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiFolder}
|
||||
@@ -360,7 +360,7 @@ class HaBackupConfigData extends LitElement {
|
||||
${
|
||||
this._addons.length
|
||||
? html`
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiPuzzle}
|
||||
@@ -566,10 +566,6 @@ class HaBackupConfigData extends LitElement {
|
||||
ha-list-base {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
}
|
||||
ha-list-item-base::part(headline),
|
||||
ha-list-item-base::part(supporting-text) {
|
||||
white-space: wrap;
|
||||
}
|
||||
ha-list-item-base::part(start) {
|
||||
color: var(--ha-color-text-secondary);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ class HaBackupConfigEncryptionKey extends LitElement {
|
||||
if (this._value) {
|
||||
return html`
|
||||
<ha-list-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.download_emergency_kit"
|
||||
@@ -48,7 +48,7 @@ class HaBackupConfigEncryptionKey extends LitElement {
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.show_encryption_key"
|
||||
@@ -70,7 +70,7 @@ class HaBackupConfigEncryptionKey extends LitElement {
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.change_encryption_key"
|
||||
@@ -99,7 +99,7 @@ class HaBackupConfigEncryptionKey extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-list-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.encryption_key.set_encryption_key"
|
||||
@@ -152,10 +152,6 @@ class HaBackupConfigEncryptionKey extends LitElement {
|
||||
ha-list-base {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
}
|
||||
ha-list-item-base::part(headline),
|
||||
ha-list-item-base::part(supporting-text) {
|
||||
white-space: wrap;
|
||||
}
|
||||
ha-button[size="s"] ha-svg-icon {
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ class HaBackupConfigRetention extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${
|
||||
this.headline ??
|
||||
@@ -256,11 +256,6 @@ class HaBackupConfigRetention extends LitElement {
|
||||
ha-row-item::part(end) {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
ha-list-item-base::part(headline),
|
||||
ha-list-item-base::part(supporting-text) {
|
||||
white-space: wrap;
|
||||
}
|
||||
ha-select {
|
||||
min-width: 210px;
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-list-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.schedule.schedule"
|
||||
@@ -154,7 +154,7 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
)}
|
||||
outlined
|
||||
>
|
||||
<ha-row-item class="days">
|
||||
<ha-row-item wrap-headline class="days">
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.schedule.backup_every"
|
||||
@@ -186,7 +186,7 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
(data.recurrence === BackupScheduleRecurrence.CUSTOM_DAYS &&
|
||||
data.days.length > 0)
|
||||
? html`
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.schedule.time"
|
||||
@@ -231,7 +231,7 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
)}
|
||||
outlined
|
||||
>
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-headline wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.schedule.custom_time_label"
|
||||
@@ -266,7 +266,7 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
${
|
||||
this.supervisor
|
||||
? html`
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.backup.schedule.update_preference.label`
|
||||
@@ -415,12 +415,6 @@ class HaBackupConfigSchedule extends LitElement {
|
||||
ha-list-base {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
}
|
||||
ha-row-item::part(headline),
|
||||
ha-row-item::part(supporting-text),
|
||||
ha-list-item-base::part(headline),
|
||||
ha-list-item-base::part(supporting-text) {
|
||||
white-space: wrap;
|
||||
}
|
||||
ha-select {
|
||||
min-width: 210px;
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ class HaBackupOverviewBackups extends LitElement {
|
||||
return html`
|
||||
<ha-backup-summary-card .heading=${heading} .status=${status}>
|
||||
<ha-list-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline>
|
||||
<ha-svg-icon slot="start" .path=${mdiBackupRestore}></ha-svg-icon>
|
||||
<span slot="headline" class=${headline === null ? "skeleton" : ""}
|
||||
>${headline}</span
|
||||
@@ -74,7 +74,7 @@ class HaBackupOverviewBackups extends LitElement {
|
||||
${
|
||||
description || description === null
|
||||
? html`
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiCalendar}
|
||||
@@ -367,9 +367,6 @@ class HaBackupOverviewBackups extends LitElement {
|
||||
ha-list-item-base::part(start) {
|
||||
color: var(--ha-color-text-secondary);
|
||||
}
|
||||
ha-list-item-base::part(headline) {
|
||||
white-space: wrap;
|
||||
}
|
||||
span.skeleton {
|
||||
position: relative;
|
||||
display: block;
|
||||
|
||||
@@ -152,7 +152,7 @@ class HaConfigBackupDetails extends LitElement {
|
||||
${
|
||||
CLOUD_AGENT === this.agentId
|
||||
? html`
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.location_encrypted"
|
||||
@@ -179,7 +179,7 @@ class HaConfigBackupDetails extends LitElement {
|
||||
`
|
||||
: encrypted
|
||||
? html`
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.location_encrypted"
|
||||
@@ -213,7 +213,7 @@ class HaConfigBackupDetails extends LitElement {
|
||||
"ui.panel.config.backup.location.encryption.warning_encryption_turn_off_description"
|
||||
)}
|
||||
</ha-alert>
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.backup.location.encryption.location_unencrypted"
|
||||
@@ -364,9 +364,6 @@ class HaConfigBackupDetails extends LitElement {
|
||||
ha-backup-data-picker {
|
||||
display: block;
|
||||
}
|
||||
ha-row-item::part(supporting-text) {
|
||||
white-space: wrap;
|
||||
}
|
||||
.dot {
|
||||
display: block;
|
||||
position: relative;
|
||||
|
||||
@@ -170,7 +170,7 @@ export class CloudRemotePref extends LitElement {
|
||||
>
|
||||
<div class="card-content">
|
||||
<ha-list-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.external_activation"
|
||||
@@ -187,7 +187,7 @@ export class CloudRemotePref extends LitElement {
|
||||
@change=${this._toggleAllowRemoteEnabledChanged}
|
||||
></ha-switch>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.remote.certificate_info"
|
||||
@@ -311,9 +311,6 @@ export class CloudRemotePref extends LitElement {
|
||||
ha-list-base {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
}
|
||||
ha-list-item-base::part(supporting-text) {
|
||||
white-space: normal;
|
||||
}
|
||||
ha-tip {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
|
||||
@@ -199,7 +199,7 @@ export class CloudWebhooks extends LitElement {
|
||||
|
||||
private _renderHookRow(entry: Webhook) {
|
||||
return html`
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-headline wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${entry.name}
|
||||
${
|
||||
@@ -374,7 +374,6 @@ export class CloudWebhooks extends LitElement {
|
||||
}
|
||||
ha-row-item::part(headline),
|
||||
ha-row-item::part(supporting-text) {
|
||||
white-space: wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { dump } from "js-yaml";
|
||||
import type { AITaskStructure, GenDataTaskResult } from "../../../data/ai_task";
|
||||
import type { RoomType } from "../../../data/area_icon_suggestions";
|
||||
import { isRoomType, ROOM_TYPES } from "../../../data/area_icon_suggestions";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { SuggestWithAIGenerateTask } from "../../../components/ha-suggest-with-ai-button";
|
||||
import {
|
||||
@@ -14,6 +16,7 @@ export interface MetadataSuggestionResult {
|
||||
category?: string;
|
||||
labels?: string[];
|
||||
floor?: string;
|
||||
room?: RoomType;
|
||||
}
|
||||
|
||||
export type MetadataSuggestionDomain =
|
||||
@@ -25,6 +28,7 @@ export interface MetadataSuggestionInclude {
|
||||
categories?: boolean;
|
||||
labels?: boolean;
|
||||
floor?: boolean;
|
||||
room?: boolean;
|
||||
}
|
||||
|
||||
export const SUGGESTION_INCLUDE_DEFAULT: MetadataSuggestionInclude = {
|
||||
@@ -86,6 +90,7 @@ export async function generateMetadataSuggestionTask<T>(
|
||||
// providers reject a select/enum schema with an empty options list.
|
||||
const includeCategories = categoryOptions.length > 0;
|
||||
const includeFloor = floorOptions.length > 0;
|
||||
const includeRoom = Boolean(include.room) && domain === "area";
|
||||
|
||||
const structure: AITaskStructure = {
|
||||
...(include.name && {
|
||||
@@ -139,6 +144,20 @@ export async function generateMetadataSuggestionTask<T>(
|
||||
},
|
||||
},
|
||||
}),
|
||||
...(includeRoom && {
|
||||
room: {
|
||||
description: `The type of room that best matches the ${domain}`,
|
||||
required: false,
|
||||
selector: {
|
||||
select: {
|
||||
options: ROOM_TYPES.map((room) => ({
|
||||
value: room,
|
||||
label: room.replace(/_/g, " "),
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const requestedParts = [
|
||||
@@ -147,6 +166,7 @@ export async function generateMetadataSuggestionTask<T>(
|
||||
includeCategories ? "a category" : null,
|
||||
include.labels ? "labels" : null,
|
||||
includeFloor ? "a floor" : null,
|
||||
includeRoom ? "a room type" : null,
|
||||
].filter((entry): entry is string => entry !== null);
|
||||
|
||||
const categoryLabels: string[] = [
|
||||
@@ -233,6 +253,10 @@ export async function processMetadataSuggestion(
|
||||
const processed: MetadataSuggestionResult = {
|
||||
name: include.name ? result.data.name : undefined,
|
||||
description: include.description ? result.data.description : undefined,
|
||||
room:
|
||||
include.room && result.data.room && isRoomType(result.data.room)
|
||||
? result.data.room
|
||||
: undefined,
|
||||
};
|
||||
|
||||
// Convert category name to ID
|
||||
|
||||
@@ -542,7 +542,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
return html`
|
||||
${
|
||||
!this.hideName && this._device
|
||||
? html`<ha-row-item class="main-entity">
|
||||
? html`<ha-row-item wrap-supporting-text class="main-entity">
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.main_entity",
|
||||
@@ -740,7 +740,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
${
|
||||
SWITCH_AS_DOMAINS_INVERT.includes(this._switchAsDomain)
|
||||
? html`
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.invert.label"
|
||||
@@ -1017,7 +1017,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
${
|
||||
this._cameraPrefs
|
||||
? html`
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.stream.preload_stream"
|
||||
@@ -1035,7 +1035,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
@change=${this._handleCameraPrefsChanged}
|
||||
></ha-switch>
|
||||
</ha-row-item>
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.stream.stream_orientation"
|
||||
@@ -1074,6 +1074,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
this.helperConfigEntry.domain !== "switch_as_x"
|
||||
? html`
|
||||
<ha-list-item-button
|
||||
wrap-supporting-text
|
||||
class="menu-item"
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._showOptionsFlow}
|
||||
@@ -1106,6 +1107,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
: nothing
|
||||
}
|
||||
<ha-list-item-button
|
||||
wrap-supporting-text
|
||||
class="menu-item"
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._handleVoiceAssistantsClicked}
|
||||
@@ -1134,6 +1136,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
supportsFeature(stateObj, VacuumEntityFeature.CLEAN_AREA)
|
||||
? html`
|
||||
<ha-list-item-button
|
||||
wrap-supporting-text
|
||||
class="menu-item"
|
||||
.disabled=${this.disabled}
|
||||
@click=${this._handleVacuumSegmentMappingClicked}
|
||||
@@ -1171,7 +1174,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
: nothing
|
||||
}
|
||||
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.enabled_label"
|
||||
@@ -1211,7 +1214,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
: nothing
|
||||
}
|
||||
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.visible_label"
|
||||
@@ -1233,7 +1236,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
${
|
||||
this.entry.device_id
|
||||
? html`
|
||||
<ha-row-item class="device-area">
|
||||
<ha-row-item wrap-supporting-text class="device-area">
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.use_device_area"
|
||||
@@ -2013,11 +2016,6 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
ha-input.entityId ha-icon-button:last-child {
|
||||
margin-inline-start: 0;
|
||||
}
|
||||
|
||||
ha-row-item::part(supporting-text),
|
||||
ha-list-item-button::part(supporting-text) {
|
||||
white-space: normal;
|
||||
}
|
||||
ha-row-item {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
}
|
||||
|
||||
+11
-2
@@ -127,7 +127,8 @@ class DialogMatterAddDevice extends LitElement {
|
||||
|
||||
private get _waitForCommissioningFinish(): boolean {
|
||||
// When the app supports reporting Matter commissioning status, defer
|
||||
// advancing past the spinner until we receive matter/commission/finish.
|
||||
// advancing past the spinner of its own flow until we receive
|
||||
// matter/commission/finish.
|
||||
return !!this.hass.auth.external?.config.hasMatterStatusReport;
|
||||
}
|
||||
|
||||
@@ -135,7 +136,13 @@ class DialogMatterAddDevice extends LitElement {
|
||||
if (!this._newDevice) {
|
||||
return;
|
||||
}
|
||||
if (this._waitForCommissioningFinish && !this._commissioningFinished) {
|
||||
// Only the app's own flow, started from the "new" step, reports back.
|
||||
// The other steps commission through core or another app.
|
||||
if (
|
||||
this._step === "new" &&
|
||||
this._waitForCommissioningFinish &&
|
||||
!this._commissioningFinished
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._step = "device_added";
|
||||
@@ -149,6 +156,8 @@ class DialogMatterAddDevice extends LitElement {
|
||||
if (this._newDevice) {
|
||||
// Device already showed up in the registry — ignore the failure signal
|
||||
// and let the user finish the rename flow.
|
||||
this._commissioningFinished = true;
|
||||
this._maybeShowDeviceAdded();
|
||||
return;
|
||||
}
|
||||
showToast(this, {
|
||||
|
||||
+12
-7
@@ -178,6 +178,7 @@ export class MatterConfigDashboard extends LitElement {
|
||||
)}
|
||||
>
|
||||
<ha-list-item-button
|
||||
wrap-headline
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}&status=disabled`}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiDevices}></ha-svg-icon>
|
||||
@@ -190,6 +191,7 @@ export class MatterConfigDashboard extends LitElement {
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button
|
||||
wrap-headline
|
||||
href=${`/config/entities?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiShape}></ha-svg-icon>
|
||||
@@ -216,7 +218,11 @@ export class MatterConfigDashboard extends LitElement {
|
||||
"ui.panel.config.matter.panel.options_title"
|
||||
)}
|
||||
>
|
||||
<ha-list-item-button href="/config/matter/options">
|
||||
<ha-list-item-button
|
||||
wrap-headline
|
||||
wrap-supporting-text
|
||||
href="/config/matter/options"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiTune}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
@@ -232,7 +238,11 @@ export class MatterConfigDashboard extends LitElement {
|
||||
</ha-list-item-button>
|
||||
${
|
||||
isComponentLoaded(this.hass.config, "thread")
|
||||
? html`<ha-list-item-button href="/config/thread">
|
||||
? html`<ha-list-item-button
|
||||
wrap-headline
|
||||
wrap-supporting-text
|
||||
href="/config/thread"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${THREAD_ICON}
|
||||
@@ -299,11 +309,6 @@ export class MatterConfigDashboard extends LitElement {
|
||||
max-width: var(--ha-content-width-narrow);
|
||||
}
|
||||
|
||||
ha-list-item-button::part(headline),
|
||||
ha-list-item-button::part(supporting-text) {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.nav-card {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ class MatterOptionsPage extends LitElement {
|
||||
}
|
||||
${
|
||||
canCommissionMatterExternal(this.hass)
|
||||
? html`<ha-row-item>
|
||||
? html`<ha-row-item wrap-headline wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.mobile_app_commisioning"
|
||||
@@ -80,7 +80,7 @@ class MatterOptionsPage extends LitElement {
|
||||
</ha-row-item>`
|
||||
: nothing
|
||||
}
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-headline wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.commission_device"
|
||||
@@ -102,7 +102,7 @@ class MatterOptionsPage extends LitElement {
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-row-item>
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-headline wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.add_shared_device"
|
||||
@@ -124,7 +124,7 @@ class MatterOptionsPage extends LitElement {
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-row-item>
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-headline wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.set_wifi_credentials"
|
||||
@@ -146,7 +146,7 @@ class MatterOptionsPage extends LitElement {
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-row-item>
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-headline wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.set_thread_credentials"
|
||||
@@ -320,11 +320,6 @@ class MatterOptionsPage extends LitElement {
|
||||
max-width: var(--ha-content-width-narrow);
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
ha-row-item::part(headline),
|
||||
ha-row-item::part(supporting-text) {
|
||||
white-space: normal;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -221,6 +221,7 @@ export class MQTTConfigPanel extends LitElement {
|
||||
)}
|
||||
>
|
||||
<ha-list-item-button
|
||||
wrap-headline
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}&status=disabled`}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiDevices}></ha-svg-icon>
|
||||
@@ -232,6 +233,7 @@ export class MQTTConfigPanel extends LitElement {
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button
|
||||
wrap-headline
|
||||
href=${`/config/entities?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiShape}></ha-svg-icon>
|
||||
@@ -253,7 +255,11 @@ export class MQTTConfigPanel extends LitElement {
|
||||
<ha-card class="nav-card">
|
||||
<div class="card-content">
|
||||
<ha-list-base>
|
||||
<ha-list-item-button @click=${this._openOptionFlow}>
|
||||
<ha-list-item-button
|
||||
wrap-headline
|
||||
wrap-supporting-text
|
||||
@click=${this._openOptionFlow}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiTune}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.mqtt.option_flow")}
|
||||
@@ -265,7 +271,11 @@ export class MQTTConfigPanel extends LitElement {
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button @click=${this._openConfigFlow}>
|
||||
<ha-list-item-button
|
||||
wrap-headline
|
||||
wrap-supporting-text
|
||||
@click=${this._openConfigFlow}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiMqttLogo}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize("ui.panel.config.mqtt.config_flow")}
|
||||
@@ -440,10 +450,6 @@ export class MQTTConfigPanel extends LitElement {
|
||||
margin: 0 auto var(--ha-space-4);
|
||||
max-width: var(--ha-content-width-narrow);
|
||||
}
|
||||
ha-list-item-button::part(headline),
|
||||
ha-list-item-button::part(supporting-text) {
|
||||
white-space: normal;
|
||||
}
|
||||
ha-select {
|
||||
width: 96px;
|
||||
margin: 0 8px;
|
||||
|
||||
+1
-2
@@ -159,7 +159,7 @@ class ThreadNetworkInfoPage extends LitElement {
|
||||
if (value === null) {
|
||||
return nothing;
|
||||
}
|
||||
return html`<ha-list-item-base>
|
||||
return html`<ha-list-item-base wrap-supporting-text>
|
||||
<span slot="headline">${label}</span>
|
||||
<span slot="supporting-text">${value}</span>
|
||||
</ha-list-item-base>`;
|
||||
@@ -180,7 +180,6 @@ class ThreadNetworkInfoPage extends LitElement {
|
||||
|
||||
ha-list-item-base::part(supporting-text) {
|
||||
font-size: var(--ha-font-size-m);
|
||||
white-space: normal;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -237,6 +237,7 @@ class ZHAConfigDashboard extends LitElement {
|
||||
)}
|
||||
>
|
||||
<ha-list-item-button
|
||||
wrap-headline
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}&status=disabled`}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiDevices}></ha-svg-icon>
|
||||
@@ -249,6 +250,7 @@ class ZHAConfigDashboard extends LitElement {
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button
|
||||
wrap-headline
|
||||
href=${`/config/entities?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiShape}></ha-svg-icon>
|
||||
@@ -260,7 +262,7 @@ class ZHAConfigDashboard extends LitElement {
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button href="/config/zha/groups">
|
||||
<ha-list-item-button wrap-headline href="/config/zha/groups">
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiFolderMultipleOutline}
|
||||
@@ -307,7 +309,11 @@ class ZHAConfigDashboard extends LitElement {
|
||||
"ui.panel.config.zha.configuration_page.options_title"
|
||||
)}
|
||||
>
|
||||
<ha-list-item-button href="/config/zha/options">
|
||||
<ha-list-item-button
|
||||
wrap-headline
|
||||
wrap-supporting-text
|
||||
href="/config/zha/options"
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiTune}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
@@ -321,7 +327,11 @@ class ZHAConfigDashboard extends LitElement {
|
||||
</div>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-list-item-button>
|
||||
<ha-list-item-button href="/config/zha/network-info">
|
||||
<ha-list-item-button
|
||||
wrap-headline
|
||||
wrap-supporting-text
|
||||
href="/config/zha/network-info"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiInformationOutline}
|
||||
@@ -340,7 +350,10 @@ class ZHAConfigDashboard extends LitElement {
|
||||
</ha-list-item-button>
|
||||
${dynamicSections.map(
|
||||
(section) => html`
|
||||
<ha-list-item-button href=${`/config/zha/section/${section}`}>
|
||||
<ha-list-item-button
|
||||
wrap-headline
|
||||
href=${`/config/zha/section/${section}`}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiTune}></ha-svg-icon>
|
||||
<div slot="headline">
|
||||
${
|
||||
@@ -364,7 +377,7 @@ class ZHAConfigDashboard extends LitElement {
|
||||
<ha-card class="nav-card">
|
||||
<div class="card-content">
|
||||
<ha-list-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.download_backup"
|
||||
@@ -389,7 +402,7 @@ class ZHAConfigDashboard extends LitElement {
|
||||
)}
|
||||
</ha-progress-button>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.migrate_radio"
|
||||
@@ -523,13 +536,6 @@ class ZHAConfigDashboard extends LitElement {
|
||||
max-width: var(--ha-content-width-narrow);
|
||||
}
|
||||
|
||||
ha-list-item-base::part(headline),
|
||||
ha-list-item-base::part(supporting-text),
|
||||
ha-list-item-button::part(headline),
|
||||
ha-list-item-button::part(supporting-text) {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.nav-card .card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -54,7 +54,7 @@ class ZHANetworkInfoPage extends LitElement {
|
||||
${
|
||||
this._networkSettings
|
||||
? html`<ha-list-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.channel_label"
|
||||
@@ -74,7 +74,7 @@ class ZHANetworkInfoPage extends LitElement {
|
||||
@click=${this._showChannelMigrationDialog}
|
||||
></ha-icon-button>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-supporting-text>
|
||||
<span slot="headline">PAN ID</span>
|
||||
<span slot="supporting-text"
|
||||
>${
|
||||
@@ -82,7 +82,7 @@ class ZHANetworkInfoPage extends LitElement {
|
||||
}</span
|
||||
>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-supporting-text>
|
||||
<span slot="headline">Extended PAN ID</span>
|
||||
<span slot="supporting-text"
|
||||
>${
|
||||
@@ -91,13 +91,13 @@ class ZHANetworkInfoPage extends LitElement {
|
||||
}</span
|
||||
>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-supporting-text>
|
||||
<span slot="headline">Coordinator IEEE</span>
|
||||
<span slot="supporting-text"
|
||||
>${this._networkSettings.settings.node_info.ieee}</span
|
||||
>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.radio_type"
|
||||
@@ -107,7 +107,7 @@ class ZHANetworkInfoPage extends LitElement {
|
||||
>${this._networkSettings.radio_type}</span
|
||||
>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.serial_port"
|
||||
@@ -121,7 +121,7 @@ class ZHANetworkInfoPage extends LitElement {
|
||||
this._networkSettings.device.baudrate &&
|
||||
!this._networkSettings.device.path.startsWith("socket://")
|
||||
? html`
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.baudrate"
|
||||
@@ -177,7 +177,6 @@ class ZHANetworkInfoPage extends LitElement {
|
||||
|
||||
ha-list-item-base::part(supporting-text) {
|
||||
font-size: var(--ha-font-size-m);
|
||||
white-space: normal;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -116,7 +116,7 @@ class ZHAOptionsPage extends LitElement {
|
||||
this._configuration
|
||||
? html`
|
||||
<ha-list-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.enable_identify_on_join_label"
|
||||
@@ -136,7 +136,7 @@ class ZHAOptionsPage extends LitElement {
|
||||
@change=${this._enableIdentifyOnJoinChanged}
|
||||
></ha-switch>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.default_light_transition_label"
|
||||
@@ -161,7 +161,7 @@ class ZHAOptionsPage extends LitElement {
|
||||
<span slot="end">s</span>
|
||||
</ha-input>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.enhanced_light_transition_label"
|
||||
@@ -181,7 +181,7 @@ class ZHAOptionsPage extends LitElement {
|
||||
@change=${this._enhancedLightTransitionChanged}
|
||||
></ha-switch>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.light_transitioning_flag_label"
|
||||
@@ -201,7 +201,7 @@ class ZHAOptionsPage extends LitElement {
|
||||
@change=${this._lightTransitioningFlagChanged}
|
||||
></ha-switch>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.group_members_assume_state_label"
|
||||
@@ -221,7 +221,7 @@ class ZHAOptionsPage extends LitElement {
|
||||
@change=${this._groupMembersAssumeStateChanged}
|
||||
></ha-switch>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.consider_unavailable_mains_label"
|
||||
@@ -265,7 +265,7 @@ class ZHAOptionsPage extends LitElement {
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.consider_unavailable_battery_label"
|
||||
@@ -309,7 +309,7 @@ class ZHAOptionsPage extends LitElement {
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.configuration_page.enable_mains_startup_polling_label"
|
||||
@@ -463,11 +463,6 @@ class ZHAOptionsPage extends LitElement {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
ha-list-item-base::part(headline),
|
||||
ha-list-item-base::part(supporting-text) {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
ha-select,
|
||||
ha-input {
|
||||
min-width: 210px;
|
||||
|
||||
+1
-6
@@ -78,7 +78,7 @@ class ZWaveJSControllerStatistics extends SubscribeMixin(LitElement) {
|
||||
|
||||
private _renderStat(key: string) {
|
||||
return html`
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.zwave_js.dashboard.statistics.${key}.label`
|
||||
@@ -104,11 +104,6 @@ class ZWaveJSControllerStatistics extends SubscribeMixin(LitElement) {
|
||||
max-width: var(--ha-content-width-narrow);
|
||||
}
|
||||
|
||||
ha-list-item-base::part(headline),
|
||||
ha-list-item-base::part(supporting-text) {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
span[slot="end"] {
|
||||
font-size: 0.95em;
|
||||
color: var(--primary-text-color);
|
||||
|
||||
+4
-5
@@ -50,7 +50,7 @@ class ZWaveJSNetworkInfoPage extends LitElement {
|
||||
${
|
||||
this._network
|
||||
? html`<ha-list-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.dashboard.home_id"
|
||||
@@ -66,7 +66,7 @@ class ZWaveJSNetworkInfoPage extends LitElement {
|
||||
}
|
||||
</span>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.dashboard.driver_version"
|
||||
@@ -76,7 +76,7 @@ class ZWaveJSNetworkInfoPage extends LitElement {
|
||||
${this._network.client.driver_version}
|
||||
</span>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.dashboard.server_version"
|
||||
@@ -86,7 +86,7 @@ class ZWaveJSNetworkInfoPage extends LitElement {
|
||||
${this._network.client.server_version}
|
||||
</span>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.dashboard.server_url"
|
||||
@@ -120,7 +120,6 @@ class ZWaveJSNetworkInfoPage extends LitElement {
|
||||
|
||||
ha-list-item-base::part(supporting-text) {
|
||||
font-size: var(--ha-font-size-m);
|
||||
white-space: normal;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -62,7 +62,7 @@ class ZWaveJSOptionsPage extends LitElement {
|
||||
${
|
||||
this._network
|
||||
? html`<ha-list-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.common.rebuild_network_routes"
|
||||
@@ -85,7 +85,7 @@ class ZWaveJSOptionsPage extends LitElement {
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-list-item-base>
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<span slot="headline">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.common.remove_node"
|
||||
@@ -149,11 +149,6 @@ class ZWaveJSOptionsPage extends LitElement {
|
||||
max-width: var(--ha-content-width-narrow);
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
ha-list-item-base::part(headline),
|
||||
ha-list-item-base::part(supporting-text) {
|
||||
white-space: normal;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -64,7 +64,11 @@ class HaConfigSectionNetwork extends LitElement {
|
||||
>
|
||||
${NETWORK_BROWSERS.map(
|
||||
(domain) => html`
|
||||
<ha-list-item-button href="/config/${domain}">
|
||||
<ha-list-item-button
|
||||
wrap-headline
|
||||
wrap-supporting-text
|
||||
href="/config/${domain}"
|
||||
>
|
||||
<div slot="headline">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.network.discovery.${domain}`
|
||||
@@ -104,10 +108,6 @@ class HaConfigSectionNetwork extends LitElement {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.discovery-card ha-list-item-button::part(headline),
|
||||
.discovery-card ha-list-item-button::part(supporting-text) {
|
||||
white-space: normal;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../../components/item/ha-list-item-base";
|
||||
import "../../../../components/ha-switch";
|
||||
@@ -14,7 +14,7 @@ class HaDebugConnectionRow extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.tools.tabs.debug.debug_connection.title"
|
||||
@@ -43,13 +43,6 @@ class HaDebugConnectionRow extends LitElement {
|
||||
setDebugConnection(debugConnection);
|
||||
storeState(this.hass);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-list-item-base::part(headline),
|
||||
ha-list-item-base::part(supporting-text) {
|
||||
white-space: normal;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { setViewTransitionDisabled } from "../../../../common/util/view-transition";
|
||||
@@ -17,7 +17,7 @@ class HaDebugDisableViewTransitionRow extends LitElement {
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.tools.tabs.debug.disable_view_transition.title"
|
||||
@@ -41,13 +41,6 @@ class HaDebugDisableViewTransitionRow extends LitElement {
|
||||
this._disabled = (ev.target as HaSwitch).checked;
|
||||
setViewTransitionDisabled(this._disabled);
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-list-item-base::part(headline),
|
||||
ha-list-item-base::part(supporting-text) {
|
||||
white-space: normal;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
@@ -19,7 +19,7 @@ class HaDebugSafeAreaToolRow extends LitElement {
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-list-item-base>
|
||||
<ha-list-item-base wrap-headline wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this._localize(
|
||||
"ui.panel.config.tools.tabs.debug.safe_area_tool.title"
|
||||
@@ -42,13 +42,6 @@ class HaDebugSafeAreaToolRow extends LitElement {
|
||||
private _checkedChanged(ev: Event) {
|
||||
this._enabled = (ev.target as HaSwitch).checked;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-list-item-base::part(headline),
|
||||
ha-list-item-base::part(supporting-text) {
|
||||
white-space: normal;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -212,7 +212,7 @@ export class AssistPref extends LitElement {
|
||||
)}
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-button>
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-headline wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.config.voice_assistants.expose.expose_new_entities"
|
||||
@@ -415,10 +415,6 @@ export class AssistPref extends LitElement {
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
ha-row-item::part(headline),
|
||||
ha-row-item::part(supporting-text) {
|
||||
white-space: normal;
|
||||
}
|
||||
.header-actions {
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
|
||||
@@ -139,7 +139,7 @@ export class CloudAlexaPref extends LitElement {
|
||||
</ha-alert>`
|
||||
: nothing
|
||||
}
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-headline wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.config.cloud.account.alexa.expose_new_entities"
|
||||
@@ -160,7 +160,7 @@ export class CloudAlexaPref extends LitElement {
|
||||
${
|
||||
alexa_registered
|
||||
? html`
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-headline wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.config.cloud.account.alexa.enable_state_reporting"
|
||||
@@ -265,10 +265,6 @@ export class CloudAlexaPref extends LitElement {
|
||||
ha-row-item {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
}
|
||||
ha-row-item::part(headline),
|
||||
ha-row-item::part(supporting-text) {
|
||||
white-space: normal;
|
||||
}
|
||||
.header-actions {
|
||||
position: absolute;
|
||||
right: 24px;
|
||||
|
||||
@@ -138,7 +138,7 @@ export class CloudGooglePref extends LitElement {
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-headline wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.config.cloud.account.google.expose_new_entities"
|
||||
@@ -183,7 +183,7 @@ export class CloudGooglePref extends LitElement {
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-headline wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this.hass!.localize(
|
||||
"ui.panel.config.cloud.account.google.enable_state_reporting"
|
||||
@@ -201,7 +201,7 @@ export class CloudGooglePref extends LitElement {
|
||||
></ha-switch>
|
||||
</ha-row-item>
|
||||
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-headline wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.cloud.account.google.security_devices"
|
||||
@@ -347,10 +347,6 @@ export class CloudGooglePref extends LitElement {
|
||||
ha-row-item {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
}
|
||||
ha-row-item::part(headline),
|
||||
ha-row-item::part(supporting-text) {
|
||||
white-space: normal;
|
||||
}
|
||||
ha-input {
|
||||
width: 250px;
|
||||
margin-top: 8px;
|
||||
|
||||
@@ -193,7 +193,7 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
|
||||
const exposedToAssist = this.exposed.conversation;
|
||||
|
||||
return html`
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-headline>
|
||||
<h3 slot="headline">
|
||||
${this.hass.localize("ui.dialogs.voice-settings.expose_header")}
|
||||
</h3>
|
||||
@@ -227,7 +227,7 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
|
||||
this._googleEntity?.might_2fa;
|
||||
|
||||
return html`
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-headline wrap-supporting-text>
|
||||
<voice-assistant-brand-icon
|
||||
slot="start"
|
||||
.voiceAssistantId=${key}
|
||||
@@ -324,7 +324,7 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
|
||||
)}
|
||||
</ha-alert>`
|
||||
: html`
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-headline wrap-supporting-text>
|
||||
<span slot="headline">${this._computedName(this.entry)}</span>
|
||||
<span slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
@@ -457,10 +457,6 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
|
||||
ha-row-item {
|
||||
--ha-row-item-padding-inline: 0;
|
||||
}
|
||||
ha-row-item::part(headline),
|
||||
ha-row-item::part(supporting-text) {
|
||||
white-space: normal;
|
||||
}
|
||||
img {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
|
||||
@@ -118,7 +118,7 @@ export class HuiCardLayoutEditor extends LitElement {
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.layout.recommended_size"
|
||||
@@ -151,7 +151,7 @@ export class HuiCardLayoutEditor extends LitElement {
|
||||
.columnMax=${gridOptions.max_columns}
|
||||
.step=${this._preciseMode ? 1 : GRID_COLUMN_MULTIPLIER}
|
||||
></ha-grid-size-picker>
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.layout.fit_content"
|
||||
@@ -169,7 +169,7 @@ export class HuiCardLayoutEditor extends LitElement {
|
||||
name="auto-height"
|
||||
></ha-switch>
|
||||
</ha-row-item>
|
||||
<ha-row-item>
|
||||
<ha-row-item wrap-supporting-text>
|
||||
<span slot="headline"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.edit_card.layout.full_width"
|
||||
@@ -425,9 +425,6 @@ export class HuiCardLayoutEditor extends LitElement {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
ha-row-item::part(supporting-text) {
|
||||
white-space: normal;
|
||||
}
|
||||
ha-grid-size-picker {
|
||||
display: block;
|
||||
margin: 16px auto;
|
||||
|
||||
@@ -15,6 +15,8 @@ import {
|
||||
DataZoomComponent,
|
||||
VisualMapComponent,
|
||||
ToolboxComponent,
|
||||
MarkAreaComponent,
|
||||
BrushComponent,
|
||||
} from "echarts/components";
|
||||
|
||||
// Features like Universal Transition and Label Layout
|
||||
@@ -48,6 +50,8 @@ import type {
|
||||
GridComponentOption,
|
||||
DataZoomComponentOption,
|
||||
VisualMapComponentOption,
|
||||
MarkAreaComponentOption,
|
||||
BrushComponentOption,
|
||||
} from "echarts/components";
|
||||
import type { ComposeOption } from "echarts/core";
|
||||
|
||||
@@ -62,6 +66,8 @@ export type ECOption = ComposeOption<
|
||||
| GridComponentOption
|
||||
| DataZoomComponentOption
|
||||
| VisualMapComponentOption
|
||||
| MarkAreaComponentOption
|
||||
| BrushComponentOption
|
||||
| SankeySeriesOption
|
||||
| GraphSeriesOption
|
||||
| SunburstSeriesOption
|
||||
@@ -91,6 +97,8 @@ echarts.use([
|
||||
UniversalTransition,
|
||||
CanvasRenderer,
|
||||
ToolboxComponent,
|
||||
MarkAreaComponent,
|
||||
BrushComponent,
|
||||
]);
|
||||
|
||||
export { LinearGradient };
|
||||
|
||||
@@ -1106,6 +1106,46 @@
|
||||
"failed_create_floor": "Failed to create floor.",
|
||||
"unknown": "Unknown floor selected"
|
||||
},
|
||||
"rooms": {
|
||||
"keywords": {
|
||||
"living_room": "living room|lounge|family room|sitting room|den|living",
|
||||
"bedroom": "bedroom|guest room|master bedroom|master suite|bed room",
|
||||
"kids_room": "kids room|kids' room|kid's room|children's room|nursery|playroom|baby room",
|
||||
"kitchen": "kitchen|kitchenette",
|
||||
"dining_room": "dining room|dining area|dining",
|
||||
"bathroom": "bathroom|bath|shower room|ensuite|washroom",
|
||||
"toilet": "toilet|wc|restroom|powder room|loo|lavatory",
|
||||
"office": "office|study|home office|workspace|workroom",
|
||||
"laundry_room": "laundry|laundry room",
|
||||
"hallway": "hallway|hall|corridor|entrance|entryway|foyer|mudroom",
|
||||
"stairs": "stairs|staircase|stairway|stairwell|landing",
|
||||
"garage": "garage|carport",
|
||||
"garden": "garden|yard|backyard|front yard|lawn|greenhouse",
|
||||
"terrace": "terrace|patio|balcony|deck|porch|veranda",
|
||||
"basement": "basement|cellar",
|
||||
"attic": "attic|loft",
|
||||
"storage": "storage|storage room|pantry|closet|shed|storeroom",
|
||||
"utility_room": "utility room|boiler room|technical room|plant room",
|
||||
"pool": "pool|swimming pool|pool house",
|
||||
"media_room": "media room|home cinema|home theater|home theatre|cinema room|movie room|tv room",
|
||||
"game_room": "game room|games room|gaming room|recreation room|rec room|billiard room",
|
||||
"music_room": "music room|music studio|piano room|practice room",
|
||||
"library": "library|reading room|book room",
|
||||
"bar": "bar|home bar|pub|wine room|wine cellar",
|
||||
"dressing_room": "dressing room|walk-in closet|walk-in wardrobe",
|
||||
"workshop": "workshop|tool room|craft room|hobby room",
|
||||
"server_room": "server room|network room|rack room",
|
||||
"safe_room": "safe room|panic room|shelter|storm shelter|bunker",
|
||||
"prayer_room": "prayer room|chapel|meditation room",
|
||||
"sauna": "sauna|steam room",
|
||||
"outdoor_kitchen": "outdoor kitchen|summer kitchen|barbecue area|bbq area|grill area",
|
||||
"driveway": "driveway|parking|parking space"
|
||||
}
|
||||
},
|
||||
"icon_picker": {
|
||||
"suggested": "Suggested",
|
||||
"all_icons": "All icons"
|
||||
},
|
||||
"area-filter": {
|
||||
"title": "Areas",
|
||||
"no_areas": "No areas",
|
||||
@@ -3245,13 +3285,13 @@
|
||||
},
|
||||
"mcp": {
|
||||
"header": "Model Context Protocol",
|
||||
"description": "Add one of the URLs below to your AI agent. See the {documentation_link} for agent-specific instructions.",
|
||||
"description_disabled": "See the {documentation_link} to learn how to connect your AI agent to Home Assistant.",
|
||||
"description": "Add one of the URLs below to your AI app. See the {documentation_link} for app-specific instructions.",
|
||||
"description_disabled": "See the {documentation_link} to learn how to connect your AI app to Home Assistant.",
|
||||
"documentation": "documentation",
|
||||
"enable": "[%key:ui::common::enable%]",
|
||||
"disable": "[%key:ui::common::disable%]",
|
||||
"disable_confirm_title": "Disable the Model Context Protocol server?",
|
||||
"disable_confirm_text": "AI agents connected to the server will lose access to Home Assistant.",
|
||||
"disable_confirm_text": "AI apps connected to the server will lose access to Home Assistant.",
|
||||
"your_api_header": "Main URL",
|
||||
"apis_header": "Individual URLs",
|
||||
"copy_url": "Copy URL",
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
limitIceServers,
|
||||
limitIceServersToPath,
|
||||
} from "../../../src/common/webrtc/limit-ice-servers";
|
||||
|
||||
// Redundant URLs cost seconds of connection time, while dropping the wrong
|
||||
// one (the UDP or the firewall friendly TLS relay) breaks remote access.
|
||||
describe("limitIceServers", () => {
|
||||
it("works", () => {
|
||||
expect(limitIceServers(undefined)).toBeUndefined();
|
||||
expect(
|
||||
limitIceServers([
|
||||
{ urls: "stun:stun.example.com:3478" },
|
||||
{ urls: ["stun:a.example.com:3478", "stun:a.example.com:80"] },
|
||||
{
|
||||
urls: [
|
||||
"turn:t.example.com:3478?transport=udp",
|
||||
"turn:t.example.com:3478?transport=tcp",
|
||||
"turns:t.example.com:5349?transport=tcp",
|
||||
"turn:t.example.com:443?transport=udp",
|
||||
"turn:t.example.com:80?transport=tcp",
|
||||
"turns:t.example.com:443?transport=tcp",
|
||||
],
|
||||
username: "user",
|
||||
credential: "secret",
|
||||
},
|
||||
{
|
||||
urls: [
|
||||
"turn:u.example.com:80?transport=tcp",
|
||||
"turn:u.example.com:443?transport=tcp",
|
||||
],
|
||||
},
|
||||
// Different hosts are different network paths, all are kept
|
||||
{
|
||||
urls: [
|
||||
"stun:v.example.com",
|
||||
"turn:v.example.com:3478",
|
||||
"turn:v.example.com:3479",
|
||||
"turn:w.example.com:3478",
|
||||
],
|
||||
},
|
||||
])
|
||||
).toEqual([
|
||||
{ urls: "stun:stun.example.com:3478" },
|
||||
{ urls: ["stun:a.example.com:3478"] },
|
||||
{
|
||||
urls: [
|
||||
"turn:t.example.com:3478?transport=udp",
|
||||
"turns:t.example.com:443?transport=tcp",
|
||||
],
|
||||
username: "user",
|
||||
credential: "secret",
|
||||
},
|
||||
{ urls: ["turn:u.example.com:80?transport=tcp"] },
|
||||
{
|
||||
urls: [
|
||||
"stun:v.example.com",
|
||||
"turn:v.example.com:3478",
|
||||
"turn:w.example.com:3478",
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const iceServers = [
|
||||
{ urls: ["stun:a.example.com:3478", "stun:a.example.com:80"] },
|
||||
{
|
||||
urls: ["turn:t.example.com", "turns:t.example.com:443"],
|
||||
username: "user",
|
||||
},
|
||||
];
|
||||
// A direct path keeps the UDP relay, which is also used to find the
|
||||
// public address. A relayed one only needs its own relay.
|
||||
expect(limitIceServersToPath(iceServers, {})).toEqual([
|
||||
{ urls: ["stun:a.example.com:3478"] },
|
||||
{ urls: ["turn:t.example.com"], username: "user" },
|
||||
]);
|
||||
// Browsers report the URL with the port and transport that were left out
|
||||
expect(
|
||||
limitIceServersToPath(iceServers, {
|
||||
relayUrl: "turns:t.example.com:443?transport=tcp",
|
||||
})
|
||||
).toEqual([{ urls: ["turns:t.example.com:443"], username: "user" }]);
|
||||
expect(
|
||||
limitIceServersToPath(iceServers, {
|
||||
relayUrl: "turn:t.example.com:3478?transport=udp",
|
||||
})
|
||||
).toEqual([{ urls: ["turn:t.example.com"], username: "user" }]);
|
||||
expect(
|
||||
limitIceServersToPath(iceServers, { relayUrl: "turn:other.example.com" })
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -27,11 +27,9 @@ describe("selectCameraStreams", () => {
|
||||
selectCameraStreams({ supportedTypes: ["hls"], hlsStreams: failed })
|
||||
).toEqual([{ type: "mjpeg", visible: true }]);
|
||||
|
||||
// Loading: HLS is shown until WebRTC is ready, unless WebRTC goes first
|
||||
// Loading: HLS is shown until WebRTC is ready
|
||||
expect(select({})).toBe("hls (web_rtc)");
|
||||
expect(select({ hlsStreams: ok })).toBe("hls (web_rtc)");
|
||||
expect(select({ webRtcFirst: true })).toBe("web_rtc");
|
||||
expect(select({ webRtcFirst: true, hlsStreams: ok })).toBe("hls (web_rtc)");
|
||||
expect(select({ hlsStreams: failed })).toBe("web_rtc");
|
||||
|
||||
// WebRTC loaded: always preferred, also when only HLS has audio
|
||||
|
||||
@@ -1,12 +1,80 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { MatterCommissioningParameters } from "../../src/data/matter";
|
||||
import {
|
||||
matterShareRemainingSeconds,
|
||||
matterShareTargetExternal,
|
||||
watchForNewMatterDevice,
|
||||
} from "../../src/data/matter";
|
||||
import { subscribeDeviceRegistry } from "../../src/data/ws-device_registry";
|
||||
import type { ExternalConfig } from "../../src/external_app/external_messaging";
|
||||
import type { HomeAssistant } from "../../src/types";
|
||||
|
||||
// The device registry collection unsubscribes from its updates once no
|
||||
// subscriber is left, so an extra unsubscribe silently breaks every other one.
|
||||
describe("watchForNewMatterDevice", () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
const fakeRegistry = () => {
|
||||
const devices: string[] = ["existing"];
|
||||
const eventCallbacks = new Set<() => void>();
|
||||
const connection = {
|
||||
connected: true,
|
||||
sendMessagePromise: vi.fn(async () =>
|
||||
devices.map((id) => ({
|
||||
id,
|
||||
identifiers: [["matter", id]],
|
||||
connections: [],
|
||||
}))
|
||||
),
|
||||
subscribeEvents: vi.fn(async (callback: () => void) => {
|
||||
eventCallbacks.add(callback);
|
||||
return async () => {
|
||||
eventCallbacks.delete(callback);
|
||||
};
|
||||
}),
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
} as unknown as Connection;
|
||||
return {
|
||||
connection,
|
||||
eventCallbacks,
|
||||
addDevice: (id: string) => {
|
||||
devices.push(id);
|
||||
eventCallbacks.forEach((callback) => callback());
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
it("keeps the registry subscribed when the caller unsubscribes after a find", async () => {
|
||||
const registry = fakeRegistry();
|
||||
const hass = { connection: registry.connection } as HomeAssistant;
|
||||
subscribeDeviceRegistry(registry.connection, () => undefined);
|
||||
|
||||
const found = vi.fn();
|
||||
const unsub = watchForNewMatterDevice(hass, found);
|
||||
await vi.advanceTimersByTimeAsync(10);
|
||||
|
||||
registry.addDevice("new");
|
||||
await vi.advanceTimersByTimeAsync(600);
|
||||
expect(found).toHaveBeenCalledExactlyOnceWith(
|
||||
expect.objectContaining({ id: "new" })
|
||||
);
|
||||
|
||||
// The add device dialog unsubscribes again when it closes.
|
||||
unsub();
|
||||
// Past the collection's grace period for unsubscribing.
|
||||
await vi.advanceTimersByTimeAsync(6000);
|
||||
expect(registry.eventCallbacks.size).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
const codesOnly: MatterCommissioningParameters = {
|
||||
setup_pin_code: 20202021,
|
||||
setup_manual_code: "34970112332",
|
||||
|
||||
@@ -105,6 +105,7 @@ const run = ({
|
||||
tag === "dialog"
|
||||
? { showModal: dialog ? () => undefined : undefined }
|
||||
: fakeElement(tag),
|
||||
createTextNode: (text) => ({ tag: "#text", textContent: text }),
|
||||
getElementById: (id) => (id === "ha-launch-screen-info-box" ? box : null),
|
||||
};
|
||||
// eslint-disable-next-line no-new-func
|
||||
@@ -124,13 +125,15 @@ const run = ({
|
||||
{ now: () => now },
|
||||
(src) => started.push(src)
|
||||
);
|
||||
const buttons = Object.fromEntries(
|
||||
(box.children.find((child) => child.tag === "div")?.children ?? []).map(
|
||||
(button) => [button.textContent, button.listeners.click]
|
||||
)
|
||||
);
|
||||
const actions =
|
||||
box.children.find((child) => child.tag === "div")?.children ?? [];
|
||||
const continueAnyway = actions.find((child) => child.tag === "button")
|
||||
?.listeners.click;
|
||||
const checkbox = actions
|
||||
.find((child) => child.tag === "label")
|
||||
?.children.find((child) => child.tag === "input");
|
||||
const shown = box.children.map((child) => child.tag);
|
||||
return { started, box, store, buttons, shown };
|
||||
return { started, box, store, continueAnyway, checkbox, shown };
|
||||
};
|
||||
|
||||
describe("index.html legacy browser check", () => {
|
||||
@@ -144,11 +147,11 @@ describe("index.html legacy browser check", () => {
|
||||
["BigInt", { bigInt: false, dialog: true }],
|
||||
["native dialog", { bigInt: true, dialog: false }],
|
||||
])("offers to continue without %s", (_, browser) => {
|
||||
const { started, box, store, buttons } = run(browser);
|
||||
const { started, box, store, continueAnyway, checkbox } = run(browser);
|
||||
expect(started).toEqual([]);
|
||||
expect(Object.keys(buttons)).toEqual(["Continue anyway"]);
|
||||
expect(checkbox).toBeUndefined();
|
||||
|
||||
buttons["Continue anyway"]();
|
||||
continueAnyway();
|
||||
expect(started).toEqual([ENTRY]);
|
||||
expect(store[ACCEPTED_KEY]).toBe(String(NOW));
|
||||
// The message stays up while the app loads.
|
||||
@@ -165,17 +168,16 @@ describe("index.html legacy browser check", () => {
|
||||
const stored = String(NOW - 30 * DAY);
|
||||
const reminder = run({ stored });
|
||||
expect(reminder.started).toEqual([]);
|
||||
expect(Object.keys(reminder.buttons)).toEqual([
|
||||
"Continue anyway",
|
||||
"Don't show again",
|
||||
]);
|
||||
expect(reminder.checkbox.type).toBe("checkbox");
|
||||
|
||||
// Unchecked: starts the next month.
|
||||
const continued = run({ stored });
|
||||
continued.buttons["Continue anyway"]();
|
||||
continued.continueAnyway();
|
||||
expect(continued.started).toEqual([ENTRY]);
|
||||
expect(continued.store[ACCEPTED_KEY]).toBe(String(NOW));
|
||||
|
||||
reminder.buttons["Don't show again"]();
|
||||
reminder.checkbox.checked = true;
|
||||
reminder.continueAnyway();
|
||||
expect(reminder.started).toEqual([ENTRY]);
|
||||
expect(reminder.store[ACCEPTED_KEY]).toBe("always");
|
||||
expect(run({ stored: "always", now: NOW + 3650 * DAY }).started).toEqual([
|
||||
@@ -185,15 +187,15 @@ describe("index.html legacy browser check", () => {
|
||||
|
||||
// A tablet that lost its clock.
|
||||
it("asks again when the clock is behind the stored time", () => {
|
||||
const { started, buttons } = run({ stored: String(NOW + DAY) });
|
||||
const { started, continueAnyway } = run({ stored: String(NOW + DAY) });
|
||||
expect(started).toEqual([]);
|
||||
expect(buttons["Continue anyway"]).toBeDefined();
|
||||
expect(continueAnyway).toBeDefined();
|
||||
});
|
||||
|
||||
it("still continues when storage is unavailable", () => {
|
||||
const { started, buttons } = run({ storageThrows: true });
|
||||
const { started, continueAnyway } = run({ storageThrows: true });
|
||||
expect(started).toEqual([]);
|
||||
buttons["Continue anyway"]();
|
||||
continueAnyway();
|
||||
expect(started).toEqual([ENTRY]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user