Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d1372d873 | |||
| f75b1f2a20 | |||
| 8894c357ec | |||
| 479b25532b | |||
| ac120ad309 | |||
| 5a2e4b0da5 | |||
| 9e56fd379e | |||
| 109d21aa81 | |||
| aacb8e3c09 | |||
| 6def14718d |
@@ -41,14 +41,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
||||
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
||||
uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -62,4 +62,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
||||
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
|
||||
@@ -13,11 +13,13 @@ jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@89ae32b08ed1a541efecbab17912962a5e38981c # v6.0.2
|
||||
- uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
process-only: "issues, prs"
|
||||
issue-inactive-days: "30"
|
||||
issue-lock-inactive-days: "30"
|
||||
issue-exclude-created-before: "2020-10-01T00:00:00Z"
|
||||
issue-lock-reason: ""
|
||||
pr-inactive-days: "1"
|
||||
pr-lock-inactive-days: "1"
|
||||
pr-exclude-created-before: "2020-11-01T00:00:00Z"
|
||||
pr-lock-reason: ""
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@868e6cb4607727d764341a158d98872cd63fa658 # master
|
||||
uses: home-assistant/actions/helpers/verify-version@f6f29a7ee3fa0eccadf3620a7b9ee00ab54ec03b # master
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: 90 days stale policy
|
||||
uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
|
||||
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 90
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "@material/mwc-top-app-bar-fixed";
|
||||
import { html, css, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import "../../src/components/ha-icon-button";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import "@material/mwc-top-app-bar-fixed";
|
||||
import { mdiMenu, mdiSwapHorizontal } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
@@ -10,7 +11,6 @@ import type { HaDrawer } from "../../src/components/ha-drawer";
|
||||
import { HaExpansionPanel } from "../../src/components/ha-expansion-panel";
|
||||
import "../../src/components/ha-icon-button";
|
||||
import "../../src/components/ha-svg-icon";
|
||||
import "../../src/components/ha-top-app-bar-fixed";
|
||||
import "../../src/managers/notification-manager";
|
||||
import { haStyle } from "../../src/resources/styles";
|
||||
import { PAGES, SIDEBAR } from "../build/import-pages";
|
||||
@@ -84,7 +84,7 @@ class HaGallery extends LitElement {
|
||||
<div class="drawer-title">Home Assistant Design</div>
|
||||
<div class="sidebar">${sidebar}</div>
|
||||
<div slot="appContent" class="app-content">
|
||||
<ha-top-app-bar-fixed>
|
||||
<mwc-top-app-bar-fixed>
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
@click=${this._menuTapped}
|
||||
@@ -94,7 +94,7 @@ class HaGallery extends LitElement {
|
||||
<div slot="title">
|
||||
${PAGES[this._page].metadata.title || this._page.split("/")[1]}
|
||||
</div>
|
||||
</ha-top-app-bar-fixed>
|
||||
</mwc-top-app-bar-fixed>
|
||||
<div class="content">
|
||||
${PAGES[this._page].description
|
||||
? html`
|
||||
@@ -227,12 +227,11 @@ class HaGallery extends LitElement {
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
--ha-sidebar-width: 256px;
|
||||
--header-height: 64px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
box-sizing: border-box;
|
||||
max-height: calc(100vh - var(--header-height));
|
||||
max-height: calc(100vh - 64px);
|
||||
overflow-y: auto;
|
||||
padding: 4px;
|
||||
}
|
||||
@@ -244,7 +243,7 @@ class HaGallery extends LitElement {
|
||||
display: flex;
|
||||
font-size: var(--ha-font-size-l);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
min-height: var(--header-height);
|
||||
min-height: 64px;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
@@ -278,10 +277,6 @@ class HaGallery extends LitElement {
|
||||
background: var(--primary-background-color);
|
||||
}
|
||||
|
||||
ha-drawer[type="dismissible"][open] ha-top-app-bar-fixed {
|
||||
--ha-top-app-bar-width: calc(100% - var(--ha-sidebar-width));
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { provide } from "@lit/context";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
|
||||
import "../../../../src/components/ha-card";
|
||||
import type { TemplateResult, PropertyValues } from "lit";
|
||||
import { html, css, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import "../../../../src/components/ha-tip";
|
||||
import { internationalizationContext } from "../../../../src/data/context";
|
||||
import type { HomeAssistantInternationalization } from "../../../../src/types";
|
||||
import "../../../../src/components/ha-card";
|
||||
import { applyThemesOnElement } from "../../../../src/common/dom/apply_themes_on_element";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
|
||||
const tips: (string | TemplateResult)[] = [
|
||||
"Test tip",
|
||||
@@ -16,25 +14,16 @@ const tips: (string | TemplateResult)[] = [
|
||||
|
||||
@customElement("demo-components-ha-tip")
|
||||
export class DemoHaTip extends LitElement {
|
||||
@provide({ context: internationalizationContext })
|
||||
@state()
|
||||
protected _i18n: HomeAssistantInternationalization = {
|
||||
localize: ((key: string) => key) as any,
|
||||
language: "en",
|
||||
selectedLanguage: null,
|
||||
locale: {} as any,
|
||||
translationMetadata: {} as any,
|
||||
loadBackendTranslation: (async () => (key: string) => key) as any,
|
||||
loadFragmentTranslation: (async () => (key: string) => key) as any,
|
||||
};
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html` ${["light", "dark"].map(
|
||||
(mode) => html`
|
||||
<div class=${mode}>
|
||||
<ha-card header="ha-tip ${mode} demo">
|
||||
<div class="card-content">
|
||||
${tips.map((tip) => html`<ha-tip>${tip}</ha-tip>`)}
|
||||
${tips.map(
|
||||
(tip) =>
|
||||
html`<ha-tip .hass=${provideHass(this)}>${tip}</ha-tip>`
|
||||
)}
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.29.7",
|
||||
"@babel/runtime": "7.29.2",
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
"@codemirror/autocomplete": "6.20.2",
|
||||
"@codemirror/commands": "6.10.3",
|
||||
@@ -40,15 +40,15 @@
|
||||
"@codemirror/view": "6.43.0",
|
||||
"@date-fns/tz": "1.5.0",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.4.7",
|
||||
"@formatjs/intl-displaynames": "7.3.9",
|
||||
"@formatjs/intl-durationformat": "0.10.13",
|
||||
"@formatjs/intl-datetimeformat": "7.4.6",
|
||||
"@formatjs/intl-displaynames": "7.3.8",
|
||||
"@formatjs/intl-durationformat": "0.10.12",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.9",
|
||||
"@formatjs/intl-listformat": "8.3.9",
|
||||
"@formatjs/intl-listformat": "8.3.8",
|
||||
"@formatjs/intl-locale": "5.3.8",
|
||||
"@formatjs/intl-numberformat": "9.3.10",
|
||||
"@formatjs/intl-pluralrules": "6.3.9",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.9",
|
||||
"@formatjs/intl-numberformat": "9.3.9",
|
||||
"@formatjs/intl-pluralrules": "6.3.8",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.8",
|
||||
"@fullcalendar/core": "6.1.20",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"@fullcalendar/interaction": "6.1.20",
|
||||
@@ -62,13 +62,17 @@
|
||||
"@lit-labs/virtualizer": "2.1.1",
|
||||
"@lit/context": "1.1.6",
|
||||
"@lit/reactive-element": "2.1.2",
|
||||
"@material/mwc-base": "0.27.0",
|
||||
"@material/mwc-formfield": "patch:@material/mwc-formfield@npm%3A0.27.0#~/.yarn/patches/@material-mwc-formfield-npm-0.27.0-9528cb60f6.patch",
|
||||
"@material/mwc-list": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
|
||||
"@material/mwc-top-app-bar": "0.27.0",
|
||||
"@material/mwc-top-app-bar-fixed": "0.27.0",
|
||||
"@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
|
||||
"@material/web": "2.4.1",
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@swc/helpers": "0.5.23",
|
||||
"@swc/helpers": "0.5.21",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "4.0.5",
|
||||
"@tsparticles/preset-links": "4.0.5",
|
||||
@@ -86,7 +90,7 @@
|
||||
"deep-clone-simple": "1.1.1",
|
||||
"deep-freeze": "0.0.1",
|
||||
"dialog-polyfill": "0.5.6",
|
||||
"echarts": "6.1.0",
|
||||
"echarts": "6.0.0",
|
||||
"element-internals-polyfill": "3.0.2",
|
||||
"fuse.js": "7.3.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
@@ -125,20 +129,21 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.29.7",
|
||||
"@babel/core": "7.29.0",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.8",
|
||||
"@babel/plugin-transform-runtime": "7.29.7",
|
||||
"@babel/preset-env": "7.29.7",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.22.2",
|
||||
"@babel/plugin-transform-runtime": "7.29.0",
|
||||
"@babel/preset-env": "7.29.5",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.22.1",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@html-eslint/eslint-plugin": "0.61.0",
|
||||
"@html-eslint/eslint-plugin": "0.60.0",
|
||||
"@lokalise/node-api": "16.0.0",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.1.0",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.12",
|
||||
"@rspack/core": "2.0.5",
|
||||
"@rspack/dev-server": "2.0.3",
|
||||
"@rsdoctor/rspack-plugin": "1.5.11",
|
||||
"@rspack/core": "2.0.4",
|
||||
"@rspack/dev-server": "2.0.1",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@types/chromecast-caf-receiver": "6.0.26",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
"@types/color-name": "2.0.0",
|
||||
@@ -150,9 +155,11 @@
|
||||
"@types/leaflet.markercluster": "1.5.6",
|
||||
"@types/lodash.merge": "4.6.9",
|
||||
"@types/luxon": "3.7.1",
|
||||
"@types/mocha": "10.0.10",
|
||||
"@types/qrcode": "1.5.6",
|
||||
"@types/sortablejs": "1.15.9",
|
||||
"@types/tar": "7.0.87",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@vitest/coverage-v8": "4.1.7",
|
||||
"babel-loader": "10.1.1",
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
@@ -179,7 +186,7 @@
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "29.1.1",
|
||||
"jszip": "3.10.1",
|
||||
"license-checker-rseidelsohn": "5.0.1",
|
||||
"license-checker-rseidelsohn": "4.4.2",
|
||||
"lint-staged": "17.0.5",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
@@ -191,10 +198,10 @@
|
||||
"serve": "14.2.6",
|
||||
"sinon": "22.0.0",
|
||||
"tar": "7.5.15",
|
||||
"terser-webpack-plugin": "5.6.1",
|
||||
"terser-webpack-plugin": "5.6.0",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.60.0",
|
||||
"typescript-eslint": "8.59.4",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.1.7",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20260527.0"
|
||||
version = "20260429.0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { HassEntities, HassEntity } from "home-assistant-js-websocket";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
HomeAssistantInternationalization,
|
||||
} from "../../types";
|
||||
import {
|
||||
entitiesContext,
|
||||
internationalizationContext,
|
||||
statesContext,
|
||||
} from "../../data/context";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { entitiesContext, statesContext } from "../../data/context";
|
||||
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
|
||||
import type { LocalizeFunc } from "../translations/localize";
|
||||
import { ensureArray } from "../array/ensure-array";
|
||||
import { transform } from "./transform";
|
||||
|
||||
interface ConsumeEntryConfig {
|
||||
@@ -27,28 +18,6 @@ const resolveAtPath = (host: unknown, path: readonly string[]) => {
|
||||
return cur;
|
||||
};
|
||||
|
||||
/** Reuse `previous` when every entry still references the same `HassEntity`. */
|
||||
export const preserveUnchangedEntityStatesRecord = <
|
||||
T extends Record<string, HassEntity | undefined>,
|
||||
>(
|
||||
previous: T | undefined,
|
||||
next: T
|
||||
): T => {
|
||||
if (!previous) {
|
||||
return next;
|
||||
}
|
||||
const nextKeys = Object.keys(next);
|
||||
if (Object.keys(previous).length !== nextKeys.length) {
|
||||
return next;
|
||||
}
|
||||
for (const key of nextKeys) {
|
||||
if (previous[key] !== next[key]) {
|
||||
return next;
|
||||
}
|
||||
}
|
||||
return previous;
|
||||
};
|
||||
|
||||
const composeDecorator = <T, V>(
|
||||
context: Parameters<typeof consume>[0]["context"],
|
||||
watchKey: string | undefined,
|
||||
@@ -86,52 +55,27 @@ export const consumeEntityState = (config: ConsumeEntryConfig) =>
|
||||
);
|
||||
|
||||
/**
|
||||
* Like {@link consumeEntityState} but for one or more entity IDs at
|
||||
* `entityIdPath` (a string or string array; wrapped with {@link ensureArray}).
|
||||
* Resolves to a record keyed by entity ID containing the currently-available
|
||||
* entities (missing entities and non-string IDs are filtered out). Returns the
|
||||
* previous record when none of the selected entities changed.
|
||||
* Like {@link consumeEntityState} but for an array of entity IDs at
|
||||
* `entityIdPath`. Resolves to a `HassEntity[]` containing one entry per
|
||||
* currently-available entity (missing entities and non-string IDs are
|
||||
* filtered out; original order is preserved).
|
||||
*/
|
||||
export const consumeEntityStates = (config: ConsumeEntryConfig) => {
|
||||
const watchKey = config.entityIdPath[0];
|
||||
const buildRecord = function (this: unknown, states: HassEntities) {
|
||||
const ids = ensureArray(resolveAtPath(this, config.entityIdPath));
|
||||
if (!ids || !states) return undefined;
|
||||
const result: Record<string, HassEntity> = {};
|
||||
for (const id of ids) {
|
||||
if (typeof id !== "string") continue;
|
||||
const state = states[id];
|
||||
if (state !== undefined) result[id] = state;
|
||||
export const consumeEntityStates = (config: ConsumeEntryConfig) =>
|
||||
composeDecorator<HassEntities, HassEntity[]>(
|
||||
statesContext,
|
||||
config.entityIdPath[0],
|
||||
function (states) {
|
||||
const ids = resolveAtPath(this, config.entityIdPath);
|
||||
if (!Array.isArray(ids) || !states) return undefined;
|
||||
const result: HassEntity[] = [];
|
||||
for (const id of ids) {
|
||||
if (typeof id !== "string") continue;
|
||||
const state = states[id];
|
||||
if (state !== undefined) result.push(state);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
return (proto: unknown, propertyKey: string) => {
|
||||
const key = String(propertyKey);
|
||||
const transformDec = transform<
|
||||
HassEntities,
|
||||
Record<string, HassEntity> | undefined
|
||||
>({
|
||||
transformer: function (this: unknown, states: HassEntities) {
|
||||
const next = buildRecord.call(this, states);
|
||||
if (next === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const previous = (this as Record<string, unknown>)[
|
||||
`__transform_${key}`
|
||||
] as Record<string, HassEntity> | undefined;
|
||||
return preserveUnchangedEntityStatesRecord(previous, next);
|
||||
},
|
||||
watch: watchKey ? [watchKey] : [],
|
||||
});
|
||||
const consumeDec = consume<any>({
|
||||
context: statesContext,
|
||||
subscribe: true,
|
||||
});
|
||||
transformDec(proto as never, propertyKey);
|
||||
consumeDec(proto as never, propertyKey);
|
||||
};
|
||||
};
|
||||
);
|
||||
|
||||
/**
|
||||
* Consumes `entitiesContext` and narrows it to the
|
||||
@@ -147,15 +91,3 @@ export const consumeEntityRegistryEntry = (config: ConsumeEntryConfig) =>
|
||||
return typeof id === "string" ? entities?.[id] : undefined;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Consumes `internationalizationContext` and narrows it to the `localize`
|
||||
* function. No host watching is needed — the decorated property updates
|
||||
* whenever the i18n context changes.
|
||||
*/
|
||||
export const consumeLocalize = () =>
|
||||
composeDecorator<HomeAssistantInternationalization, LocalizeFunc>(
|
||||
internationalizationContext,
|
||||
undefined,
|
||||
({ localize }) => localize
|
||||
);
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import type { PickerComboBoxItem } from "../../components/ha-picker-combo-box";
|
||||
import type { RelatedResult } from "../../data/search";
|
||||
|
||||
export interface RelatedIdSets {
|
||||
areas: Set<string>;
|
||||
devices: Set<string>;
|
||||
entities: Set<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a set of related IDs for a given related result.
|
||||
* @param related - The related result to build the sets from.
|
||||
* @returns The related ID sets.
|
||||
*/
|
||||
export const buildRelatedIdSets = (related?: RelatedResult): RelatedIdSets => ({
|
||||
areas: new Set(related?.area || []),
|
||||
devices: new Set(related?.device || []),
|
||||
entities: new Set(related?.entity || []),
|
||||
});
|
||||
|
||||
/**
|
||||
* Stable partition sort: related items float to the top,
|
||||
* preserving relative order (e.g. Fuse score) within each group.
|
||||
* @param items - The items to sort.
|
||||
* @returns The sorted items.
|
||||
*/
|
||||
export const sortRelatedFirst = (
|
||||
items: PickerComboBoxItem[]
|
||||
): PickerComboBoxItem[] =>
|
||||
[...items].sort((a, b) => {
|
||||
const aRelated = Boolean(a.isRelated);
|
||||
const bRelated = Boolean(b.isRelated);
|
||||
if (aRelated === bRelated) {
|
||||
return 0;
|
||||
}
|
||||
return aRelated ? -1 : 1;
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../ha-tooltip";
|
||||
|
||||
export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
|
||||
|
||||
@@ -13,7 +12,6 @@ export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
|
||||
*
|
||||
* @attr {"pass"|"fail"|"invalid"|"unknown"} state - The current live-test state. Defaults to `unknown`.
|
||||
* @attr {string} label - Accessible label announced by assistive technology.
|
||||
* @attr {string} message - Optional tooltip body shown on hover/focus.
|
||||
*/
|
||||
@customElement("ha-automation-row-live-test")
|
||||
export class HaAutomationRowLiveTest extends LitElement {
|
||||
@@ -21,8 +19,6 @@ export class HaAutomationRowLiveTest extends LitElement {
|
||||
|
||||
@property() public label = "";
|
||||
|
||||
@property() public message?: string;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div
|
||||
@@ -31,9 +27,6 @@ export class HaAutomationRowLiveTest extends LitElement {
|
||||
tabindex="0"
|
||||
aria-label=${this.label}
|
||||
></div>
|
||||
${this.message
|
||||
? html`<ha-tooltip for="indicator">${this.message}</ha-tooltip>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import type {
|
||||
ECElementEvent,
|
||||
LegendComponentOption,
|
||||
LineSeriesOption,
|
||||
TooltipOption,
|
||||
XAXisOption,
|
||||
YAXisOption,
|
||||
} from "echarts/types/dist/shared";
|
||||
@@ -30,59 +29,22 @@ import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { listenMediaQuery } from "../../common/dom/media_query";
|
||||
import { afterNextRender } from "../../common/util/render-status";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
import { uiContext } from "../../data/context";
|
||||
import type { Themes } from "../../data/ws-themes";
|
||||
import type {
|
||||
ECOption,
|
||||
HaECOption,
|
||||
HaECSeries,
|
||||
HaECSeriesItem,
|
||||
HaTooltipOption,
|
||||
} from "../../resources/echarts/echarts";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
import type { HomeAssistant, HomeAssistantUI } from "../../types";
|
||||
import { isMac } from "../../util/is_mac";
|
||||
import "../chips/ha-assist-chip";
|
||||
import "../ha-icon-button";
|
||||
import { formatTimeLabel } from "./axis-label";
|
||||
import { downSampleLineData } from "./down-sample";
|
||||
import { wrapLitTooltipFormatter } from "./lit-tooltip-formatter";
|
||||
|
||||
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
|
||||
const LEGEND_OVERFLOW_LIMIT = 10;
|
||||
const LEGEND_OVERFLOW_LIMIT_MOBILE = 6;
|
||||
const DOUBLE_TAP_TIME = 300;
|
||||
|
||||
type RawSeriesOption = Exclude<
|
||||
NonNullable<ECOption["series"]>,
|
||||
readonly unknown[]
|
||||
>;
|
||||
|
||||
const toEChartsFormatter = (
|
||||
fn: ReturnType<typeof wrapLitTooltipFormatter>
|
||||
): NonNullable<TooltipOption["formatter"]> =>
|
||||
fn as NonNullable<TooltipOption["formatter"]>;
|
||||
|
||||
const convertHaTooltipFormatter = (tooltip: HaTooltipOption): TooltipOption => {
|
||||
const { formatter, ...rest } = tooltip;
|
||||
const next: TooltipOption = { ...rest };
|
||||
if (typeof formatter === "function") {
|
||||
next.formatter = toEChartsFormatter(wrapLitTooltipFormatter(formatter));
|
||||
} else if (formatter !== undefined) {
|
||||
next.formatter = formatter;
|
||||
}
|
||||
return next;
|
||||
};
|
||||
|
||||
const processSeriesTooltipFormatter = (s: HaECSeriesItem): RawSeriesOption => {
|
||||
if (s.tooltip && typeof s.tooltip.formatter === "function") {
|
||||
return {
|
||||
...s,
|
||||
tooltip: convertHaTooltipFormatter(s.tooltip),
|
||||
} as RawSeriesOption;
|
||||
}
|
||||
return s as RawSeriesOption;
|
||||
};
|
||||
|
||||
export type CustomLegendOption = ECOption["legend"] & {
|
||||
type: "custom";
|
||||
data?: {
|
||||
@@ -104,9 +66,9 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public data: HaECSeries = [];
|
||||
@property({ attribute: false }) public data: ECOption["series"] = [];
|
||||
|
||||
@property({ attribute: false }) public options?: HaECOption;
|
||||
@property({ attribute: false }) public options?: ECOption;
|
||||
|
||||
@property({ type: String }) public height?: string;
|
||||
|
||||
@@ -652,7 +614,7 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
// Return an array of all IDs associated with the legend item of the primaryId
|
||||
private _getAllIdsFromLegend(
|
||||
options: HaECOption | undefined,
|
||||
options: ECOption | undefined,
|
||||
primaryId: string
|
||||
): string[] {
|
||||
if (!options) return [primaryId];
|
||||
@@ -672,7 +634,7 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
// Parses the options structure and adds all ids of unselected legend items to hiddenDatasets.
|
||||
// No known need to remove items at this time.
|
||||
private _updateHiddenStatsFromOptions(options: HaECOption | undefined) {
|
||||
private _updateHiddenStatsFromOptions(options: ECOption | undefined) {
|
||||
if (!options) return;
|
||||
const legend = ensureArray(this.options?.legend || [])[0] as
|
||||
| LegendComponentOption
|
||||
@@ -795,34 +757,22 @@ export class HaChartBase extends LitElement {
|
||||
xAxis,
|
||||
};
|
||||
|
||||
if (options.tooltip) {
|
||||
const isMobile = window.matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
// Shallow-copy each tooltip object so wrap/mobile mutations don't leak
|
||||
// back into the caller's options.tooltip reference (callers may cache the
|
||||
// options object via memoizeOne, in which case in-place mutation would
|
||||
// pollute that cache across chart instances).
|
||||
const processTooltip = (tooltip: HaTooltipOption): TooltipOption => {
|
||||
const next = convertHaTooltipFormatter(tooltip);
|
||||
if (isMobile) {
|
||||
// mobile charts are full width so we need to confine the tooltip to the chart
|
||||
next.confine = true;
|
||||
next.appendTo = undefined;
|
||||
next.triggerOn = "click";
|
||||
}
|
||||
return next;
|
||||
};
|
||||
const haTooltip = options.tooltip;
|
||||
const processedTooltip = Array.isArray(haTooltip)
|
||||
? haTooltip.map(processTooltip)
|
||||
: processTooltip(haTooltip);
|
||||
return {
|
||||
...options,
|
||||
tooltip: processedTooltip,
|
||||
} as ECOption;
|
||||
const isMobile = window.matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
if (isMobile && options.tooltip) {
|
||||
// mobile charts are full width so we need to confine the tooltip to the chart
|
||||
const tooltips = Array.isArray(options.tooltip)
|
||||
? options.tooltip
|
||||
: [options.tooltip];
|
||||
tooltips.forEach((tooltip) => {
|
||||
tooltip.confine = true;
|
||||
tooltip.appendTo = undefined;
|
||||
tooltip.triggerOn = "click";
|
||||
});
|
||||
options.tooltip = tooltips;
|
||||
}
|
||||
return options as ECOption;
|
||||
return options;
|
||||
}
|
||||
|
||||
private _createTheme(style: CSSStyleDeclaration) {
|
||||
@@ -1006,16 +956,30 @@ export class HaChartBase extends LitElement {
|
||||
const xAxis = (this.options?.xAxis?.[0] ?? this.options?.xAxis) as
|
||||
| XAXisOption
|
||||
| undefined;
|
||||
const yAxis = (this.options?.yAxis?.[0] ?? this.options?.yAxis) as
|
||||
| YAXisOption
|
||||
| undefined;
|
||||
const series = ensureArray(this.data).map((s) => {
|
||||
const data = this._hiddenDatasets.has(String(s.id ?? s.name))
|
||||
? undefined
|
||||
: s.data;
|
||||
let result = {
|
||||
...s,
|
||||
data,
|
||||
} as HaECSeriesItem;
|
||||
if (data && s.type === "line") {
|
||||
if ((s as LineSeriesOption).sampling === "minmax") {
|
||||
if (yAxis?.type === "log") {
|
||||
// set <=0 values to null so they render as gaps on a log graph
|
||||
return {
|
||||
...s,
|
||||
data: (data as LineSeriesOption["data"])!.map((v) =>
|
||||
Array.isArray(v)
|
||||
? [
|
||||
v[0],
|
||||
typeof v[1] !== "number" || v[1] > 0 ? v[1] : null,
|
||||
...v.slice(2),
|
||||
]
|
||||
: v
|
||||
),
|
||||
};
|
||||
}
|
||||
if (s.sampling === "minmax") {
|
||||
const minX = xAxis?.min
|
||||
? xAxis.min instanceof Date
|
||||
? xAxis.min.getTime()
|
||||
@@ -1030,8 +994,8 @@ export class HaChartBase extends LitElement {
|
||||
? xAxis.max
|
||||
: undefined
|
||||
: undefined;
|
||||
result = {
|
||||
...result,
|
||||
return {
|
||||
...s,
|
||||
sampling: undefined,
|
||||
data: downSampleLineData(
|
||||
data as LineSeriesOption["data"],
|
||||
@@ -1039,10 +1003,11 @@ export class HaChartBase extends LitElement {
|
||||
minX,
|
||||
maxX
|
||||
),
|
||||
} as HaECSeriesItem;
|
||||
};
|
||||
}
|
||||
}
|
||||
return processSeriesTooltipFormatter(result);
|
||||
const name = filterXSS(String(s.name ?? s.id ?? ""));
|
||||
return { ...s, name, data };
|
||||
});
|
||||
return series as ECOption["series"];
|
||||
}
|
||||
@@ -1379,8 +1344,8 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
|
||||
private _compareCustomLegendOptions(
|
||||
oldOptions: HaECOption | undefined,
|
||||
newOptions: HaECOption | undefined
|
||||
oldOptions: ECOption | undefined,
|
||||
newOptions: ECOption | undefined
|
||||
): boolean {
|
||||
const oldLegends = ensureArray(
|
||||
oldOptions?.legend || []
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("ha-chart-tooltip-marker")
|
||||
class HaChartTooltipMarker extends LitElement {
|
||||
@property() public color = "";
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public rtl = false;
|
||||
|
||||
protected willUpdate(changed: PropertyValues) {
|
||||
if (changed.has("color")) {
|
||||
this.style.backgroundColor = this.color;
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
margin-inline-end: 4px;
|
||||
margin-inline-start: initial;
|
||||
border-radius: 10px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
:host([rtl]) {
|
||||
direction: rtl;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-chart-tooltip-marker": HaChartTooltipMarker;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { EChartsType } from "echarts/core";
|
||||
import type { GraphSeriesOption } from "echarts/charts";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state, query } from "lit/decorators";
|
||||
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
import { mdiFormatTextVariant, mdiGoogleCirclesGroup } from "@mdi/js";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { listenMediaQuery } from "../../common/dom/media_query";
|
||||
import type { HaECOption } from "../../resources/echarts/echarts";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
import "./ha-chart-base";
|
||||
import type { HaChartBase } from "./ha-chart-base";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@@ -78,7 +78,7 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ attribute: false }) public tooltipFormatter?: (
|
||||
params: TopLevelFormatterParams
|
||||
) => TemplateResult | typeof nothing | null;
|
||||
) => string;
|
||||
|
||||
/**
|
||||
* Optional callback that returns additional searchable strings for a node.
|
||||
@@ -182,7 +182,7 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _createOptions = memoizeOne(
|
||||
(categories?: NetworkData["categories"]): HaECOption => ({
|
||||
(categories?: NetworkData["categories"]): ECOption => ({
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
confine: true,
|
||||
|
||||
@@ -11,10 +11,10 @@ import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import SankeyChart from "../../resources/echarts/components/sankey/install";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HaECOption } from "../../resources/echarts/echarts";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
import { measureTextWidth } from "../../util/text";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
import "./ha-chart-base";
|
||||
import "./ha-chart-tooltip-marker";
|
||||
import { NODE_SIZE } from "../trace/hat-graph-const";
|
||||
import "../ha-alert";
|
||||
|
||||
@@ -71,7 +71,7 @@ export class HaSankeyChart extends LitElement {
|
||||
});
|
||||
|
||||
render() {
|
||||
const options: HaECOption = {
|
||||
const options = {
|
||||
grid: {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
@@ -83,7 +83,7 @@ export class HaSankeyChart extends LitElement {
|
||||
formatter: this._renderTooltip,
|
||||
appendTo: document.body,
|
||||
},
|
||||
};
|
||||
} as ECOption;
|
||||
|
||||
return html`<ha-chart-base
|
||||
.hass=${this.hass}
|
||||
@@ -103,16 +103,12 @@ export class HaSankeyChart extends LitElement {
|
||||
: data.value;
|
||||
if (data.id) {
|
||||
const node = this.data.nodes.find((n) => n.id === data.id);
|
||||
return html`<ha-chart-tooltip-marker
|
||||
.color=${String(params.color ?? "")}
|
||||
></ha-chart-tooltip-marker>
|
||||
${node?.label ?? data.id}<br />${value}`;
|
||||
return `${params.marker} ${filterXSS(node?.label ?? data.id)}<br>${value}`;
|
||||
}
|
||||
if (data.source && data.target) {
|
||||
const source = this.data.nodes.find((n) => n.id === data.source);
|
||||
const target = this.data.nodes.find((n) => n.id === data.target);
|
||||
return html`${source?.label ?? data.source} →
|
||||
${target?.label ?? data.target}<br />${value}`;
|
||||
return `${filterXSS(source?.label ?? data.source)} → ${filterXSS(target?.label ?? data.target)}<br>${value}`;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -5,9 +5,10 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { getGraphColorByIndex } from "../../common/color/colors";
|
||||
import type { HaECOption } from "../../resources/echarts/echarts";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./ha-chart-base";
|
||||
import "./ha-chart-tooltip-marker";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/consistent-type-imports
|
||||
let SunburstChart: typeof import("echarts/lib/chart/sunburst/install");
|
||||
@@ -24,6 +25,8 @@ export interface SunburstNode {
|
||||
|
||||
@customElement("ha-sunburst-chart")
|
||||
export class HaSunburstChart extends LitElement {
|
||||
public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public data?: SunburstNode;
|
||||
|
||||
@property({ attribute: false }) public valueFormatter?: (
|
||||
@@ -47,13 +50,13 @@ export class HaSunburstChart extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const options: HaECOption = {
|
||||
const options = {
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
formatter: this._renderTooltip,
|
||||
appendTo: document.body,
|
||||
},
|
||||
};
|
||||
} as ECOption;
|
||||
|
||||
return html`<ha-chart-base
|
||||
.data=${this._createData(this.data)}
|
||||
@@ -68,10 +71,7 @@ export class HaSunburstChart extends LitElement {
|
||||
const value = this.valueFormatter
|
||||
? this.valueFormatter(data.value)
|
||||
: data.value;
|
||||
return html`<ha-chart-tooltip-marker
|
||||
.color=${String(params.color ?? "")}
|
||||
></ha-chart-tooltip-marker>
|
||||
${data.name}<br />${value}`;
|
||||
return `${params.marker} ${filterXSS(data.name)}<br>${value}`;
|
||||
};
|
||||
|
||||
private _createData = memoizeOne(
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
import { nothing, render } from "lit";
|
||||
import type { LitTooltipFormatter } from "../../resources/echarts/echarts";
|
||||
|
||||
type WrappedTooltipFormatter = (
|
||||
params: unknown,
|
||||
ticket?: string
|
||||
) => HTMLElement | null;
|
||||
|
||||
export type { WrappedTooltipFormatter };
|
||||
|
||||
const litTooltipFormatterCache = new WeakMap<
|
||||
LitTooltipFormatter | WrappedTooltipFormatter,
|
||||
WrappedTooltipFormatter
|
||||
>();
|
||||
|
||||
export const wrapLitTooltipFormatter = (
|
||||
fn: LitTooltipFormatter | WrappedTooltipFormatter
|
||||
): WrappedTooltipFormatter => {
|
||||
const cached = litTooltipFormatterCache.get(fn);
|
||||
if (cached) return cached;
|
||||
const container = document.createElement("div");
|
||||
// display:contents keeps the wrapper layout-invisible so its children act as
|
||||
// direct children of echarts' tooltip box, matching the prior innerHTML behavior.
|
||||
container.style.display = "contents";
|
||||
const wrapped: WrappedTooltipFormatter = (params, ticket) => {
|
||||
const result = (fn as LitTooltipFormatter)(params, ticket);
|
||||
// `nothing` and null/undefined must all suppress the tooltip. Returning
|
||||
// `nothing` to echarts via `render(nothing, container)` leaves a Lit
|
||||
// comment marker behind so echarts would show an empty box; convert it to
|
||||
// null instead so `setContent(null)` clears innerHTML and `show()` hides.
|
||||
if (result === null || result === undefined || result === nothing) {
|
||||
return null;
|
||||
}
|
||||
render(result, container);
|
||||
return container;
|
||||
};
|
||||
litTooltipFormatterCache.set(fn, wrapped);
|
||||
// Idempotent re-wrap: looking up the wrapped fn returns itself.
|
||||
litTooltipFormatterCache.set(wrapped, wrapped);
|
||||
return wrapped;
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { VisualMapComponentOption } from "echarts/components";
|
||||
import type { LineSeriesOption } from "echarts/charts";
|
||||
@@ -12,9 +12,8 @@ import type { LineChartEntity, LineChartState } from "../../data/history";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
import { sideTooltipPosition } from "./chart-tooltip-position";
|
||||
import "./ha-chart-tooltip-marker";
|
||||
import { computeYAxisFractionDigits } from "./y-axis-fraction-digits";
|
||||
import type { HaECOption } from "../../resources/echarts/echarts";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import {
|
||||
getNumberFormatOptions,
|
||||
@@ -25,6 +24,7 @@ import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
|
||||
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
import { computeAttributeValueDisplay } from "../../common/entity/compute_attribute_display";
|
||||
|
||||
const safeParseFloat = (value) => {
|
||||
@@ -108,7 +108,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
private _datasetToDataIndex: number[] = [];
|
||||
|
||||
@state() private _chartOptions?: HaECOption;
|
||||
@state() private _chartOptions?: ECOption;
|
||||
|
||||
private _hiddenStats = new Set<string>();
|
||||
|
||||
@@ -141,11 +141,12 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
private _renderTooltip = (params: any) => {
|
||||
const time = params[0].axisValue;
|
||||
const title = formatDateTimeWithSeconds(
|
||||
new Date(time),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
);
|
||||
const title =
|
||||
formatDateTimeWithSeconds(
|
||||
new Date(time),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
) + "<br>";
|
||||
const datapoints: Record<string, any>[] = [];
|
||||
this._chartData.forEach((dataset, index) => {
|
||||
if (
|
||||
@@ -176,44 +177,52 @@ export class StateHistoryChartLine extends LitElement {
|
||||
seriesName: dataset.name,
|
||||
seriesIndex: index,
|
||||
value: lastData,
|
||||
color: dataset.color,
|
||||
// HTML copied from echarts. May change based on options
|
||||
marker: `<span style="display:inline-block;margin-right:4px;margin-inline-end:4px;margin-inline-start:initial;border-radius:10px;width:10px;height:10px;background-color:${dataset.color};"></span>`,
|
||||
});
|
||||
});
|
||||
const unit = this.unit
|
||||
? `${blankBeforeUnit(this.unit, this.hass.locale)}${this.unit}`
|
||||
: "";
|
||||
|
||||
return html`${title}${datapoints.map((param) => {
|
||||
const entityId = this._entityIds[param.seriesIndex];
|
||||
const stateObj = this.hass.states[entityId];
|
||||
const entry = this.hass.entities[entityId];
|
||||
const stateValue = String(param.value[1]);
|
||||
const value = stateObj
|
||||
? this.hass.formatEntityState(stateObj, stateValue)
|
||||
: `${formatNumber(
|
||||
stateValue,
|
||||
this.hass.locale,
|
||||
getNumberFormatOptions(undefined, entry)
|
||||
)}${unit}`;
|
||||
const dataIndex = this._datasetToDataIndex[param.seriesIndex];
|
||||
const data = this.data[dataIndex];
|
||||
let statSuffix: TemplateResult | typeof nothing = nothing;
|
||||
if (data.statistics && data.statistics.length > 0) {
|
||||
const source =
|
||||
data.states.length === 0 ||
|
||||
param.value[0] < data.states[0].last_changed
|
||||
? this.hass.localize("ui.components.history_charts.source_stats")
|
||||
: this.hass.localize("ui.components.history_charts.source_history");
|
||||
// Five non-breaking spaces indent the source label.
|
||||
statSuffix = html`<br />${"\u00a0".repeat(5)}${source}`;
|
||||
}
|
||||
return html`<br /><ha-chart-tooltip-marker
|
||||
.color=${String(param.color ?? "")}
|
||||
></ha-chart-tooltip-marker>
|
||||
${param.seriesName
|
||||
? html`${param.seriesName}: `
|
||||
: nothing}${value}${statSuffix}`;
|
||||
})}`;
|
||||
return (
|
||||
title +
|
||||
datapoints
|
||||
.map((param) => {
|
||||
const entityId = this._entityIds[param.seriesIndex];
|
||||
const stateObj = this.hass.states[entityId];
|
||||
const entry = this.hass.entities[entityId];
|
||||
const stateValue = String(param.value[1]);
|
||||
let value = stateObj
|
||||
? this.hass.formatEntityState(stateObj, stateValue)
|
||||
: `${formatNumber(
|
||||
stateValue,
|
||||
this.hass.locale,
|
||||
getNumberFormatOptions(undefined, entry)
|
||||
)}${unit}`;
|
||||
const dataIndex = this._datasetToDataIndex[param.seriesIndex];
|
||||
const data = this.data[dataIndex];
|
||||
if (data.statistics && data.statistics.length > 0) {
|
||||
value += "<br> ";
|
||||
const source =
|
||||
data.states.length === 0 ||
|
||||
param.value[0] < data.states[0].last_changed
|
||||
? `${this.hass.localize(
|
||||
"ui.components.history_charts.source_stats"
|
||||
)}`
|
||||
: `${this.hass.localize(
|
||||
"ui.components.history_charts.source_history"
|
||||
)}`;
|
||||
value += source;
|
||||
}
|
||||
|
||||
if (param.seriesName) {
|
||||
return `${param.marker} ${filterXSS(param.seriesName)}: ${value}`;
|
||||
}
|
||||
return `${param.marker} ${value}`;
|
||||
})
|
||||
.join("<br>")
|
||||
);
|
||||
};
|
||||
|
||||
private _datasetHidden(ev: CustomEvent) {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type {
|
||||
CustomSeriesOption,
|
||||
CustomSeriesRenderItem,
|
||||
ECElementEvent,
|
||||
TooltipFormatterCallback,
|
||||
TooltipPositionCallbackParams,
|
||||
} from "echarts/types/dist/shared";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
@@ -14,9 +15,8 @@ import type { TimelineEntity } from "../../data/history";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
import { sideTooltipPosition } from "./chart-tooltip-position";
|
||||
import "./ha-chart-tooltip-marker";
|
||||
import { computeTimelineColor } from "./timeline-color";
|
||||
import type { HaECOption, HaECSeries } from "../../resources/echarts/echarts";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
import echarts from "../../resources/echarts/echarts";
|
||||
import { luminosity } from "../../common/color/rgb";
|
||||
import { hex2rgb } from "../../common/color/convert-color";
|
||||
@@ -57,7 +57,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
@state() private _chartData: CustomSeriesOption[] = [];
|
||||
|
||||
@state() private _chartOptions?: HaECOption;
|
||||
@state() private _chartOptions?: ECOption;
|
||||
|
||||
@state() private _yWidth = 0;
|
||||
|
||||
@@ -69,7 +69,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.options=${this._chartOptions}
|
||||
.height=${`${this.data.length * 30 + 30}px`}
|
||||
.data=${this._chartData as HaECSeries}
|
||||
.data=${this._chartData as ECOption["series"]}
|
||||
small-controls
|
||||
@chart-click=${this._handleChartClick}
|
||||
@chart-zoom=${this._handleDataZoom}
|
||||
@@ -132,35 +132,42 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
return rect;
|
||||
};
|
||||
|
||||
private _renderTooltip = (params: TooltipPositionCallbackParams) => {
|
||||
const { value, name, seriesName, color } = Array.isArray(params)
|
||||
? params[0]
|
||||
: params;
|
||||
const durationInMs = value![2] - value![1];
|
||||
const formattedDuration = `${this.hass.localize(
|
||||
"ui.components.history_charts.duration"
|
||||
)}: ${millisecondsToDuration(durationInMs)}`;
|
||||
private _renderTooltip: TooltipFormatterCallback<TooltipPositionCallbackParams> =
|
||||
(params: TooltipPositionCallbackParams) => {
|
||||
const { value, name, marker, seriesName, color } = Array.isArray(params)
|
||||
? params[0]
|
||||
: params;
|
||||
const title = seriesName
|
||||
? `<h4 style="text-align: center; margin: 0;">${seriesName}</h4>`
|
||||
: "";
|
||||
const durationInMs = value![2] - value![1];
|
||||
const formattedDuration = `${this.hass.localize(
|
||||
"ui.components.history_charts.duration"
|
||||
)}: ${millisecondsToDuration(durationInMs)}`;
|
||||
|
||||
const rtl = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
return html`${seriesName
|
||||
? html`<h4 style="text-align: center; margin: 0;">${seriesName}</h4>`
|
||||
: nothing}<ha-chart-tooltip-marker
|
||||
.color=${String(color ?? "")}
|
||||
.rtl=${rtl}
|
||||
></ha-chart-tooltip-marker
|
||||
>${name}<br />${formatDateTimeWithSeconds(
|
||||
new Date(value![1]),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}<br />${formatDateTimeWithSeconds(
|
||||
new Date(value![2]),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}<br />${formattedDuration}`;
|
||||
};
|
||||
const markerLocalized = !computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
? marker
|
||||
: `<span style="direction: rtl;display:inline-block;margin-right:4px;margin-inline-end:4px;border-radius:10px;width:10px;height:10px;background-color:${color};"></span>`;
|
||||
|
||||
const lines = [
|
||||
markerLocalized + name,
|
||||
formatDateTimeWithSeconds(
|
||||
new Date(value![1]),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
),
|
||||
formatDateTimeWithSeconds(
|
||||
new Date(value![2]),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
),
|
||||
formattedDuration,
|
||||
].join("<br>");
|
||||
return [title, lines].join("");
|
||||
};
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
if (
|
||||
|
||||
@@ -4,7 +4,7 @@ import type {
|
||||
ZRColor,
|
||||
} from "echarts/types/dist/shared";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -34,13 +34,12 @@ import {
|
||||
isExternalStatistic,
|
||||
statisticsHaveType,
|
||||
} from "../../data/recorder";
|
||||
import type { HaECOption } from "../../resources/echarts/echarts";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { getPeriodicAxisLabelConfig } from "./axis-label";
|
||||
import type { CustomLegendOption } from "./ha-chart-base";
|
||||
import "./ha-chart-base";
|
||||
import { sideTooltipPosition } from "./chart-tooltip-position";
|
||||
import "./ha-chart-tooltip-marker";
|
||||
import { fillDataGapsAndRoundCaps } from "./round-caps";
|
||||
import { computeYAxisFractionDigits } from "./y-axis-fraction-digits";
|
||||
|
||||
@@ -127,7 +126,7 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
@state() private _statisticIds: string[] = [];
|
||||
|
||||
@state() private _chartOptions?: HaECOption;
|
||||
@state() private _chartOptions?: ECOption;
|
||||
|
||||
@state() private _hiddenStats = new Set<string>();
|
||||
|
||||
@@ -252,101 +251,91 @@ export class StatisticsChart extends LitElement {
|
||||
const unit = this.unit
|
||||
? `${blankBeforeUnit(this.unit, this.hass.locale)}${this.unit}`
|
||||
: "";
|
||||
const rows: {
|
||||
time?: string;
|
||||
color: string;
|
||||
seriesName?: string;
|
||||
value: string;
|
||||
}[] = [];
|
||||
for (const param of params) {
|
||||
if (rendered[param.seriesIndex]) continue;
|
||||
rendered[param.seriesIndex] = true;
|
||||
return params
|
||||
.map((param, index: number) => {
|
||||
if (rendered[param.seriesIndex]) return "";
|
||||
rendered[param.seriesIndex] = true;
|
||||
|
||||
const statisticId = this._statisticIds[param.seriesIndex];
|
||||
const stateObj = this.hass.states[statisticId];
|
||||
const entry = this.hass.entities[statisticId];
|
||||
let rawValue: string;
|
||||
let rawTime: string;
|
||||
if (chartIsBar) {
|
||||
// For bar charts value is always second value.
|
||||
rawValue = String(param.value[1]);
|
||||
// Time value is third value (un-shifted date) if given, otherwise first value
|
||||
let startTime: Date;
|
||||
let endTime: Date | undefined;
|
||||
if (param.value[2]) {
|
||||
startTime = new Date(param.value[2]);
|
||||
if (param.value[3]) {
|
||||
endTime = new Date(param.value[3]);
|
||||
const statisticId = this._statisticIds[param.seriesIndex];
|
||||
const stateObj = this.hass.states[statisticId];
|
||||
const entry = this.hass.entities[statisticId];
|
||||
let rawValue: string;
|
||||
let rawTime: string;
|
||||
if (chartIsBar) {
|
||||
// For bar charts value is always second value.
|
||||
rawValue = String(param.value[1]);
|
||||
// Time value is third value (un-shifted date) if given, otherwise first value
|
||||
let startTime: Date;
|
||||
let endTime: Date | undefined;
|
||||
if (param.value[2]) {
|
||||
startTime = new Date(param.value[2]);
|
||||
if (param.value[3]) {
|
||||
endTime = new Date(param.value[3]);
|
||||
}
|
||||
} else {
|
||||
startTime = new Date(param.value[0]);
|
||||
}
|
||||
if (
|
||||
period === "year" ||
|
||||
period === "month" ||
|
||||
period === "week" ||
|
||||
period === "day"
|
||||
) {
|
||||
// For year/month/day periods, show only the date
|
||||
rawTime =
|
||||
formatDate(startTime, this.hass.locale, this.hass.config) +
|
||||
(endTime && period !== "day"
|
||||
? ` – ${formatDate(
|
||||
endTime,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}`
|
||||
: "") +
|
||||
"<br>";
|
||||
} else {
|
||||
// For other time periods, include time in render, and optionally show range
|
||||
// if we have an end time.
|
||||
rawTime =
|
||||
formatDateTimeWithSeconds(
|
||||
startTime,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
) +
|
||||
(endTime
|
||||
? ` – ${formatTimeWithSeconds(
|
||||
endTime,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}`
|
||||
: "") +
|
||||
"<br>";
|
||||
}
|
||||
} else {
|
||||
startTime = new Date(param.value[0]);
|
||||
// For lines max series can have 3 values, as the second value is the max-min to form a band
|
||||
rawValue = String(param.value[2] ?? param.value[1]);
|
||||
// Time value is always first value
|
||||
rawTime = `${formatDateTimeWithSeconds(
|
||||
new Date(param.value[0]),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)} <br>`;
|
||||
}
|
||||
if (
|
||||
period === "year" ||
|
||||
period === "month" ||
|
||||
period === "week" ||
|
||||
period === "day"
|
||||
) {
|
||||
// For year/month/day periods, show only the date
|
||||
rawTime =
|
||||
formatDate(startTime, this.hass.locale, this.hass.config) +
|
||||
(endTime && period !== "day"
|
||||
? ` – ${formatDate(endTime, this.hass.locale, this.hass.config)}`
|
||||
: "");
|
||||
} else {
|
||||
// For other time periods, include time in render, and optionally show range
|
||||
// if we have an end time.
|
||||
rawTime =
|
||||
formatDateTimeWithSeconds(
|
||||
startTime,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
) +
|
||||
(endTime
|
||||
? ` – ${formatTimeWithSeconds(
|
||||
endTime,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}`
|
||||
: "");
|
||||
}
|
||||
} else {
|
||||
// For lines max series can have 3 values, as the second value is the max-min to form a band
|
||||
rawValue = String(param.value[2] ?? param.value[1]);
|
||||
// Time value is always first value
|
||||
rawTime = formatDateTimeWithSeconds(
|
||||
new Date(param.value[0]),
|
||||
|
||||
const options = getNumberFormatOptions(stateObj, entry) ?? {
|
||||
maximumFractionDigits: 2,
|
||||
};
|
||||
|
||||
const value = `${formatNumber(
|
||||
rawValue,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
);
|
||||
}
|
||||
options
|
||||
)}${unit}`;
|
||||
|
||||
const options = getNumberFormatOptions(stateObj, entry) ?? {
|
||||
maximumFractionDigits: 2,
|
||||
};
|
||||
|
||||
const value = `${formatNumber(rawValue, this.hass.locale, options)}${unit}`;
|
||||
|
||||
rows.push({
|
||||
time: rows.length === 0 ? rawTime : undefined,
|
||||
color: String(param.color ?? ""),
|
||||
seriesName: param.seriesName,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
if (rows.length === 0) return nothing;
|
||||
|
||||
return html`${rows.map(
|
||||
(row, i) =>
|
||||
html`${row.time
|
||||
? html`${row.time}<br />`
|
||||
: nothing}<ha-chart-tooltip-marker
|
||||
.color=${row.color}
|
||||
></ha-chart-tooltip-marker>
|
||||
${row.seriesName}:
|
||||
${row.value}${i < rows.length - 1 ? html`<br />` : nothing}`
|
||||
)}`;
|
||||
const time = index === 0 ? rawTime : "";
|
||||
return `${time}${param.marker} ${param.seriesName}: ${value}`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("<br>");
|
||||
};
|
||||
|
||||
private _createOptions() {
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { mdiDragHorizontalVariant, mdiEye, mdiEyeOff } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-button";
|
||||
import "../ha-dialog-footer";
|
||||
import "../ha-icon-button";
|
||||
@@ -25,9 +24,7 @@ import type { DataTableSettingsDialogParams } from "./show-dialog-data-table-set
|
||||
|
||||
@customElement("dialog-data-table-settings")
|
||||
export class DialogDataTableSettings extends LitElement {
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: DataTableSettingsDialogParams;
|
||||
|
||||
@@ -120,7 +117,7 @@ export class DialogDataTableSettings extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const localize = this._params.localizeFunc || this._localize;
|
||||
const localize = this._params.localizeFunc || this.hass.localize;
|
||||
|
||||
const columns = this._sortedColumns(
|
||||
this._params.columns,
|
||||
@@ -175,7 +172,7 @@ export class DialogDataTableSettings extends LitElement {
|
||||
.hidden=${!isVisible}
|
||||
.path=${isVisible ? mdiEye : mdiEyeOff}
|
||||
slot="meta"
|
||||
.label=${localize(
|
||||
.label=${this.hass!.localize(
|
||||
`ui.components.data-table.settings.${isVisible ? "hide" : "show"}`,
|
||||
{ title: typeof col.title === "string" ? col.title : "" }
|
||||
)}
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import {
|
||||
mdiChartLine,
|
||||
mdiHelpCircleOutline,
|
||||
mdiPencil,
|
||||
mdiShape,
|
||||
} from "@mdi/js";
|
||||
import { mdiChartLine, mdiHelpCircleOutline, mdiShape } from "@mdi/js";
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { type HASSDomEvent, fireEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
|
||||
import { computeStateName } from "../../common/entity/compute_state_name";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
@@ -58,16 +53,6 @@ const SEARCH_KEYS = [
|
||||
{ name: "id", weight: 2 },
|
||||
];
|
||||
|
||||
export interface StatisticElementChangedEvent {
|
||||
statisticId: string;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"edit-statistics-element": StatisticElementChangedEvent;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("ha-statistic-picker")
|
||||
export class HaStatisticPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -145,8 +130,6 @@ export class HaStatisticPicker extends LitElement {
|
||||
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
@property({ attribute: "can-edit", type: Boolean }) public canEdit?: boolean;
|
||||
|
||||
public willUpdate(changedProps: PropertyValues<this>) {
|
||||
if (
|
||||
(!this.hasUpdated && !this.statisticIds) ||
|
||||
@@ -358,15 +341,6 @@ export class HaStatisticPicker extends LitElement {
|
||||
${item.secondary
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing}
|
||||
${this.canEdit
|
||||
? html`<ha-icon-button
|
||||
slot="end"
|
||||
.value=${statisticId}
|
||||
.label=${this.hass.localize("ui.common.edit")}
|
||||
.path=${mdiPencil}
|
||||
@click=${this._editItem}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -376,12 +350,6 @@ export class HaStatisticPicker extends LitElement {
|
||||
|
||||
private _valueRenderer: PickerValueRenderer = this._makeValueRenderer();
|
||||
|
||||
private _editItem(ev: HASSDomEvent<StatisticElementChangedEvent>) {
|
||||
ev.stopPropagation();
|
||||
const statisticId = (ev.currentTarget as any).value;
|
||||
fireEvent(this, "edit-statistics-element", { statisticId });
|
||||
}
|
||||
|
||||
private _computeItem(statisticId: string): StatisticComboBoxItem {
|
||||
const stateObj = this.hass.states[statisticId];
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { type HASSDomEvent, fireEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { ValueChangedEvent, HomeAssistant } from "../../types";
|
||||
import "./ha-statistic-picker";
|
||||
import type { StatisticElementChangedEvent } from "./ha-statistic-picker";
|
||||
|
||||
@customElement("ha-statistics-picker")
|
||||
class HaStatisticsPicker extends LitElement {
|
||||
@@ -60,8 +59,6 @@ class HaStatisticsPicker extends LitElement {
|
||||
})
|
||||
public ignoreRestrictionsOnFirstStatistic = false;
|
||||
|
||||
@property({ attribute: "can-edit", type: Boolean }) public canEdit?;
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
@@ -102,9 +99,7 @@ class HaStatisticsPicker extends LitElement {
|
||||
.statisticIds=${this.statisticIds}
|
||||
.excludeStatistics=${this.value}
|
||||
.allowCustomEntity=${this.allowCustomEntity}
|
||||
.canEdit=${this.canEdit}
|
||||
@value-changed=${this._statisticChanged}
|
||||
@edit-statistics-element=${this._editItem}
|
||||
></ha-statistic-picker>
|
||||
</div>
|
||||
`
|
||||
@@ -127,17 +122,6 @@ class HaStatisticsPicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _editItem(ev: HASSDomEvent<StatisticElementChangedEvent>) {
|
||||
const statisticId = ev.detail.statisticId;
|
||||
const index = this._currentStatistics!.findIndex((e) => e === statisticId);
|
||||
fireEvent(this, "edit-detail-element", {
|
||||
subElementConfig: {
|
||||
index,
|
||||
type: "row",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private get _currentStatistics() {
|
||||
return this.value || [];
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ class StateInfo extends LitElement {
|
||||
)}:
|
||||
</span>
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${this.stateObj.last_changed}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
@@ -54,6 +55,7 @@ class StateInfo extends LitElement {
|
||||
)}:
|
||||
</span>
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${this.stateObj.last_updated}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
@@ -61,6 +63,7 @@ class StateInfo extends LitElement {
|
||||
</ha-tooltip>
|
||||
<ha-relative-time
|
||||
id="relative-time"
|
||||
.hass=${this.hass}
|
||||
.datetime=${this.stateObj.last_changed}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
|
||||
@@ -1,34 +1,18 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { addDays, differenceInMilliseconds, startOfDay } from "date-fns";
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { transform } from "../common/decorators/transform";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { absoluteTime } from "../common/datetime/absolute_time";
|
||||
import { configContext, internationalizationContext } from "../data/context";
|
||||
import type {
|
||||
HomeAssistantConfig,
|
||||
HomeAssistantInternationalization,
|
||||
} from "../types";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
const SAFE_MARGIN = 5 * 1000;
|
||||
|
||||
@customElement("ha-absolute-time")
|
||||
class HaAbsoluteTime extends ReactiveElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public datetime?: string | Date;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n?: HomeAssistantInternationalization;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
@transform<HomeAssistantConfig, HassConfig>({
|
||||
transformer: ({ config }) => config,
|
||||
})
|
||||
private _config?: HassConfig;
|
||||
|
||||
private _timeout?: number;
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
@@ -78,17 +62,13 @@ class HaAbsoluteTime extends ReactiveElement {
|
||||
}
|
||||
|
||||
private _updateAbsolute(): void {
|
||||
if (!this._i18n || !this._config) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.datetime) {
|
||||
this.innerHTML = this._i18n.localize("ui.components.absolute_time.never");
|
||||
this.innerHTML = this.hass.localize("ui.components.absolute_time.never");
|
||||
} else {
|
||||
this.innerHTML = absoluteTime(
|
||||
new Date(this.datetime),
|
||||
this._i18n.locale,
|
||||
this._config
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,8 @@ import {
|
||||
mdiInformationOutline,
|
||||
} from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-icon-button";
|
||||
@@ -40,9 +39,7 @@ class HaAlert extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public dismissable = false;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize?: LocalizeFunc;
|
||||
@property({ attribute: false }) public localize?: LocalizeFunc;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@@ -71,7 +68,7 @@ class HaAlert extends LitElement {
|
||||
${this.dismissable
|
||||
? html`<ha-icon-button
|
||||
@click=${this._dismissClicked}
|
||||
.label=${this._localize?.("ui.common.dismiss_alert")}
|
||||
.label=${this.localize!("ui.common.dismiss_alert")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
|
||||
@@ -61,6 +61,7 @@ export class HaAreasDisplayEditor extends LitElement {
|
||||
>
|
||||
<ha-svg-icon slot="leading-icon" .path=${mdiTextureBox}></ha-svg-icon>
|
||||
<ha-items-display-editor
|
||||
.hass=${this.hass}
|
||||
.items=${items}
|
||||
.value=${value}
|
||||
@value-changed=${this._areaDisplayChanged}
|
||||
|
||||
@@ -107,6 +107,7 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
|
||||
></ha-svg-icon>
|
||||
`}
|
||||
<ha-items-display-editor
|
||||
.hass=${this.hass}
|
||||
.items=${groupedAreasItems[floor.floor_id]}
|
||||
.value=${value}
|
||||
.floorId=${floor.floor_id}
|
||||
|
||||
@@ -20,7 +20,6 @@ export class HaCheckListItem extends CheckListItemBase {
|
||||
separateCheckboxClick = false;
|
||||
|
||||
async onChange(event) {
|
||||
event.stopPropagation();
|
||||
super.onChange(event);
|
||||
fireEvent(this, event.type);
|
||||
}
|
||||
|
||||
@@ -11,15 +11,12 @@ import {
|
||||
mdiStateMachine,
|
||||
mdiWeatherSunny,
|
||||
} from "@mdi/js";
|
||||
import { consume } from "@lit/context";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import type { HassConfig, Connection } from "home-assistant-js-websocket";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { transform } from "../common/decorators/transform";
|
||||
import { configContext, connectionContext } from "../data/context";
|
||||
import { conditionIcon, FALLBACK_DOMAIN_ICONS } from "../data/icons";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@@ -39,24 +36,12 @@ export const CONDITION_ICONS = {
|
||||
|
||||
@customElement("ha-condition-icon")
|
||||
export class HaConditionIcon extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public condition?: string;
|
||||
|
||||
@property() public icon?: string;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
@transform<{ config: HassConfig }, HassConfig>({
|
||||
transformer: ({ config }) => config,
|
||||
})
|
||||
private _config?: HassConfig;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
@transform<{ connection: Connection }, Connection>({
|
||||
transformer: ({ connection }) => connection,
|
||||
})
|
||||
private _connection?: Connection;
|
||||
|
||||
protected render() {
|
||||
if (this.icon) {
|
||||
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
|
||||
@@ -66,13 +51,13 @@ export class HaConditionIcon extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this._connection || !this._config) {
|
||||
if (!this.hass) {
|
||||
return this._renderFallback();
|
||||
}
|
||||
|
||||
const icon = conditionIcon(
|
||||
this._connection,
|
||||
this._config,
|
||||
this.hass.connection,
|
||||
this.hass.config,
|
||||
this.condition
|
||||
).then((icn) => {
|
||||
if (icn) {
|
||||
|
||||
@@ -54,6 +54,7 @@ export class HaEntitiesDisplayEditor extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-items-display-editor
|
||||
.hass=${this.hass}
|
||||
.items=${items}
|
||||
.value=${value}
|
||||
@value-changed=${this._itemDisplayChanged}
|
||||
|
||||
@@ -3,8 +3,6 @@ import { mdiFilterVariantRemove } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { deepEqual } from "../common/util/deep-equal";
|
||||
import type { Blueprints } from "../data/blueprint";
|
||||
@@ -22,10 +20,6 @@ import "./ha-list";
|
||||
export class HaFilterBlueprints extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@property({ attribute: false }) public value?: string[];
|
||||
|
||||
@property() public type?: "automation" | "script";
|
||||
@@ -60,7 +54,7 @@ export class HaFilterBlueprints extends LitElement {
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
${this._localize("ui.panel.config.blueprint.caption")}
|
||||
${this.hass.localize("ui.panel.config.blueprint.caption")}
|
||||
${this.value?.length
|
||||
? html`<div class="badge">${this.value?.length}</div>
|
||||
<ha-icon-button
|
||||
|
||||
@@ -4,8 +4,6 @@ import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
@@ -24,10 +22,6 @@ import "../panels/config/voice-assistants/expose/expose-assistant-icon";
|
||||
export class HaFilterVoiceAssistants extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
// the list of selected voiceAssistantIds
|
||||
@property({ attribute: false }) public value: string[] = [];
|
||||
|
||||
@@ -50,7 +44,9 @@ export class HaFilterVoiceAssistants extends LitElement {
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
${this._localize("ui.panel.config.dashboard.voice_assistants.main")}
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.dashboard.voice_assistants.main"
|
||||
)}
|
||||
${this.value?.length
|
||||
? html`<div class="badge">${this.value?.length}</div>
|
||||
<ha-icon-button
|
||||
|
||||
@@ -77,7 +77,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
| "bottom-start"
|
||||
| "bottom-end"
|
||||
| "left-start"
|
||||
| "left-end" = "bottom";
|
||||
| "left-end" = "bottom-start";
|
||||
|
||||
/** If set picker shows an add button instead of textbox when value isn't set */
|
||||
@property({ attribute: "add-button-label" }) public addButtonLabel?: string;
|
||||
|
||||
@@ -1,77 +1,59 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
// @ts-ignore
|
||||
import topAppBarStyles from "@material/top-app-bar/dist/mdc.top-app-bar.min.css";
|
||||
import { css, html, LitElement, unsafeCSS } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-header-bar")
|
||||
export class HaHeaderBar extends LitElement {
|
||||
protected render() {
|
||||
return html`<header class="header-bar">
|
||||
<div class="row">
|
||||
<section class="section" id="navigation">
|
||||
return html`<header class="mdc-top-app-bar">
|
||||
<div class="mdc-top-app-bar__row">
|
||||
<section
|
||||
class="mdc-top-app-bar__section mdc-top-app-bar__section--align-start"
|
||||
id="navigation"
|
||||
>
|
||||
<slot name="navigationIcon"></slot>
|
||||
<span class="title">
|
||||
<span class="mdc-top-app-bar__title">
|
||||
<slot name="title"></slot>
|
||||
</span>
|
||||
</section>
|
||||
<section class="section end" id="actions" role="toolbar">
|
||||
<section
|
||||
class="mdc-top-app-bar__section mdc-top-app-bar__section--align-end"
|
||||
id="actions"
|
||||
role="toolbar"
|
||||
>
|
||||
<slot name="actionItems"></slot>
|
||||
</section>
|
||||
</div>
|
||||
</header>`;
|
||||
}
|
||||
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
box-sizing: border-box;
|
||||
color: var(--app-header-text-color, var(--primary-text-color));
|
||||
background-color: var(
|
||||
--app-header-background-color,
|
||||
var(--primary-background-color)
|
||||
);
|
||||
padding: var(--header-bar-padding);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: var(--header-height);
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
padding: 0 var(--ha-space-3);
|
||||
}
|
||||
|
||||
#navigation {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.section.end {
|
||||
flex: none;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: var(--ha-font-size-xl);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
line-height: var(--header-height);
|
||||
padding-inline-start: var(--ha-space-6);
|
||||
}
|
||||
`;
|
||||
static get styles() {
|
||||
return [
|
||||
unsafeCSS(topAppBarStyles),
|
||||
css`
|
||||
.mdc-top-app-bar__row {
|
||||
height: var(--header-height);
|
||||
}
|
||||
.mdc-top-app-bar {
|
||||
position: static;
|
||||
color: var(--mdc-theme-on-primary, #fff);
|
||||
padding: var(--header-bar-padding);
|
||||
}
|
||||
.mdc-top-app-bar__section.mdc-top-app-bar__section--align-start {
|
||||
flex: 1;
|
||||
}
|
||||
.mdc-top-app-bar__section.mdc-top-app-bar__section--align-end {
|
||||
flex: none;
|
||||
}
|
||||
.mdc-top-app-bar__title {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
padding-inline-start: 24px;
|
||||
padding-inline-end: initial;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -2,11 +2,10 @@ import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { haStyle } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-dropdown";
|
||||
import "./ha-dropdown-item";
|
||||
import "./ha-icon-button";
|
||||
@@ -27,9 +26,7 @@ export interface IconOverflowMenuItem {
|
||||
|
||||
@customElement("ha-icon-overflow-menu")
|
||||
export class HaIconOverflowMenu extends LitElement {
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Array }) public items: IconOverflowMenuItem[] = [];
|
||||
|
||||
@@ -47,7 +44,7 @@ export class HaIconOverflowMenu extends LitElement {
|
||||
@click=${stopPropagation}
|
||||
>
|
||||
<ha-icon-button
|
||||
.label=${this._localize("ui.common.overflow_menu")}
|
||||
.label=${this.hass.localize("ui.common.overflow_menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
slot="trigger"
|
||||
></ha-icon-button>
|
||||
|
||||
@@ -121,6 +121,7 @@ export class HaIconPicker extends LitElement {
|
||||
.label=${this.label}
|
||||
.value=${this._value}
|
||||
.searchFn=${this._filterIcons}
|
||||
popover-placement="bottom-start"
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
<slot name="start"></slot>
|
||||
|
||||
@@ -14,7 +14,7 @@ class InputHelperText extends LitElement {
|
||||
:host {
|
||||
display: block;
|
||||
color: var(--mdc-text-field-label-ink-color, rgba(0, 0, 0, 0.6));
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-size: 0.75rem;
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
padding-inline-start: 16px;
|
||||
|
||||
@@ -8,11 +8,10 @@ import { ifDefined } from "lit/directives/if-defined";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { until } from "lit/directives/until";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { stopPropagation } from "../common/dom/stop_propagation";
|
||||
import { orderCompare } from "../common/string/compare";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon";
|
||||
import "./ha-icon-button";
|
||||
import "./ha-icon-next";
|
||||
@@ -47,9 +46,7 @@ declare global {
|
||||
|
||||
@customElement("ha-items-display-editor")
|
||||
export class HaItemDisplayEditor extends LitElement {
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public items: DisplayItem[] = [];
|
||||
|
||||
@@ -164,7 +161,7 @@ export class HaItemDisplayEditor extends LitElement {
|
||||
? html`<ha-icon-button
|
||||
.path=${isVisible ? mdiEye : mdiEyeOff}
|
||||
slot="end"
|
||||
.label=${this._localize(
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.items-display-editor.${isVisible ? "hide" : "show"}`,
|
||||
{
|
||||
label: label,
|
||||
|
||||
@@ -50,9 +50,7 @@ class HaLabel extends LitElement {
|
||||
<div class="container" .id=${this._elementId}>
|
||||
<span class="content">
|
||||
<slot name="icon"></slot>
|
||||
<span class="label-content">
|
||||
<slot></slot>
|
||||
</span>
|
||||
<slot></slot>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
@@ -115,10 +113,6 @@ class HaLabel extends LitElement {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.label-content {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
:host([dense]) {
|
||||
height: 20px;
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
@@ -132,29 +126,6 @@ class HaLabel extends LitElement {
|
||||
margin-inline-start: -4px;
|
||||
margin-inline-end: 4px;
|
||||
}
|
||||
|
||||
:host(.text-ellipsis) {
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
:host(.text-ellipsis) .container {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
:host(.text-ellipsis) span.content {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
:host(.text-ellipsis) .label-content {
|
||||
display: block;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -152,6 +152,7 @@ export class HaLanguagePicker extends LitElement {
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.autofocus=${this.autofocus}
|
||||
popover-placement="bottom-end"
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
.emptyLabel=${this.hass?.localize(
|
||||
"ui.components.language-picker.no_languages"
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { parseISO } from "date-fns";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { relativeTime } from "../common/datetime/relative_time";
|
||||
import { capitalizeFirstLetter } from "../common/string/capitalize-first-letter";
|
||||
import { internationalizationContext } from "../data/context";
|
||||
import type { HomeAssistantInternationalization } from "../types";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("ha-relative-time")
|
||||
class HaRelativeTime extends ReactiveElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public datetime?: string | Date;
|
||||
|
||||
@property({ type: Boolean }) public capitalize = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n?: HomeAssistantInternationalization;
|
||||
|
||||
private _interval?: number;
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
@@ -61,19 +57,15 @@ class HaRelativeTime extends ReactiveElement {
|
||||
}
|
||||
|
||||
private _updateRelative(): void {
|
||||
if (!this._i18n) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.datetime) {
|
||||
this.innerHTML = this._i18n.localize("ui.components.relative_time.never");
|
||||
this.innerHTML = this.hass.localize("ui.components.relative_time.never");
|
||||
} else {
|
||||
const date =
|
||||
typeof this.datetime === "string"
|
||||
? parseISO(this.datetime)
|
||||
: this.datetime;
|
||||
|
||||
const relTime = relativeTime(date, this._i18n.locale);
|
||||
const relTime = relativeTime(date, this.hass.locale);
|
||||
this.innerHTML = this.capitalize
|
||||
? capitalizeFirstLetter(relTime)
|
||||
: relTime;
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeRTL } from "../common/util/compute_rtl";
|
||||
import { internationalizationContext, uiContext } from "../data/context";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./radio/ha-radio-group";
|
||||
import type { HaRadioGroup } from "./radio/ha-radio-group";
|
||||
import "./radio/ha-radio-option";
|
||||
@@ -27,6 +26,8 @@ export interface SelectBoxOption {
|
||||
|
||||
@customElement("ha-select-box")
|
||||
export class HaSelectBox extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public options: SelectBoxOption[] = [];
|
||||
|
||||
@property({ attribute: false }) public value?: string;
|
||||
@@ -39,14 +40,6 @@ export class HaSelectBox extends LitElement {
|
||||
@property({ type: Boolean, attribute: "stacked_image" })
|
||||
public stackedImage = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
protected _i18n?: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: uiContext, subscribe: true })
|
||||
protected _ui?: ContextType<typeof uiContext>;
|
||||
|
||||
render() {
|
||||
const maxColumns = this.maxColumns ?? 3;
|
||||
const columns = Math.min(maxColumns, this.options.length);
|
||||
@@ -69,11 +62,11 @@ export class HaSelectBox extends LitElement {
|
||||
const disabled = option.disabled || this.disabled || false;
|
||||
const selected = option.value === this.value;
|
||||
|
||||
const isDark = this._ui?.themes.darkMode || false;
|
||||
const isRTL = this._i18n
|
||||
const isDark = this.hass?.themes.darkMode || false;
|
||||
const isRTL = this.hass
|
||||
? computeRTL(
|
||||
this._i18n.language,
|
||||
this._i18n.translationMetadata.translations
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
: false;
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { consumeEntityStates } from "../../common/decorators/consume-context-entry";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { AttributeSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../entity/ha-entity-attribute-picker";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
|
||||
@customElement("ha-selector-attribute")
|
||||
export class HaSelectorAttribute extends LitElement {
|
||||
@@ -29,10 +27,6 @@ export class HaSelectorAttribute extends LitElement {
|
||||
filter_entity?: string | string[];
|
||||
};
|
||||
|
||||
@state()
|
||||
@consumeEntityStates({ entityIdPath: ["context", "filter_entity"] })
|
||||
private _filterEntityStates?: Record<string, HassEntity>;
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-entity-attribute-picker
|
||||
@@ -79,7 +73,7 @@ export class HaSelectorAttribute extends LitElement {
|
||||
const entityIds = ensureArray(this.context.filter_entity);
|
||||
|
||||
invalid = !entityIds.some((entityId) => {
|
||||
const stateObj = this._filterEntityStates?.[entityId];
|
||||
const stateObj = this.hass.states[entityId];
|
||||
return (
|
||||
stateObj &&
|
||||
this.value in stateObj.attributes &&
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
LocalizeFunc,
|
||||
LocalizeKeys,
|
||||
} from "../../common/translations/localize";
|
||||
import type { LocalizeKeys } from "../../common/translations/localize";
|
||||
import type {
|
||||
AutomationBehavior,
|
||||
AutomationBehaviorConditionMode,
|
||||
AutomationBehaviorSelector,
|
||||
AutomationBehaviorTriggerMode,
|
||||
} from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-select-box";
|
||||
import type { SelectBoxOption } from "../ha-select-box";
|
||||
import "../ha-select-box";
|
||||
|
||||
const TRIGGER_BEHAVIORS: AutomationBehaviorTriggerMode[] = [
|
||||
"each",
|
||||
"any",
|
||||
"first",
|
||||
"all",
|
||||
"last",
|
||||
];
|
||||
|
||||
const CONDITION_BEHAVIORS: AutomationBehaviorConditionMode[] = ["any", "all"];
|
||||
|
||||
@customElement("ha-selector-automation_behavior")
|
||||
export class HaSelectorAutomationBehavior extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public selector!: AutomationBehaviorSelector;
|
||||
|
||||
@@ -40,9 +39,6 @@ export class HaSelectorAutomationBehavior extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@consumeLocalize()
|
||||
protected _localize?: LocalizeFunc;
|
||||
|
||||
protected render() {
|
||||
const { mode } = this.selector.automation_behavior ?? {};
|
||||
const modeKey = mode ?? "trigger";
|
||||
@@ -64,6 +60,7 @@ export class HaSelectorAutomationBehavior extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-select-box
|
||||
.hass=${this.hass}
|
||||
.options=${options}
|
||||
.value=${this.value ?? ""}
|
||||
max_columns="1"
|
||||
@@ -98,10 +95,8 @@ export class HaSelectorAutomationBehavior extends LitElement {
|
||||
return translated;
|
||||
}
|
||||
}
|
||||
return (
|
||||
this._localize?.(
|
||||
`ui.components.selectors.automation_behavior.${mode ?? "trigger"}.options.${behavior}.${field}` as LocalizeKeys
|
||||
) || behavior
|
||||
return this.hass.localize(
|
||||
`ui.components.selectors.automation_behavior.${mode ?? "trigger"}.options.${behavior}.${field}` as LocalizeKeys
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-formfield";
|
||||
import "../ha-switch";
|
||||
import "../ha-input-helper-text";
|
||||
|
||||
@customElement("ha-selector-boolean")
|
||||
export class HaBooleanSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public value = false;
|
||||
|
||||
@property() public placeholder?: any;
|
||||
|
||||
@@ -1,26 +1,14 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import type { ButtonToggleSelector, SelectOption } from "../../data/selector";
|
||||
import { internationalizationContext } from "../../data/context";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import type {
|
||||
HomeAssistantInternationalization,
|
||||
ToggleButton,
|
||||
} from "../../types";
|
||||
import type { HomeAssistant, ToggleButton } from "../../types";
|
||||
import "../ha-button-toggle-group";
|
||||
|
||||
@customElement("ha-selector-button_toggle")
|
||||
export class HaButtonToggleSelector extends LitElement {
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
|
||||
transformer: ({ locale }) => locale,
|
||||
})
|
||||
private _locale!: FrontendLocaleData;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: ButtonToggleSelector;
|
||||
|
||||
@@ -60,7 +48,11 @@ export class HaButtonToggleSelector extends LitElement {
|
||||
|
||||
if (this.selector.button_toggle?.sort) {
|
||||
options.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a.label, b.label, this._locale.language)
|
||||
caseInsensitiveStringCompare(
|
||||
a.label,
|
||||
b.label,
|
||||
this.hass.locale.language
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,10 @@ import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, 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 { isTemplate } from "../../common/string/has-template";
|
||||
import type { ChooseSelector, Selector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import "../ha-button-toggle-group";
|
||||
import "./ha-selector";
|
||||
|
||||
@@ -30,9 +28,6 @@ export class HaChooseSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@consumeLocalize()
|
||||
protected _localize?: LocalizeFunc;
|
||||
|
||||
@state() public _activeChoice?: string;
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>): void {
|
||||
@@ -67,7 +62,7 @@ export class HaChooseSelector extends LitElement {
|
||||
.buttons=${this._toggleButtons(
|
||||
this.selector.choose.choices,
|
||||
this.selector.choose.translation_key,
|
||||
this._localize
|
||||
this.hass.localize
|
||||
)}
|
||||
.active=${this._activeChoice}
|
||||
@value-changed=${this._choiceChanged}
|
||||
@@ -88,7 +83,7 @@ export class HaChooseSelector extends LitElement {
|
||||
(
|
||||
choices: ChooseSelector["choose"]["choices"],
|
||||
translationKey?: string,
|
||||
_localize?: LocalizeFunc
|
||||
_localize?: HomeAssistant["localize"]
|
||||
) =>
|
||||
Object.keys(choices).map((choice) => ({
|
||||
label:
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import type { DateSelector } from "../../data/selector";
|
||||
import { internationalizationContext } from "../../data/context";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import type { HomeAssistantInternationalization } from "../../types";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-date-input";
|
||||
import type { HaDateInput } from "../ha-date-input";
|
||||
|
||||
@customElement("ha-selector-date")
|
||||
export class HaDateSelector extends LitElement {
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
|
||||
transformer: ({ locale }) => locale,
|
||||
})
|
||||
private _locale!: FrontendLocaleData;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: DateSelector;
|
||||
|
||||
@@ -40,7 +31,7 @@ export class HaDateSelector extends LitElement {
|
||||
return html`
|
||||
<ha-date-input
|
||||
.label=${this.label}
|
||||
.locale=${this._locale}
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${this.disabled}
|
||||
.value=${typeof this.value === "string" ? this.value : undefined}
|
||||
.required=${this.required}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import type { DateTimeSelector } from "../../data/selector";
|
||||
import { internationalizationContext } from "../../data/context";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import type { HomeAssistantInternationalization } from "../../types";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-date-input";
|
||||
import type { HaDateInput } from "../ha-date-input";
|
||||
import "../ha-time-input";
|
||||
@@ -15,12 +11,7 @@ import type { HaTimeInput } from "../ha-time-input";
|
||||
|
||||
@customElement("ha-selector-datetime")
|
||||
export class HaDateTimeSelector extends LitElement {
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
|
||||
transformer: ({ locale }) => locale,
|
||||
})
|
||||
private _locale!: FrontendLocaleData;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: DateTimeSelector;
|
||||
|
||||
@@ -50,7 +41,7 @@ export class HaDateTimeSelector extends LitElement {
|
||||
<div class="input">
|
||||
<ha-date-input
|
||||
.label=${this.label}
|
||||
.locale=${this._locale}
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.value=${values?.[0]}
|
||||
@@ -60,7 +51,7 @@ export class HaDateTimeSelector extends LitElement {
|
||||
<ha-time-input
|
||||
enable-second
|
||||
.value=${values?.[1] || "00:00:00"}
|
||||
.locale=${this._locale}
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
@value-changed=${this._valueChanged}
|
||||
|
||||
@@ -2,11 +2,14 @@ import { html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { DurationSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-duration-input";
|
||||
import type { HaDurationData, HaDurationInput } from "../ha-duration-input";
|
||||
|
||||
@customElement("ha-selector-duration")
|
||||
export class HaTimeDuration extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: DurationSelector;
|
||||
|
||||
@property({ attribute: false }) public value?:
|
||||
|
||||
@@ -3,12 +3,10 @@ import type { PropertyValues } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
|
||||
import { removeFile, uploadFile } from "../../data/file_upload";
|
||||
import type { FileSelector } from "../../data/selector";
|
||||
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import "../ha-file-upload";
|
||||
|
||||
@customElement("ha-selector-file")
|
||||
@@ -27,9 +25,6 @@ export class HaFileSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@consumeLocalize()
|
||||
protected _localize?: LocalizeFunc;
|
||||
|
||||
@state() private _filename?: { fileId: string; name: string };
|
||||
|
||||
@state() private _busy = false;
|
||||
@@ -47,7 +42,7 @@ export class HaFileSelector extends LitElement {
|
||||
.uploading=${this._busy}
|
||||
.value=${this.value
|
||||
? this._filename?.name ||
|
||||
this._localize!("ui.components.selectors.file.unknown_file")
|
||||
this.hass.localize("ui.components.selectors.file.unknown_file")
|
||||
: undefined}
|
||||
@file-picked=${this._uploadFile}
|
||||
@change=${this._removeFile}
|
||||
@@ -77,7 +72,7 @@ export class HaFileSelector extends LitElement {
|
||||
fireEvent(this, "value-changed", { value: fileId });
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
text: this._localize!("ui.components.selectors.file.upload_failed", {
|
||||
text: this.hass.localize("ui.components.selectors.file.upload_failed", {
|
||||
reason: err.message || err,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { mdiPlayBox, mdiPlus } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { supportsFeature } from "../../common/entity/supports-feature";
|
||||
import { getSignedPath } from "../../data/auth";
|
||||
import type { MediaPickedEvent } from "../../data/media-player";
|
||||
import {
|
||||
MediaClassBrowserSettings,
|
||||
@@ -12,10 +13,14 @@ import {
|
||||
} from "../../data/media-player";
|
||||
import type { MediaSelector, MediaSelectorValue } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
brandsUrl,
|
||||
extractDomainFromBrandUrl,
|
||||
isBrandUrl,
|
||||
} from "../../util/brands-url";
|
||||
import "../ha-alert";
|
||||
import "../ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../ha-form/types";
|
||||
import "../media-player/ha-media-browser-thumbnail";
|
||||
import { showMediaBrowserDialog } from "../media-player/show-media-browser-dialog";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import "../ha-picture-upload";
|
||||
@@ -49,6 +54,8 @@ export class HaMediaSelector extends LitElement {
|
||||
filter_entity?: string | string[];
|
||||
};
|
||||
|
||||
@state() private _thumbnailUrl?: string | null;
|
||||
|
||||
private _contextEntities: string[] | undefined;
|
||||
|
||||
private get _hasAccept(): boolean {
|
||||
@@ -61,6 +68,35 @@ export class HaMediaSelector extends LitElement {
|
||||
this._contextEntities = ensureArray(this.context?.filter_entity);
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProps.has("value")) {
|
||||
const thumbnail = this.value?.metadata?.thumbnail;
|
||||
const oldThumbnail = (changedProps.get("value") as this["value"])
|
||||
?.metadata?.thumbnail;
|
||||
if (thumbnail === oldThumbnail) {
|
||||
return;
|
||||
}
|
||||
if (thumbnail && isBrandUrl(thumbnail)) {
|
||||
// The backend is not aware of the theme used by the users,
|
||||
// so we rewrite the URL to show a proper icon
|
||||
this._thumbnailUrl = brandsUrl(
|
||||
{
|
||||
domain: extractDomainFromBrandUrl(thumbnail),
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
);
|
||||
} else if (thumbnail && thumbnail.startsWith("/")) {
|
||||
this._thumbnailUrl = undefined;
|
||||
// Thumbnails served by local API require authentication
|
||||
getSignedPath(this.hass, thumbnail).then((signedPath) => {
|
||||
this._thumbnailUrl = signedPath.path;
|
||||
});
|
||||
} else {
|
||||
this._thumbnailUrl = thumbnail;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -150,12 +186,10 @@ export class HaMediaSelector extends LitElement {
|
||||
),
|
||||
})}
|
||||
image"
|
||||
>
|
||||
<ha-media-browser-thumbnail
|
||||
.hass=${this.hass}
|
||||
.url=${this.value.metadata.thumbnail}
|
||||
></ha-media-browser-thumbnail>
|
||||
</div>
|
||||
style=${this._thumbnailUrl
|
||||
? `background-image: url(${this._thumbnailUrl});`
|
||||
: ""}
|
||||
></div>
|
||||
`
|
||||
: html`
|
||||
<div class="icon-holder image">
|
||||
@@ -376,11 +410,13 @@ export class HaMediaSelector extends LitElement {
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
--ha-media-browser-thumbnail-fit: cover;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
.centered-image {
|
||||
margin: 4px;
|
||||
--ha-media-browser-thumbnail-fit: contain;
|
||||
background-size: contain;
|
||||
}
|
||||
.icon-holder {
|
||||
display: flex;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { PeriodKey, PeriodSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@@ -42,9 +41,6 @@ export class HaPeriodSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@consumeLocalize()
|
||||
protected _localize?: LocalizeFunc;
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(
|
||||
selectedPeriodKey: PeriodKey | undefined,
|
||||
@@ -82,7 +78,7 @@ export class HaPeriodSelector extends LitElement {
|
||||
const schema = this._schema(
|
||||
typeof data.period === "string" ? (data.period as PeriodKey) : undefined,
|
||||
this.selector,
|
||||
this._localize!
|
||||
this.hass.localize
|
||||
);
|
||||
|
||||
return html`
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
import { mdiDragHorizontalVariant } from "@mdi/js";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consume } from "@lit/context";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import { internationalizationContext } from "../../data/context";
|
||||
import type { SelectOption, SelectSelector } from "../../data/selector";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
HomeAssistantInternationalization,
|
||||
} from "../../types";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../chips/ha-chip-set";
|
||||
import "../chips/ha-input-chip";
|
||||
import "../ha-checkbox";
|
||||
@@ -32,13 +25,6 @@ import "../radio/ha-radio-option";
|
||||
export class HaSelectSelector extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
|
||||
transformer: ({ locale }) => locale,
|
||||
})
|
||||
private _locale!: FrontendLocaleData;
|
||||
|
||||
@property({ attribute: false }) public selector!: SelectSelector;
|
||||
|
||||
@property() public value?: string | string[];
|
||||
@@ -89,7 +75,11 @@ export class HaSelectSelector extends LitElement {
|
||||
|
||||
if (this.selector.select?.sort) {
|
||||
options.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a.label, b.label, this._locale.language)
|
||||
caseInsensitiveStringCompare(
|
||||
a.label,
|
||||
b.label,
|
||||
this.hass.locale.language
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -106,6 +96,7 @@ export class HaSelectSelector extends LitElement {
|
||||
.value=${this.value as string | undefined}
|
||||
@value-changed=${this._selectChanged}
|
||||
.maxColumns=${this.selector.select?.box_max_columns}
|
||||
.hass=${this.hass}
|
||||
></ha-select-box>
|
||||
${this._renderHelper()}
|
||||
`;
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
LocalizeFunc,
|
||||
@@ -169,9 +168,6 @@ export class HaSelectorSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public required = true;
|
||||
|
||||
@consumeLocalize()
|
||||
protected _localize?: LocalizeFunc;
|
||||
|
||||
private _yamlMode = false;
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues<this>) {
|
||||
@@ -240,7 +236,7 @@ export class HaSelectorSelector extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
const schema = this._schema(type, this._localize!);
|
||||
const schema = this._schema(type, this.hass.localize);
|
||||
|
||||
return html`<div>
|
||||
<p>${this.label ? this.label : ""}</p>
|
||||
@@ -294,7 +290,7 @@ export class HaSelectorSelector extends LitElement {
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (schema: any): string =>
|
||||
this._localize!(
|
||||
this.hass.localize(
|
||||
`ui.components.selectors.selector.${schema.name}` as LocalizeKeys
|
||||
) || schema.name;
|
||||
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import type { TimeSelector } from "../../data/selector";
|
||||
import { internationalizationContext } from "../../data/context";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import type { HomeAssistantInternationalization } from "../../types";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-time-input";
|
||||
import type { HaTimeInput } from "../ha-time-input";
|
||||
|
||||
@customElement("ha-selector-time")
|
||||
export class HaTimeSelector extends LitElement {
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
|
||||
transformer: ({ locale }) => locale,
|
||||
})
|
||||
private _locale!: FrontendLocaleData;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selector!: TimeSelector;
|
||||
|
||||
@@ -40,7 +31,7 @@ export class HaTimeSelector extends LitElement {
|
||||
return html`
|
||||
<ha-time-input
|
||||
.value=${typeof this.value === "string" ? this.value : undefined}
|
||||
.locale=${this._locale}
|
||||
.locale=${this.hass.locale}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
clearable
|
||||
|
||||
@@ -1,39 +1,24 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import type { Connection, HassConfig } from "home-assistant-js-websocket";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { transform } from "../common/decorators/transform";
|
||||
import { configContext, connectionContext } from "../data/context";
|
||||
import {
|
||||
DEFAULT_SERVICE_ICON,
|
||||
FALLBACK_DOMAIN_ICONS,
|
||||
serviceIcon,
|
||||
} from "../data/icons";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-service-icon")
|
||||
export class HaServiceIcon extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public service?: string;
|
||||
|
||||
@property() public icon?: string;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
@transform<{ config: HassConfig }, HassConfig>({
|
||||
transformer: ({ config }) => config,
|
||||
})
|
||||
private _config?: HassConfig;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
@transform<{ connection: Connection }, Connection>({
|
||||
transformer: ({ connection }) => connection,
|
||||
})
|
||||
private _connection?: Connection;
|
||||
|
||||
protected render() {
|
||||
if (this.icon) {
|
||||
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
|
||||
@@ -43,18 +28,20 @@ export class HaServiceIcon extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this._connection || !this._config) {
|
||||
if (!this.hass) {
|
||||
return this._renderFallback();
|
||||
}
|
||||
|
||||
const icon = serviceIcon(this._connection, this._config, this.service).then(
|
||||
(icn) => {
|
||||
if (icn) {
|
||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||
}
|
||||
return this._renderFallback();
|
||||
const icon = serviceIcon(
|
||||
this.hass.connection,
|
||||
this.hass.config,
|
||||
this.service
|
||||
).then((icn) => {
|
||||
if (icn) {
|
||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||
}
|
||||
);
|
||||
return this._renderFallback();
|
||||
});
|
||||
|
||||
return html`${until(icon)}`;
|
||||
}
|
||||
|
||||
@@ -1,36 +1,21 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import type { Connection, HassConfig } from "home-assistant-js-websocket";
|
||||
import { transform } from "../common/decorators/transform";
|
||||
import { configContext, connectionContext } from "../data/context";
|
||||
import { serviceSectionIcon } from "../data/icons";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon";
|
||||
import "./ha-svg-icon";
|
||||
import { serviceSectionIcon } from "../data/icons";
|
||||
|
||||
@customElement("ha-service-section-icon")
|
||||
export class HaServiceSectionIcon extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public service?: string;
|
||||
|
||||
@property() public section?: string;
|
||||
|
||||
@property() public icon?: string;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
@transform<{ config: HassConfig }, HassConfig>({
|
||||
transformer: ({ config }) => config,
|
||||
})
|
||||
private _config?: HassConfig;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
@transform<{ connection: Connection }, Connection>({
|
||||
transformer: ({ connection }) => connection,
|
||||
})
|
||||
private _connection?: Connection;
|
||||
|
||||
protected render() {
|
||||
if (this.icon) {
|
||||
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
|
||||
@@ -40,13 +25,13 @@ export class HaServiceSectionIcon extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this._connection || !this._config) {
|
||||
if (!this.hass) {
|
||||
return this._renderFallback();
|
||||
}
|
||||
|
||||
const icon = serviceSectionIcon(
|
||||
this._connection,
|
||||
this._config,
|
||||
this.hass.connection,
|
||||
this.hass.config,
|
||||
this.service,
|
||||
this.section
|
||||
).then((icn) => {
|
||||
|
||||
@@ -539,7 +539,11 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
rtl: isRTL,
|
||||
})}
|
||||
>
|
||||
<ha-user-badge slot="start" .user=${this.hass.user}></ha-user-badge>
|
||||
<ha-user-badge
|
||||
slot="start"
|
||||
.user=${this.hass.user}
|
||||
.hass=${this.hass}
|
||||
></ha-user-badge>
|
||||
<span class="item-text" slot="headline"
|
||||
>${this.hass.user ? this.hass.user.name : ""}</span
|
||||
>
|
||||
|
||||
@@ -82,6 +82,7 @@ export class HaThemePicker extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
@value-changed=${this._changed}
|
||||
popover-placement="bottom"
|
||||
></ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,25 +1,24 @@
|
||||
import { mdiLightbulbOutline } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import { consumeLocalize } from "../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-tip")
|
||||
class HaTip extends LitElement {
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
public render() {
|
||||
if (!this._localize) {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-svg-icon .path=${mdiLightbulbOutline}></ha-svg-icon>
|
||||
<span class="prefix">${this._localize("ui.panel.config.tips.tip")}</span>
|
||||
<span class="prefix"
|
||||
>${this.hass.localize("ui.panel.config.tips.tip")}</span
|
||||
>
|
||||
<span class="text"><slot></slot></span>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,231 +1,64 @@
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
|
||||
const PASSIVE_EVENT_OPTIONS = { passive: true } as const;
|
||||
|
||||
export const haTopAppBarFixedStyles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.top-app-bar {
|
||||
box-sizing: border-box;
|
||||
color: var(--app-header-text-color, #fff);
|
||||
background-color: var(--app-header-background-color, var(--primary-color));
|
||||
position: fixed;
|
||||
top: 0;
|
||||
inset-inline-end: 0;
|
||||
width: var(--ha-top-app-bar-width, 100%);
|
||||
z-index: 4;
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
transition:
|
||||
box-shadow var(--ha-animation-duration-normal) ease,
|
||||
width var(--ha-animation-duration-normal) ease,
|
||||
padding-left var(--ha-animation-duration-normal) ease,
|
||||
padding-right var(--ha-animation-duration-normal) ease;
|
||||
}
|
||||
|
||||
:host([narrow]) .top-app-bar {
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
}
|
||||
|
||||
.top-app-bar.scrolled:not(.pane-header) {
|
||||
box-shadow: var(--ha-box-shadow-s);
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: var(--header-height);
|
||||
border-bottom: var(--app-header-border-bottom);
|
||||
}
|
||||
|
||||
.section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
padding: 0 var(--ha-space-3);
|
||||
}
|
||||
|
||||
#navigation {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.section.center {
|
||||
flex: 1 1 auto;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.section.end {
|
||||
flex: none;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: var(--ha-font-size-xl);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
line-height: var(--header-height);
|
||||
padding-inline-start: var(--ha-space-6);
|
||||
}
|
||||
|
||||
:host([narrow]) .title {
|
||||
padding-inline-start: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.top-app-bar-fixed-adjust {
|
||||
padding-top: calc(
|
||||
var(--header-height, 0px) + var(--safe-area-inset-top, 0px)
|
||||
);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
}
|
||||
|
||||
:host([narrow]) .top-app-bar-fixed-adjust {
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
}
|
||||
`;
|
||||
import { TopAppBarFixedBase } from "@material/mwc-top-app-bar-fixed/mwc-top-app-bar-fixed-base";
|
||||
import { styles } from "@material/mwc-top-app-bar/mwc-top-app-bar.css";
|
||||
import { css } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("ha-top-app-bar-fixed")
|
||||
export class HaTopAppBarFixed extends LitElement {
|
||||
export class HaTopAppBarFixed extends TopAppBarFixedBase {
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@property({ attribute: "center-title", type: Boolean }) centerTitle = false;
|
||||
|
||||
@query(".top-app-bar") protected _barElement!: HTMLElement;
|
||||
|
||||
private _scrollTarget?: HTMLElement | Window;
|
||||
|
||||
@property({ attribute: false })
|
||||
public get scrollTarget(): HTMLElement | Window {
|
||||
return this._scrollTarget || window;
|
||||
}
|
||||
|
||||
public set scrollTarget(value: HTMLElement | Window) {
|
||||
const old = this.scrollTarget;
|
||||
this._unregisterListeners();
|
||||
this._scrollTarget = value;
|
||||
this._updateBarPosition();
|
||||
this.requestUpdate("scrollTarget", old);
|
||||
if (this.isConnected) {
|
||||
this._registerListeners();
|
||||
this._syncScrollState();
|
||||
}
|
||||
}
|
||||
|
||||
protected _isPaneHeader(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`${this._renderHeader()}${this._renderContent()}`;
|
||||
}
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
|
||||
if (this.hasUpdated) {
|
||||
this._updateBarPosition();
|
||||
this._registerListeners();
|
||||
this._syncScrollState();
|
||||
}
|
||||
}
|
||||
|
||||
protected _renderHeader() {
|
||||
const title = html`<span class="title">
|
||||
<slot name="title"></slot>
|
||||
</span>`;
|
||||
const paneHeader = this._isPaneHeader();
|
||||
|
||||
return html`
|
||||
<header
|
||||
class="top-app-bar ${classMap({
|
||||
"pane-header": paneHeader,
|
||||
})}"
|
||||
>
|
||||
<div class="row">
|
||||
${paneHeader
|
||||
? html`<section class="section" id="title">
|
||||
<slot name="navigationIcon"></slot>
|
||||
${title}
|
||||
</section>`
|
||||
: nothing}
|
||||
<section class="section" id="navigation">
|
||||
${paneHeader
|
||||
? nothing
|
||||
: html`<slot name="navigationIcon"></slot> ${this.centerTitle
|
||||
? nothing
|
||||
: title}`}
|
||||
</section>
|
||||
${!paneHeader && this.centerTitle
|
||||
? html`<section class="section center">${title}</section>`
|
||||
: nothing}
|
||||
<section class="section end" id="actions" role="toolbar">
|
||||
<slot name="actionItems"></slot>
|
||||
</section>
|
||||
</div>
|
||||
</header>
|
||||
`;
|
||||
}
|
||||
|
||||
protected _renderContent() {
|
||||
return html`<div class="top-app-bar-fixed-adjust">
|
||||
<slot></slot>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProperties);
|
||||
this._updateBarPosition();
|
||||
this._registerListeners();
|
||||
this._syncScrollState();
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._unregisterListeners();
|
||||
}
|
||||
|
||||
protected _updateBarPosition() {
|
||||
if (this._barElement) {
|
||||
this._barElement.style.position =
|
||||
this.scrollTarget === window ? "" : "absolute";
|
||||
}
|
||||
}
|
||||
|
||||
protected _syncScrollState = () => {
|
||||
const scrollTop =
|
||||
this.scrollTarget instanceof Window
|
||||
? this.scrollTarget.pageYOffset
|
||||
: this.scrollTarget.scrollTop;
|
||||
this._barElement?.classList.toggle("scrolled", scrollTop > 0);
|
||||
};
|
||||
|
||||
protected _registerListeners() {
|
||||
this.scrollTarget.addEventListener(
|
||||
"scroll",
|
||||
this._syncScrollState,
|
||||
PASSIVE_EVENT_OPTIONS
|
||||
);
|
||||
}
|
||||
|
||||
protected _unregisterListeners() {
|
||||
this.scrollTarget.removeEventListener("scroll", this._syncScrollState);
|
||||
}
|
||||
|
||||
static override styles: CSSResultGroup = haTopAppBarFixedStyles;
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
header {
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
}
|
||||
.mdc-top-app-bar__row {
|
||||
height: var(--header-height);
|
||||
border-bottom: var(--app-header-border-bottom);
|
||||
}
|
||||
.mdc-top-app-bar--fixed-adjust {
|
||||
padding-top: calc(
|
||||
var(--header-height, 0px) + var(--safe-area-inset-top, 0px)
|
||||
);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
}
|
||||
:host([narrow]) .mdc-top-app-bar--fixed-adjust {
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
}
|
||||
.mdc-top-app-bar {
|
||||
--mdc-typography-headline6-font-weight: var(--ha-font-weight-normal);
|
||||
color: var(--app-header-text-color, var(--mdc-theme-on-primary, #fff));
|
||||
background-color: var(
|
||||
--app-header-background-color,
|
||||
var(--mdc-theme-primary)
|
||||
);
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
transition:
|
||||
width var(--ha-animation-duration-normal) ease,
|
||||
padding-left var(--ha-animation-duration-normal) ease,
|
||||
padding-right var(--ha-animation-duration-normal) ease;
|
||||
}
|
||||
:host([narrow]) .mdc-top-app-bar {
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.mdc-top-app-bar {
|
||||
transition: 1ms;
|
||||
}
|
||||
}
|
||||
.mdc-top-app-bar__title {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
padding-inline-start: var(--ha-space-6);
|
||||
padding-inline-end: initial;
|
||||
}
|
||||
:host([narrow]) .mdc-top-app-bar__title {
|
||||
padding-inline-start: var(--ha-space-2);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { TopAppBarBase } from "@material/mwc-top-app-bar/mwc-top-app-bar-base";
|
||||
import { styles } from "@material/mwc-top-app-bar/mwc-top-app-bar.css";
|
||||
import { css } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-top-app-bar")
|
||||
export class HaTopAppBar extends TopAppBarBase {
|
||||
static override styles = [
|
||||
styles,
|
||||
css`
|
||||
.mdc-top-app-bar__row {
|
||||
height: var(--header-height);
|
||||
border-bottom: var(--app-header-border-bottom);
|
||||
}
|
||||
.mdc-top-app-bar--fixed-adjust {
|
||||
padding-top: calc(var(--safe-area-inset-top) + var(--header-height));
|
||||
}
|
||||
.mdc-top-app-bar {
|
||||
--mdc-typography-headline6-font-weight: var(--ha-font-weight-normal);
|
||||
color: var(--app-header-text-color, var(--mdc-theme-on-primary, #fff));
|
||||
background-color: var(
|
||||
--app-header-background-color,
|
||||
var(--mdc-theme-primary)
|
||||
);
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-top-app-bar": HaTopAppBar;
|
||||
}
|
||||
}
|
||||
@@ -17,16 +17,13 @@ import {
|
||||
mdiWeatherSunny,
|
||||
mdiWebhook,
|
||||
} from "@mdi/js";
|
||||
import { consume } from "@lit/context";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import type { Connection, HassConfig } from "home-assistant-js-websocket";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { transform } from "../common/decorators/transform";
|
||||
import { configContext, connectionContext } from "../data/context";
|
||||
import { FALLBACK_DOMAIN_ICONS, triggerIcon } from "../data/icons";
|
||||
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@@ -53,24 +50,12 @@ export const TRIGGER_ICONS = {
|
||||
|
||||
@customElement("ha-trigger-icon")
|
||||
export class HaTriggerIcon extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public trigger?: string;
|
||||
|
||||
@property() public icon?: string;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
@transform<{ config: HassConfig }, HassConfig>({
|
||||
transformer: ({ config }) => config,
|
||||
})
|
||||
private _config?: HassConfig;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
@transform<{ connection: Connection }, Connection>({
|
||||
transformer: ({ connection }) => connection,
|
||||
})
|
||||
private _connection?: Connection;
|
||||
|
||||
protected render() {
|
||||
if (this.icon) {
|
||||
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
|
||||
@@ -80,18 +65,20 @@ export class HaTriggerIcon extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this._connection || !this._config) {
|
||||
if (!this.hass) {
|
||||
return this._renderFallback();
|
||||
}
|
||||
|
||||
const icon = triggerIcon(this._connection, this._config, this.trigger).then(
|
||||
(icn) => {
|
||||
if (icn) {
|
||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||
}
|
||||
return this._renderFallback();
|
||||
const icon = triggerIcon(
|
||||
this.hass.connection,
|
||||
this.hass.config,
|
||||
this.trigger
|
||||
).then((icn) => {
|
||||
if (icn) {
|
||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||
}
|
||||
);
|
||||
return this._renderFallback();
|
||||
});
|
||||
|
||||
return html`${until(icon)}`;
|
||||
}
|
||||
|
||||
@@ -1,37 +1,136 @@
|
||||
import {
|
||||
addHasRemoveClass,
|
||||
BaseElement,
|
||||
} from "@material/mwc-base/base-element";
|
||||
import { supportsPassiveEventListener } from "@material/mwc-base/utils";
|
||||
import type { MDCTopAppBarAdapter } from "@material/top-app-bar/adapter";
|
||||
import { strings } from "@material/top-app-bar/constants";
|
||||
// eslint-disable-next-line import-x/no-named-as-default
|
||||
import MDCFixedTopAppBarFoundation from "@material/top-app-bar/fixed/foundation";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, css, nothing } from "lit";
|
||||
import { property, query, customElement } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styles } from "@material/mwc-top-app-bar/mwc-top-app-bar.css";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import {
|
||||
HaTopAppBarFixed,
|
||||
haTopAppBarFixedStyles,
|
||||
} from "./ha-top-app-bar-fixed";
|
||||
|
||||
const PASSIVE_EVENT_OPTIONS = { passive: true } as const;
|
||||
export const passiveEventOptionsIfSupported = supportsPassiveEventListener
|
||||
? { passive: true }
|
||||
: undefined;
|
||||
|
||||
@customElement("ha-two-pane-top-app-bar-fixed")
|
||||
export class HaTwoPaneTopAppBarFixed extends HaTopAppBarFixed {
|
||||
export class TopAppBarBaseBase extends BaseElement {
|
||||
protected override mdcFoundation!: MDCFixedTopAppBarFoundation;
|
||||
|
||||
protected override mdcFoundationClass = MDCFixedTopAppBarFoundation;
|
||||
|
||||
@query(".mdc-top-app-bar") protected mdcRoot!: HTMLElement;
|
||||
|
||||
// _actionItemsSlot should have type HTMLSlotElement, but when TypeScript's
|
||||
// emitDecoratorMetadata is enabled, the HTMLSlotElement constructor will
|
||||
// be emitted into the runtime, which will cause an "HTMLSlotElement is
|
||||
// undefined" error in browsers that don't define it (e.g. IE11).
|
||||
@query('slot[name="actionItems"]') protected _actionItemsSlot!: HTMLElement;
|
||||
|
||||
protected _scrollTarget!: HTMLElement | Window;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
|
||||
@property({ attribute: "center-title", type: Boolean }) centerTitle = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) prominent = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) dense = false;
|
||||
|
||||
@property({ type: Boolean }) pane = false;
|
||||
|
||||
@property({ type: Boolean }) footer = false;
|
||||
|
||||
@query(".content") private _contentElement?: HTMLElement;
|
||||
@query(".content") private _contentElement!: HTMLElement;
|
||||
|
||||
@query(".pane .ha-scrollbar") private _paneElement?: HTMLElement;
|
||||
|
||||
protected override _isPaneHeader(): boolean {
|
||||
return this.pane;
|
||||
@property({ attribute: false })
|
||||
get scrollTarget() {
|
||||
return this._scrollTarget || window;
|
||||
}
|
||||
|
||||
protected override _renderContent() {
|
||||
set scrollTarget(value) {
|
||||
this.unregisterListeners();
|
||||
const old = this.scrollTarget;
|
||||
this._scrollTarget = value;
|
||||
this.updateRootPosition();
|
||||
this.requestUpdate("scrollTarget", old);
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
protected updateRootPosition() {
|
||||
if (this.mdcRoot) {
|
||||
const windowScroller = this.scrollTarget === window;
|
||||
// we add support for top-app-bar's tied to an element scroller.
|
||||
this.mdcRoot.style.position = windowScroller ? "" : "absolute";
|
||||
}
|
||||
}
|
||||
|
||||
protected barClasses() {
|
||||
return {
|
||||
"mdc-top-app-bar--dense": this.dense,
|
||||
"mdc-top-app-bar--prominent": this.prominent,
|
||||
"center-title": this.centerTitle,
|
||||
"mdc-top-app-bar--fixed": true,
|
||||
"mdc-top-app-bar--pane": this.pane,
|
||||
};
|
||||
}
|
||||
|
||||
protected contentClasses() {
|
||||
return {
|
||||
"mdc-top-app-bar--fixed-adjust": !this.dense && !this.prominent,
|
||||
"mdc-top-app-bar--dense-fixed-adjust": this.dense && !this.prominent,
|
||||
"mdc-top-app-bar--prominent-fixed-adjust": !this.dense && this.prominent,
|
||||
"mdc-top-app-bar--dense-prominent-fixed-adjust":
|
||||
this.dense && this.prominent,
|
||||
"mdc-top-app-bar--pane": this.pane,
|
||||
};
|
||||
}
|
||||
|
||||
protected override render() {
|
||||
const title = html`<span class="mdc-top-app-bar__title"
|
||||
><slot name="title"></slot
|
||||
></span>`;
|
||||
return html`
|
||||
<div
|
||||
class=${classMap({
|
||||
"top-app-bar-fixed-adjust": true,
|
||||
"top-app-bar-fixed-adjust--pane": this.pane,
|
||||
})}
|
||||
>
|
||||
<header class="mdc-top-app-bar ${classMap(this.barClasses())}">
|
||||
<div class="mdc-top-app-bar__row">
|
||||
${this.pane
|
||||
? html`<section
|
||||
class="mdc-top-app-bar__section mdc-top-app-bar__section--align-start"
|
||||
id="title"
|
||||
>
|
||||
<slot
|
||||
name="navigationIcon"
|
||||
@click=${this.handleNavigationClick}
|
||||
></slot>
|
||||
${title}
|
||||
</section>`
|
||||
: nothing}
|
||||
<section class="mdc-top-app-bar__section" id="navigation">
|
||||
${this.pane
|
||||
? nothing
|
||||
: html`<slot
|
||||
name="navigationIcon"
|
||||
@click=${this.handleNavigationClick}
|
||||
></slot
|
||||
>${title}`}
|
||||
</section>
|
||||
<section
|
||||
class="mdc-top-app-bar__section mdc-top-app-bar__section--align-end"
|
||||
id="actions"
|
||||
role="toolbar"
|
||||
>
|
||||
<slot name="actionItems"></slot>
|
||||
</section>
|
||||
</div>
|
||||
</header>
|
||||
<div class=${classMap(this.contentClasses())}>
|
||||
${this.pane
|
||||
? html`<div class="pane">
|
||||
<div class="shadow-container"></div>
|
||||
@@ -55,57 +154,117 @@ export class HaTwoPaneTopAppBarFixed extends HaTopAppBarFixed {
|
||||
`;
|
||||
}
|
||||
|
||||
protected override willUpdate(changedProperties: PropertyValues<this>) {
|
||||
super.willUpdate(changedProperties);
|
||||
if (changedProperties.has("pane") && this.hasUpdated) {
|
||||
this._unregisterListeners();
|
||||
}
|
||||
}
|
||||
|
||||
protected override updated(changedProperties: PropertyValues<this>) {
|
||||
protected updated(changedProperties: PropertyValues<this>) {
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
changedProperties.has("pane") &&
|
||||
changedProperties.get("pane") !== undefined
|
||||
) {
|
||||
this._registerListeners();
|
||||
this._syncScrollState();
|
||||
this.unregisterListeners();
|
||||
this.registerListeners();
|
||||
}
|
||||
}
|
||||
|
||||
private _handlePaneScroll = (ev: Event) => {
|
||||
const target = ev.currentTarget as HTMLElement;
|
||||
target.parentElement?.classList.toggle("scrolled", target.scrollTop > 0);
|
||||
protected createAdapter(): MDCTopAppBarAdapter {
|
||||
return {
|
||||
...addHasRemoveClass(this.mdcRoot),
|
||||
setStyle: (prprty: string, value: string) =>
|
||||
this.mdcRoot.style.setProperty(prprty, value),
|
||||
getTopAppBarHeight: () => this.mdcRoot.clientHeight,
|
||||
notifyNavigationIconClicked: () => {
|
||||
this.dispatchEvent(
|
||||
new Event(strings.NAVIGATION_EVENT, {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
);
|
||||
},
|
||||
getViewportScrollY: () =>
|
||||
this.scrollTarget instanceof Window
|
||||
? this.scrollTarget.pageYOffset
|
||||
: this.scrollTarget.scrollTop,
|
||||
getTotalActionItems: () =>
|
||||
(this._actionItemsSlot as HTMLSlotElement).assignedNodes({
|
||||
flatten: true,
|
||||
}).length,
|
||||
};
|
||||
}
|
||||
|
||||
protected handleTargetScroll = () => {
|
||||
this.mdcFoundation.handleTargetScroll();
|
||||
};
|
||||
|
||||
protected override _registerListeners() {
|
||||
protected handlePaneScroll = (ev) => {
|
||||
if (ev.target.scrollTop > 0) {
|
||||
ev.target.parentElement.classList.add("scrolled");
|
||||
} else {
|
||||
ev.target.parentElement.classList.remove("scrolled");
|
||||
}
|
||||
};
|
||||
|
||||
protected handleNavigationClick = () => {
|
||||
this.mdcFoundation.handleNavigationClick();
|
||||
};
|
||||
|
||||
protected registerListeners() {
|
||||
if (this.pane) {
|
||||
this._paneElement?.addEventListener(
|
||||
this._paneElement!.addEventListener(
|
||||
"scroll",
|
||||
this._handlePaneScroll,
|
||||
PASSIVE_EVENT_OPTIONS
|
||||
this.handlePaneScroll,
|
||||
passiveEventOptionsIfSupported
|
||||
);
|
||||
this._contentElement?.addEventListener(
|
||||
this._contentElement.addEventListener(
|
||||
"scroll",
|
||||
this._handlePaneScroll,
|
||||
PASSIVE_EVENT_OPTIONS
|
||||
this.handlePaneScroll,
|
||||
passiveEventOptionsIfSupported
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
super._registerListeners();
|
||||
this.scrollTarget.addEventListener(
|
||||
"scroll",
|
||||
this.handleTargetScroll,
|
||||
passiveEventOptionsIfSupported
|
||||
);
|
||||
}
|
||||
|
||||
protected override _unregisterListeners() {
|
||||
this._paneElement?.removeEventListener("scroll", this._handlePaneScroll);
|
||||
this._contentElement?.removeEventListener("scroll", this._handlePaneScroll);
|
||||
super._unregisterListeners();
|
||||
protected unregisterListeners() {
|
||||
this._paneElement?.removeEventListener("scroll", this.handlePaneScroll);
|
||||
this._contentElement.removeEventListener("scroll", this.handlePaneScroll);
|
||||
this.scrollTarget.removeEventListener("scroll", this.handleTargetScroll);
|
||||
}
|
||||
|
||||
protected override firstUpdated() {
|
||||
super.firstUpdated();
|
||||
this.updateRootPosition();
|
||||
this.registerListeners();
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.unregisterListeners();
|
||||
}
|
||||
|
||||
static override styles = [
|
||||
haTopAppBarFixedStyles,
|
||||
styles,
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
header {
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
}
|
||||
.mdc-top-app-bar__row {
|
||||
height: var(--header-height);
|
||||
border-bottom: var(--app-header-border-bottom);
|
||||
}
|
||||
.mdc-top-app-bar--fixed-adjust {
|
||||
padding-top: calc(
|
||||
var(--header-height, 0px) + var(--safe-area-inset-top, 0px)
|
||||
);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
}
|
||||
:host([narrow]) .mdc-top-app-bar--fixed-adjust {
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
}
|
||||
.shadow-container {
|
||||
position: absolute;
|
||||
top: calc(-1 * var(--header-height));
|
||||
@@ -114,11 +273,39 @@ export class HaTwoPaneTopAppBarFixed extends HaTopAppBarFixed {
|
||||
z-index: 1;
|
||||
transition: box-shadow 200ms linear;
|
||||
}
|
||||
|
||||
.scrolled .shadow-container {
|
||||
box-shadow: var(--ha-box-shadow-m);
|
||||
box-shadow: var(
|
||||
--mdc-top-app-bar-fixed-box-shadow,
|
||||
0px 2px 4px -1px rgba(0, 0, 0, 0.2),
|
||||
0px 4px 5px 0px rgba(0, 0, 0, 0.14),
|
||||
0px 1px 10px 0px rgba(0, 0, 0, 0.12)
|
||||
);
|
||||
}
|
||||
.mdc-top-app-bar {
|
||||
--mdc-typography-headline6-font-weight: var(--ha-font-weight-normal);
|
||||
color: var(--app-header-text-color, var(--mdc-theme-on-primary, #fff));
|
||||
background-color: var(
|
||||
--app-header-background-color,
|
||||
var(--mdc-theme-primary)
|
||||
);
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
transition:
|
||||
width var(--ha-animation-duration-normal) ease,
|
||||
padding-left var(--ha-animation-duration-normal) ease,
|
||||
padding-right var(--ha-animation-duration-normal) ease;
|
||||
}
|
||||
:host([narrow]) .mdc-top-app-bar {
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.mdc-top-app-bar {
|
||||
transition: 1ms;
|
||||
}
|
||||
}
|
||||
.mdc-top-app-bar--pane.mdc-top-app-bar--fixed-scrolled {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
#title {
|
||||
border-right: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-inline-end: 1px solid rgba(255, 255, 255, 0.12);
|
||||
@@ -127,8 +314,7 @@ export class HaTwoPaneTopAppBarFixed extends HaTopAppBarFixed {
|
||||
flex: 0 0 var(--sidepane-width, 250px);
|
||||
width: var(--sidepane-width, 250px);
|
||||
}
|
||||
|
||||
.top-app-bar-fixed-adjust--pane {
|
||||
div.mdc-top-app-bar--pane {
|
||||
display: flex;
|
||||
height: calc(
|
||||
100vh - var(--header-height, 0px) - var(
|
||||
@@ -137,7 +323,6 @@ export class HaTwoPaneTopAppBarFixed extends HaTopAppBarFixed {
|
||||
) - var(--safe-area-inset-bottom, 0px)
|
||||
);
|
||||
}
|
||||
|
||||
.pane {
|
||||
border-right: 1px solid var(--divider-color);
|
||||
border-inline-end: 1px solid var(--divider-color);
|
||||
@@ -149,36 +334,36 @@ export class HaTwoPaneTopAppBarFixed extends HaTopAppBarFixed {
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pane .ha-scrollbar {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.pane .footer {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.main {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.top-app-bar-fixed-adjust--pane .main {
|
||||
.mdc-top-app-bar--pane .main {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.top-app-bar-fixed-adjust--pane .content {
|
||||
.mdc-top-app-bar--pane .content {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
.mdc-top-app-bar__title {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
padding-inline-start: 24px;
|
||||
padding-inline-end: initial;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-two-pane-top-app-bar-fixed": HaTwoPaneTopAppBarFixed;
|
||||
"ha-two-pane-top-app-bar-fixed": TopAppBarBaseBase;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { type LitElement, css } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { nativeElementInternalsSupported } from "../../common/feature-detect/support-native-element-internals";
|
||||
import type { Constructor } from "../../types";
|
||||
import { nativeElementInternalsSupported } from "../../common/feature-detect/support-native-element-internals";
|
||||
|
||||
/**
|
||||
* Minimal interface for the inner wa-input / wa-textarea element.
|
||||
@@ -339,7 +339,7 @@ export const waInputStyles = css`
|
||||
min-height: var(--ha-space-5);
|
||||
margin-block-start: 0;
|
||||
margin-inline-start: var(--ha-space-3);
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--ha-color-text-secondary);
|
||||
|
||||
@@ -227,7 +227,7 @@ class DialogMediaManage extends LitElement {
|
||||
</ha-list>
|
||||
`}
|
||||
${isComponentLoaded(this.hass.config, "hassio")
|
||||
? html`<ha-tip>
|
||||
? html`<ha-tip .hass=${this.hass}>
|
||||
${this.hass.localize(
|
||||
"ui.components.media-browser.file_management.tip_media_storage",
|
||||
{
|
||||
|
||||
@@ -1,147 +0,0 @@
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
brandsUrl,
|
||||
extractDomainFromBrandUrl,
|
||||
isBrandUrl,
|
||||
} from "../../util/brands-url";
|
||||
|
||||
const SMALL_THUMBNAIL_THRESHOLD = 16;
|
||||
|
||||
const isSvgUrl = (url: string): boolean =>
|
||||
/\.svg(\?|#|$)/i.test(url) || url.startsWith("data:image/svg+xml");
|
||||
|
||||
const resolveThumbnailURL = (
|
||||
hass: HomeAssistant,
|
||||
thumbnailUrl: string
|
||||
): Promise<string> => {
|
||||
if (isBrandUrl(thumbnailUrl)) {
|
||||
return Promise.resolve(
|
||||
brandsUrl(
|
||||
{
|
||||
domain: extractDomainFromBrandUrl(thumbnailUrl),
|
||||
type: "icon",
|
||||
darkOptimized: hass.themes?.darkMode,
|
||||
},
|
||||
hass.auth.data.hassUrl
|
||||
)
|
||||
);
|
||||
}
|
||||
if (thumbnailUrl.startsWith("/")) {
|
||||
// Local thumbnails require authentication; fetch and inline as base64.
|
||||
return hass
|
||||
.fetchWithAuth(thumbnailUrl)
|
||||
.then((response) => response.blob())
|
||||
.then(
|
||||
(blob) =>
|
||||
new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () =>
|
||||
resolve(typeof reader.result === "string" ? reader.result : "");
|
||||
reader.onerror = (e) => reject(e);
|
||||
reader.readAsDataURL(blob);
|
||||
})
|
||||
);
|
||||
}
|
||||
return Promise.resolve(thumbnailUrl);
|
||||
};
|
||||
|
||||
@customElement("ha-media-browser-thumbnail")
|
||||
export class HaMediaBrowserThumbnail extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public url?: string;
|
||||
|
||||
@state() private _resolvedUrl?: string;
|
||||
|
||||
@state() private _small = false;
|
||||
|
||||
@state() private _brand = false;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("url")) {
|
||||
this._resolve();
|
||||
}
|
||||
}
|
||||
|
||||
private async _resolve(): Promise<void> {
|
||||
this._small = false;
|
||||
this._brand = !!this.url && isBrandUrl(this.url);
|
||||
if (!this.url) {
|
||||
this._resolvedUrl = undefined;
|
||||
return;
|
||||
}
|
||||
const requested = this.url;
|
||||
try {
|
||||
const resolved = await resolveThumbnailURL(this.hass, requested);
|
||||
if (requested !== this.url) return;
|
||||
this._resolvedUrl = resolved;
|
||||
this._probeSize(resolved);
|
||||
} catch (_err) {
|
||||
if (requested === this.url) this._resolvedUrl = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _probeSize(url: string): void {
|
||||
// SVGs (including brand icons) scale natively; pixelated rendering would
|
||||
// break vector output.
|
||||
if (this.url && isBrandUrl(this.url)) return;
|
||||
if (isSvgUrl(url)) return;
|
||||
const img = new Image();
|
||||
img.addEventListener("load", () => {
|
||||
if (this._resolvedUrl !== url) return;
|
||||
if (
|
||||
img.naturalWidth > 0 &&
|
||||
img.naturalWidth <= SMALL_THUMBNAIL_THRESHOLD
|
||||
) {
|
||||
this._small = true;
|
||||
}
|
||||
});
|
||||
img.src = url;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
if (!this._resolvedUrl) return nothing;
|
||||
return html`
|
||||
<div
|
||||
class=${classMap({
|
||||
image: true,
|
||||
small: this._small,
|
||||
brand: this._brand,
|
||||
})}
|
||||
style="background-image: url(${this._resolvedUrl})"
|
||||
></div>
|
||||
`;
|
||||
}
|
||||
|
||||
static readonly styles: CSSResultGroup = css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-size: var(--ha-media-browser-thumbnail-fit, contain);
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
.image.brand {
|
||||
background-size: 40%;
|
||||
}
|
||||
.image.small {
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-media-browser-thumbnail": HaMediaBrowserThumbnail;
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { until } from "lit/directives/until";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { slugify } from "../../common/string/slugify";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
@@ -38,6 +39,11 @@ import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle, haStyleScrollbar } from "../../resources/styles";
|
||||
import { loadVirtualizer } from "../../resources/virtualizer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
brandsUrl,
|
||||
extractDomainFromBrandUrl,
|
||||
isBrandUrl,
|
||||
} from "../../util/brands-url";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import "../entity/ha-entity-picker";
|
||||
import "../ha-alert";
|
||||
@@ -46,7 +52,6 @@ import "../ha-card";
|
||||
import "../ha-icon-button";
|
||||
import "../ha-list";
|
||||
import "../ha-list-item";
|
||||
import "./ha-media-browser-thumbnail";
|
||||
import "../ha-spinner";
|
||||
import "../ha-svg-icon";
|
||||
import "../ha-tooltip";
|
||||
@@ -406,6 +411,12 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
? MediaClassBrowserSettings[currentItem.children_media_class]
|
||||
: MediaClassBrowserSettings.directory;
|
||||
|
||||
const backgroundImage = currentItem.thumbnail
|
||||
? this._getThumbnailURLorBase64(currentItem.thumbnail).then(
|
||||
(value) => `url(${value})`
|
||||
)
|
||||
: "none";
|
||||
|
||||
return html`
|
||||
${
|
||||
currentItem.can_play
|
||||
@@ -420,11 +431,13 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
<div class="header-content">
|
||||
${currentItem.thumbnail
|
||||
? html`
|
||||
<div class="img">
|
||||
<ha-media-browser-thumbnail
|
||||
.hass=${this.hass}
|
||||
.url=${currentItem.thumbnail}
|
||||
></ha-media-browser-thumbnail>
|
||||
<div
|
||||
class="img"
|
||||
style="background-image: ${until(
|
||||
backgroundImage,
|
||||
""
|
||||
)}"
|
||||
>
|
||||
${this.narrow &&
|
||||
currentItem?.can_play &&
|
||||
(!this.accept ||
|
||||
@@ -625,6 +638,12 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
|
||||
private _renderGridItem = (child: MediaPlayerItem): TemplateResult => {
|
||||
const backgroundImage = child.thumbnail
|
||||
? this._getThumbnailURLorBase64(child.thumbnail).then(
|
||||
(value) => `url(${value})`
|
||||
)
|
||||
: "none";
|
||||
|
||||
return html`
|
||||
<div class="child" .item=${child} @click=${this._childClicked}>
|
||||
<ha-card outlined>
|
||||
@@ -636,13 +655,10 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
"centered-image": ["app", "directory"].includes(
|
||||
child.media_class
|
||||
),
|
||||
"brand-image": isBrandUrl(child.thumbnail),
|
||||
})} image"
|
||||
>
|
||||
<ha-media-browser-thumbnail
|
||||
.hass=${this.hass}
|
||||
.url=${child.thumbnail}
|
||||
></ha-media-browser-thumbnail>
|
||||
</div>
|
||||
style="background-image: ${until(backgroundImage, "")}"
|
||||
></div>
|
||||
`
|
||||
: html`
|
||||
<div class="icon-holder image">
|
||||
@@ -687,7 +703,13 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
private _renderListItem = (child: MediaPlayerItem): TemplateResult => {
|
||||
const currentItem = this._currentItem;
|
||||
const mediaClass = MediaClassBrowserSettings[currentItem!.media_class];
|
||||
const showImage = mediaClass.show_list_images && !!child.thumbnail;
|
||||
|
||||
const backgroundImage =
|
||||
mediaClass.show_list_images && child.thumbnail
|
||||
? this._getThumbnailURLorBase64(child.thumbnail).then(
|
||||
(value) => `url(${value})`
|
||||
)
|
||||
: "none";
|
||||
|
||||
return html`
|
||||
<ha-list-item
|
||||
@@ -695,7 +717,7 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
.item=${child}
|
||||
.graphic=${mediaClass.show_list_images ? "medium" : "avatar"}
|
||||
>
|
||||
${!showImage && !child.can_play
|
||||
${backgroundImage === "none" && !child.can_play
|
||||
? html`<ha-svg-icon
|
||||
.path=${MediaClassBrowserSettings[
|
||||
child.media_class === "directory"
|
||||
@@ -709,14 +731,9 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
graphic: true,
|
||||
thumbnail: mediaClass.show_list_images === true,
|
||||
})}
|
||||
style="background-image: ${until(backgroundImage, "")}"
|
||||
slot="graphic"
|
||||
>
|
||||
${showImage
|
||||
? html`<ha-media-browser-thumbnail
|
||||
.hass=${this.hass}
|
||||
.url=${child.thumbnail}
|
||||
></ha-media-browser-thumbnail>`
|
||||
: nothing}
|
||||
${child.can_play
|
||||
? html`<ha-icon-button
|
||||
class="play ${classMap({
|
||||
@@ -736,6 +753,51 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
`;
|
||||
};
|
||||
|
||||
private async _getThumbnailURLorBase64(
|
||||
thumbnailUrl: string | undefined
|
||||
): Promise<string> {
|
||||
if (!thumbnailUrl) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (isBrandUrl(thumbnailUrl)) {
|
||||
// The backend is not aware of the theme used by the users,
|
||||
// so we rewrite the URL to show a proper icon
|
||||
return brandsUrl(
|
||||
{
|
||||
domain: extractDomainFromBrandUrl(thumbnailUrl),
|
||||
type: "icon",
|
||||
darkOptimized: this.hass.themes?.darkMode,
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
);
|
||||
}
|
||||
|
||||
if (thumbnailUrl.startsWith("/")) {
|
||||
// Thumbnails served by local API require authentication
|
||||
return new Promise((resolve, reject) => {
|
||||
this.hass
|
||||
.fetchWithAuth(thumbnailUrl!)
|
||||
// Since we are fetching with an authorization header, we cannot just put the
|
||||
// URL directly into the document; we need to embed the image. We could do this
|
||||
// using blob URLs, but then we would need to keep track of them in order to
|
||||
// release them properly. Instead, we embed the thumbnail using base64.
|
||||
.then((response) => response.blob())
|
||||
.then((blob) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const result = reader.result;
|
||||
resolve(typeof result === "string" ? result : "");
|
||||
};
|
||||
reader.onerror = (e) => reject(e);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return thumbnailUrl;
|
||||
}
|
||||
|
||||
private _actionClicked = (ev: MouseEvent): void => {
|
||||
ev.stopPropagation();
|
||||
const item = (ev.currentTarget as any).item;
|
||||
@@ -986,20 +1048,14 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.header-content .img {
|
||||
position: relative;
|
||||
height: 175px;
|
||||
width: 175px;
|
||||
margin-right: 16px;
|
||||
background-size: cover;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
width 0.4s,
|
||||
height 0.4s;
|
||||
--ha-media-browser-thumbnail-fit: cover;
|
||||
}
|
||||
.header-content .img ha-media-browser-thumbnail {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
.header-info {
|
||||
display: flex;
|
||||
@@ -1135,12 +1191,18 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
right: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
--ha-media-browser-thumbnail-fit: cover;
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.centered-image {
|
||||
margin: 0 8px;
|
||||
--ha-media-browser-thumbnail-fit: contain;
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.brand-image {
|
||||
background-size: 40%;
|
||||
}
|
||||
|
||||
.children ha-card .icon-holder {
|
||||
@@ -1216,21 +1278,17 @@ export class HaMediaPlayerBrowse extends LitElement {
|
||||
}
|
||||
|
||||
ha-list-item .graphic {
|
||||
position: relative;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
background-position: center;
|
||||
border-radius: var(--ha-border-radius-sm);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-content: center;
|
||||
align-items: center;
|
||||
line-height: initial;
|
||||
}
|
||||
|
||||
ha-list-item .graphic ha-media-browser-thumbnail {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
ha-list-item .graphic .play {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
margin: auto;
|
||||
opacity: 0;
|
||||
transition: all 0.5s;
|
||||
background-color: rgba(var(--rgb-card-background-color), 0.5);
|
||||
|
||||
@@ -99,8 +99,6 @@ export class HaRadioOption extends Radio {
|
||||
--ha-radio-option-checked-background-color,
|
||||
var(--ha-color-fill-primary-normal-resting)
|
||||
);
|
||||
color: var(--checked-icon-color);
|
||||
border-color: var(--checked-icon-color);
|
||||
}
|
||||
|
||||
[part~="label"] {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import type { LogbookEntry } from "../../data/logbook";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./hat-logbook-note";
|
||||
@@ -19,9 +17,6 @@ export class HaTraceLogbook extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public logbookEntries!: LogbookEntry[];
|
||||
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return this.logbookEntries.length
|
||||
? html`
|
||||
@@ -31,10 +26,13 @@ export class HaTraceLogbook extends LitElement {
|
||||
.entries=${this.logbookEntries}
|
||||
.narrow=${this.narrow}
|
||||
></ha-logbook-renderer>
|
||||
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
|
||||
<hat-logbook-note
|
||||
.hass=${this.hass}
|
||||
.domain=${this.trace.domain}
|
||||
></hat-logbook-note>
|
||||
`
|
||||
: html`<div class="padded-box">
|
||||
${this._localize(
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.trace.path.no_logbook_entries"
|
||||
)}
|
||||
</div>`;
|
||||
|
||||
@@ -374,7 +374,10 @@ export class HaTracePathDetails extends LitElement {
|
||||
.entries=${entries}
|
||||
.narrow=${this.narrow}
|
||||
></ha-logbook-renderer>
|
||||
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
|
||||
<hat-logbook-note
|
||||
.hass=${this.hass}
|
||||
.domain=${this.trace.domain}
|
||||
></hat-logbook-note>
|
||||
`
|
||||
: html`<div class="padded-box">
|
||||
${this.hass!.localize(
|
||||
|
||||
@@ -28,7 +28,10 @@ export class HaTraceTimeline extends LitElement {
|
||||
allow-pick
|
||||
>
|
||||
</hat-trace-timeline>
|
||||
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
|
||||
<hat-logbook-note
|
||||
.hass=${this.hass}
|
||||
.domain=${this.trace.domain}
|
||||
></hat-logbook-note>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
import { css, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
@customElement("hat-logbook-note")
|
||||
class HatLogbookNote extends LitElement {
|
||||
@property() public domain: "automation" | "script" = "automation";
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
@property() public domain: "automation" | "script" = "automation";
|
||||
|
||||
render() {
|
||||
if (this.domain === "script") {
|
||||
return this._localize(
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.automation.trace.messages.not_all_entries_are_related_script_note"
|
||||
);
|
||||
}
|
||||
return this._localize(
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.automation.trace.messages.not_all_entries_are_related_automation_note"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import type { BasePerson } from "../../data/person";
|
||||
import { computeUserInitials } from "../../data/user";
|
||||
import { connectionContext } from "../../data/context";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
@customElement("ha-person-badge")
|
||||
class PersonBadge extends LitElement {
|
||||
@property({ attribute: false }) public person?: BasePerson;
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
private _connection?: ContextType<typeof connectionContext>;
|
||||
@property({ attribute: false }) public person?: BasePerson;
|
||||
|
||||
protected render() {
|
||||
if (!this.person) {
|
||||
@@ -22,10 +19,10 @@ class PersonBadge extends LitElement {
|
||||
|
||||
const picture = this.person.picture;
|
||||
|
||||
if (picture && this._connection) {
|
||||
if (picture) {
|
||||
return html`<div
|
||||
style=${styleMap({
|
||||
backgroundImage: `url(${this._connection.hassUrl(picture)})`,
|
||||
backgroundImage: `url(${this.hass.hassUrl(picture)})`,
|
||||
})}
|
||||
class="picture"
|
||||
></div>`;
|
||||
|
||||
@@ -1,62 +1,57 @@
|
||||
import type { HassEntities, HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { consumeEntityState } from "../../common/decorators/consume-context-entry";
|
||||
import type { User } from "../../data/user";
|
||||
import { computeUserInitials } from "../../data/user";
|
||||
import { connectionContext, statesContext } from "../../data/context";
|
||||
import type { CurrentUser } from "../../types";
|
||||
import type { CurrentUser, HomeAssistant } from "../../types";
|
||||
|
||||
@customElement("ha-user-badge")
|
||||
class UserBadge extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public user?: User | CurrentUser;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
private _connection?: ContextType<typeof connectionContext>;
|
||||
@state() private _personPicture?: string;
|
||||
|
||||
@state()
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states?: HassEntities;
|
||||
private _personEntityId?: string;
|
||||
|
||||
@state() private _personEntityId?: string;
|
||||
|
||||
@state()
|
||||
@consumeEntityState({ entityIdPath: ["_personEntityId"] })
|
||||
private _personState?: HassEntity;
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
public willUpdate(changedProps: PropertyValues<this>) {
|
||||
super.willUpdate(changedProps);
|
||||
// Re-scan for the user's person entity when the user changes, or when the
|
||||
// states change while we don't have a (still-present) person entity. Once
|
||||
// resolved, `_personState` keeps the picture up to date via
|
||||
// `consumeEntityState`, so there's no need to rescan on every state update.
|
||||
if (changedProps.has("user")) {
|
||||
this._getPersonPicture();
|
||||
return;
|
||||
}
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
if (
|
||||
changedProps.has("user") ||
|
||||
(changedProps.has("_states") &&
|
||||
(!this._personEntityId || !this._states?.[this._personEntityId]))
|
||||
this._personEntityId &&
|
||||
oldHass &&
|
||||
this.hass.states[this._personEntityId] !==
|
||||
oldHass.states[this._personEntityId]
|
||||
) {
|
||||
this._updatePersonEntityId();
|
||||
const entityState = this.hass.states[this._personEntityId];
|
||||
if (entityState) {
|
||||
this._personPicture = entityState.attributes.entity_picture;
|
||||
} else {
|
||||
this._getPersonPicture();
|
||||
}
|
||||
} else if (!this._personEntityId && oldHass) {
|
||||
this._getPersonPicture();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.user) {
|
||||
if (!this.hass || !this.user) {
|
||||
return nothing;
|
||||
}
|
||||
const picture =
|
||||
this._personEntityId &&
|
||||
(this._personState?.attributes.entity_picture as string | undefined);
|
||||
const picture = this._personPicture;
|
||||
|
||||
if (picture && this._connection) {
|
||||
if (picture) {
|
||||
return html`<div
|
||||
style=${styleMap({
|
||||
backgroundImage: `url(${this._connection.hassUrl(picture)})`,
|
||||
backgroundImage: `url(${this.hass.hassUrl(picture)})`,
|
||||
})}
|
||||
class="picture"
|
||||
></div>`;
|
||||
@@ -69,18 +64,20 @@ class UserBadge extends LitElement {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _updatePersonEntityId() {
|
||||
private _getPersonPicture() {
|
||||
this._personEntityId = undefined;
|
||||
if (!this.user || !this._states) {
|
||||
this._personPicture = undefined;
|
||||
if (!this.hass || !this.user) {
|
||||
return;
|
||||
}
|
||||
for (const entity of Object.values(this._states)) {
|
||||
for (const entity of Object.values(this.hass.states)) {
|
||||
if (
|
||||
entity.attributes.user_id === this.user.id &&
|
||||
computeStateDomain(entity) === "person"
|
||||
) {
|
||||
this._personEntityId = entity.entity_id;
|
||||
return;
|
||||
this._personPicture = entity.attributes.entity_picture;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,11 @@ class HaUserPicker extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-user-badge slot="start" .user=${user}></ha-user-badge>
|
||||
<ha-user-badge
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.user=${user}
|
||||
></ha-user-badge>
|
||||
<span slot="headline">${user.name}</span>
|
||||
`;
|
||||
};
|
||||
@@ -90,7 +94,11 @@ class HaUserPicker extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-combo-box-item type="button" compact>
|
||||
<ha-user-badge slot="start" .user=${item.user}></ha-user-badge>
|
||||
<ha-user-badge
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.user=${item.user}
|
||||
></ha-user-badge>
|
||||
<span slot="headline">${item.primary}</span>
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createContext } from "@lit/context";
|
||||
import type {
|
||||
Connection,
|
||||
HassEntityAttributeBase,
|
||||
@@ -95,7 +96,7 @@ export interface TriggerList {
|
||||
|
||||
export interface BaseTrigger {
|
||||
alias?: string;
|
||||
note?: string;
|
||||
comment?: string;
|
||||
/** @deprecated Use `trigger` instead */
|
||||
platform?: string;
|
||||
trigger: string;
|
||||
@@ -241,7 +242,7 @@ export type Trigger = LegacyTrigger | TriggerList | PlatformTrigger;
|
||||
interface BaseCondition {
|
||||
condition: string;
|
||||
alias?: string;
|
||||
note?: string;
|
||||
comment?: string;
|
||||
enabled?: boolean;
|
||||
options?: Record<string, unknown>;
|
||||
}
|
||||
@@ -490,12 +491,12 @@ export const migrateAutomationTrigger = (
|
||||
|
||||
export const flattenTriggers = (
|
||||
triggers: undefined | Trigger | Trigger[]
|
||||
): Trigger[] => {
|
||||
): Exclude<Trigger, TriggerList>[] => {
|
||||
if (!triggers) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const flatTriggers: Trigger[] = [];
|
||||
const flatTriggers: Exclude<Trigger, TriggerList>[] = [];
|
||||
|
||||
ensureArray(triggers).forEach((t) => {
|
||||
if ("triggers" in t) {
|
||||
@@ -609,7 +610,7 @@ export interface AutomationClipboard {
|
||||
export interface BaseSidebarConfig {
|
||||
delete: () => void;
|
||||
close: (focus?: boolean) => void;
|
||||
editNote: () => void;
|
||||
editComment: () => void;
|
||||
}
|
||||
|
||||
export interface TriggerSidebarConfig extends BaseSidebarConfig {
|
||||
@@ -671,7 +672,7 @@ export interface OptionSidebarConfig extends BaseSidebarConfig {
|
||||
rename: () => void;
|
||||
duplicate: () => void;
|
||||
defaultOption?: boolean;
|
||||
note?: string;
|
||||
comment?: string;
|
||||
}
|
||||
|
||||
export interface ScriptFieldSidebarConfig extends BaseSidebarConfig {
|
||||
@@ -697,3 +698,7 @@ export interface ShowAutomationEditorParams {
|
||||
data?: Partial<AutomationConfig>;
|
||||
expanded?: boolean;
|
||||
}
|
||||
|
||||
export const automationConfigContext = createContext<
|
||||
AutomationConfig | undefined
|
||||
>("automationConfig");
|
||||
|
||||
@@ -27,6 +27,7 @@ import type {
|
||||
LegacyTrigger,
|
||||
Trigger,
|
||||
} from "./automation";
|
||||
import { flattenTriggers } from "./automation";
|
||||
import { getConditionDomain, getConditionObjectId } from "./condition";
|
||||
import type {
|
||||
DeviceCondition,
|
||||
@@ -107,6 +108,41 @@ const formatNumericLimitValue = (
|
||||
: value;
|
||||
};
|
||||
|
||||
export interface TriggerInfo {
|
||||
id: string;
|
||||
label: string;
|
||||
triggerType: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export const getTriggerInfos = (
|
||||
triggers: Trigger[] | undefined,
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[]
|
||||
): TriggerInfo[] => {
|
||||
if (!triggers) {
|
||||
return [];
|
||||
}
|
||||
const map = new Map<string, TriggerInfo>();
|
||||
for (const t of flattenTriggers(triggers)) {
|
||||
if (isTriggerList(t) || !t.id) {
|
||||
continue;
|
||||
}
|
||||
const existing = map.get(t.id);
|
||||
if (existing) {
|
||||
existing.count++;
|
||||
} else {
|
||||
map.set(t.id, {
|
||||
id: t.id,
|
||||
label: describeTrigger(t, hass, entityRegistry),
|
||||
triggerType: t.trigger,
|
||||
count: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
return Array.from(map.values());
|
||||
};
|
||||
|
||||
export const describeTrigger = (
|
||||
trigger: Trigger,
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -256,7 +256,6 @@ export const normalizeSubscriptionEventData = (
|
||||
dtstart: eventStart,
|
||||
dtend: eventEnd,
|
||||
description: eventData.description ?? undefined,
|
||||
location: eventData.location ?? undefined,
|
||||
uid: eventData.uid ?? undefined,
|
||||
recurrence_id: eventData.recurrence_id ?? undefined,
|
||||
rrule: eventData.rrule ?? undefined,
|
||||
|
||||
@@ -40,6 +40,7 @@ export const createConfigFlow = (
|
||||
"config/config_entries/flow",
|
||||
{
|
||||
handler,
|
||||
show_advanced_options: Boolean(hass.userData?.showAdvanced),
|
||||
entry_id,
|
||||
},
|
||||
HEADERS
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { createContext } from "@lit/context";
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
HomeAssistantApi,
|
||||
@@ -11,12 +10,10 @@ import type {
|
||||
HomeAssistantRegistries,
|
||||
HomeAssistantUI,
|
||||
} from "../../types";
|
||||
import type { RelatedIdSets } from "../../common/search/related-context";
|
||||
import type { ConfigEntry } from "../config_entries";
|
||||
import type { EntityRegistryEntry } from "../entity/entity_registry";
|
||||
import type { DomainManifestLookup } from "../integration";
|
||||
import type { LabelRegistryEntry } from "../label/label_registry";
|
||||
import type { ItemType } from "../search";
|
||||
|
||||
/**
|
||||
* Entity, device, area, and floor registries
|
||||
@@ -97,11 +94,6 @@ export const areasContext = createContext<HomeAssistant["areas"]>("areas");
|
||||
*/
|
||||
export const floorsContext = createContext<HomeAssistant["floors"]>("floors");
|
||||
|
||||
/**
|
||||
* Whether the main Home Assistant viewport is using the narrow layout.
|
||||
*/
|
||||
export const narrowViewportContext = createContext<boolean>("narrowViewport");
|
||||
|
||||
// #region lazy-contexts
|
||||
|
||||
/**
|
||||
@@ -170,30 +162,3 @@ export const panelsContext = createContext<HomeAssistant["panels"]>("panels");
|
||||
export const authContext = createContext<HomeAssistant["auth"]>("auth");
|
||||
|
||||
// #endregion deprecated-contexts
|
||||
|
||||
// #region related-context
|
||||
|
||||
export interface RelatedContextItem {
|
||||
itemType: ItemType;
|
||||
itemId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolved related entities/devices/areas for the current page context.
|
||||
* Set by `RelatedContextProvider` when a page fires `hass-related-context`.
|
||||
* Cleared on navigation.
|
||||
*/
|
||||
export const relatedContext = createContext<RelatedIdSets | undefined>(
|
||||
"related"
|
||||
);
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"hass-related-context": RelatedContextItem | undefined;
|
||||
}
|
||||
interface HTMLElementEventMap {
|
||||
"hass-related-context": HASSDomEvent<RelatedContextItem | undefined>;
|
||||
}
|
||||
}
|
||||
|
||||
// #endregion related-context
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
|
||||
export interface DeviceAutomation {
|
||||
alias?: string;
|
||||
note?: string;
|
||||
comment?: string;
|
||||
device_id: string;
|
||||
domain: string;
|
||||
entity_id?: string;
|
||||
|
||||
@@ -161,10 +161,6 @@ export interface VacuumEntityOptions {
|
||||
last_seen_segments?: Segment[];
|
||||
}
|
||||
|
||||
export interface DeviceTrackerEntityOptions {
|
||||
associated_zone?: string | null;
|
||||
}
|
||||
|
||||
export interface EntityRegistryOptions {
|
||||
number?: NumberEntityOptions;
|
||||
sensor?: SensorEntityOptions;
|
||||
@@ -176,7 +172,6 @@ export interface EntityRegistryOptions {
|
||||
cover?: CoverEntityOptions;
|
||||
valve?: ValveEntityOptions;
|
||||
vacuum?: VacuumEntityOptions;
|
||||
device_tracker?: DeviceTrackerEntityOptions;
|
||||
switch_as_x?: SwitchAsXEntityOptions;
|
||||
conversation?: Record<string, unknown>;
|
||||
"cloud.alexa"?: Record<string, unknown>;
|
||||
@@ -202,8 +197,7 @@ export interface EntityRegistryEntryUpdateParams {
|
||||
| LightEntityOptions
|
||||
| CoverEntityOptions
|
||||
| ValveEntityOptions
|
||||
| VacuumEntityOptions
|
||||
| DeviceTrackerEntityOptions;
|
||||
| VacuumEntityOptions;
|
||||
aliases?: (string | null)[];
|
||||
labels?: string[];
|
||||
categories?: Record<string, string | null>;
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Connection } from "home-assistant-js-websocket";
|
||||
import type { ShortcutItem } from "./home_shortcuts";
|
||||
|
||||
export interface CoreFrontendUserData {
|
||||
showAdvanced?: boolean;
|
||||
showEntityIdPicker?: boolean;
|
||||
default_panel?: string;
|
||||
apps_info_dismissed?: boolean;
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { LovelaceCardFeatureContext } from "../panels/lovelace/card-features/types";
|
||||
import type { LovelaceCardConfig } from "./lovelace/config/card";
|
||||
|
||||
export interface CustomCardSuggestion<
|
||||
T extends LovelaceCardConfig = LovelaceCardConfig,
|
||||
> {
|
||||
label?: string;
|
||||
config: T;
|
||||
}
|
||||
|
||||
export interface CustomCardEntry {
|
||||
type: string;
|
||||
@@ -16,10 +8,6 @@ export interface CustomCardEntry {
|
||||
description?: string;
|
||||
preview?: boolean;
|
||||
documentationURL?: string;
|
||||
getEntitySuggestion?: (
|
||||
hass: HomeAssistant,
|
||||
entityId: string
|
||||
) => CustomCardSuggestion | CustomCardSuggestion[] | null;
|
||||
}
|
||||
|
||||
export interface CustomBadgeEntry {
|
||||
|
||||
@@ -2,10 +2,7 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { navigate } from "../common/navigate";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import {
|
||||
subscribeDeviceRegistry,
|
||||
type DeviceRegistryEntry,
|
||||
} from "./device/device_registry";
|
||||
import { subscribeDeviceRegistry } from "./device/device_registry";
|
||||
import { getThreadDataSetTLV, listThreadDataSets } from "./thread";
|
||||
|
||||
export enum NetworkType {
|
||||
@@ -80,9 +77,9 @@ export const startExternalCommissioning = async (hass: HomeAssistant) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const watchForNewMatterDevice = (
|
||||
export const redirectOnNewMatterDevice = (
|
||||
hass: HomeAssistant,
|
||||
callback: (device: DeviceRegistryEntry) => void
|
||||
callback?: () => void
|
||||
): UnsubscribeFunc => {
|
||||
let curMatterDevices: Set<string> | undefined;
|
||||
const unsubDeviceReg = subscribeDeviceRegistry(hass.connection, (entries) => {
|
||||
@@ -104,7 +101,8 @@ export const watchForNewMatterDevice = (
|
||||
if (newMatterDevices.length) {
|
||||
unsubDeviceReg();
|
||||
curMatterDevices = undefined;
|
||||
callback(newMatterDevices[0]);
|
||||
callback?.();
|
||||
navigate(`/config/devices/device/${newMatterDevices[0].id}`);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
@@ -113,15 +111,6 @@ export const watchForNewMatterDevice = (
|
||||
};
|
||||
};
|
||||
|
||||
export const redirectOnNewMatterDevice = (
|
||||
hass: HomeAssistant,
|
||||
callback?: () => void
|
||||
): UnsubscribeFunc =>
|
||||
watchForNewMatterDevice(hass, (device) => {
|
||||
callback?.();
|
||||
navigate(`/config/devices/device/${device.id}`);
|
||||
});
|
||||
|
||||
export const addMatterDevice = (hass: HomeAssistant) => {
|
||||
startExternalCommissioning(hass);
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ export const createOptionsFlow = (hass: HomeAssistant, handler: string) =>
|
||||
"config/config_entries/options/flow",
|
||||
{
|
||||
handler,
|
||||
show_advanced_options: Boolean(hass.userData?.showAdvanced),
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ export const isMaxMode = arrayLiteralIncludes(MODES_MAX);
|
||||
|
||||
export const baseActionStruct = object({
|
||||
alias: optional(string()),
|
||||
note: optional(string()),
|
||||
comment: optional(string()),
|
||||
continue_on_error: optional(boolean()),
|
||||
enabled: optional(boolean()),
|
||||
});
|
||||
@@ -106,7 +106,7 @@ export interface Field {
|
||||
|
||||
interface BaseAction {
|
||||
alias?: string;
|
||||
note?: string;
|
||||
comment?: string;
|
||||
continue_on_error?: boolean;
|
||||
enabled?: boolean;
|
||||
}
|
||||
@@ -197,7 +197,7 @@ export interface ForEachRepeat extends BaseRepeat {
|
||||
|
||||
export interface Option {
|
||||
alias?: string;
|
||||
note?: string;
|
||||
comment?: string;
|
||||
conditions: string | Condition[];
|
||||
sequence: Action | Action[];
|
||||
}
|
||||
|
||||
@@ -36,11 +36,11 @@ export type ItemType =
|
||||
| "script_blueprint";
|
||||
|
||||
export const findRelated = (
|
||||
hass: Pick<HomeAssistant, "callWS">,
|
||||
hass: HomeAssistant,
|
||||
itemType: ItemType,
|
||||
itemId: string
|
||||
): Promise<RelatedResult> =>
|
||||
hass.callWS<RelatedResult>({
|
||||
hass.callWS({
|
||||
type: "search/related",
|
||||
item_type: itemType,
|
||||
item_id: itemId,
|
||||
|
||||
@@ -125,7 +125,7 @@ export interface BooleanSelector {
|
||||
boolean: {} | null;
|
||||
}
|
||||
|
||||
export type AutomationBehaviorTriggerMode = "first" | "all" | "each";
|
||||
export type AutomationBehaviorTriggerMode = "first" | "last" | "any";
|
||||
|
||||
export type AutomationBehaviorConditionMode = "all" | "any";
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ export const createSubConfigFlow = (
|
||||
"config/config_entries/subentries/flow",
|
||||
{
|
||||
handler: [configEntryId, subFlowType],
|
||||
show_advanced_options: Boolean(hass.userData?.showAdvanced),
|
||||
subentry_id,
|
||||
},
|
||||
HEADERS
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
Trigger,
|
||||
TriggerList,
|
||||
} from "./automation";
|
||||
import { flattenTriggers } from "./automation";
|
||||
import type { Selector, TargetSelector } from "./selector";
|
||||
|
||||
export const TRIGGER_COLLECTIONS: AutomationElementGroupCollection[] = [
|
||||
@@ -56,6 +57,49 @@ export const TRIGGER_COLLECTIONS: AutomationElementGroupCollection[] = [
|
||||
export const isTriggerList = (trigger: Trigger): trigger is TriggerList =>
|
||||
"triggers" in trigger;
|
||||
|
||||
export const getTriggerIds = (triggers: Trigger[]): string[] =>
|
||||
flattenTriggers(triggers)
|
||||
.map((trigger) => trigger.id)
|
||||
.filter((id): id is string => !!id);
|
||||
|
||||
export const getNextNumericTriggerId = (triggers: Trigger[]): string => {
|
||||
let max = 0;
|
||||
for (const id of getTriggerIds(triggers)) {
|
||||
const num = Number(id);
|
||||
if (Number.isInteger(num) && num > max) {
|
||||
max = num;
|
||||
}
|
||||
}
|
||||
return String(max + 1);
|
||||
};
|
||||
|
||||
const computeUniqueId = (id: string, existing: Set<string>): string => {
|
||||
if (!existing.has(id)) {
|
||||
return id;
|
||||
}
|
||||
|
||||
// Split into a base and a trailing integer suffix so we can bump the
|
||||
// suffix on collision (e.g. "foo2" -> "foo3"); if there's no trailing
|
||||
// digit we start at 2 ("foo" -> "foo2").
|
||||
const match = id.match(/^(.*?)(\d+)$/);
|
||||
let base: string;
|
||||
let num: number;
|
||||
if (match) {
|
||||
base = match[1];
|
||||
num = Number(match[2]) + 1;
|
||||
} else {
|
||||
base = id;
|
||||
num = 2;
|
||||
}
|
||||
while (existing.has(`${base}${num}`)) {
|
||||
num++;
|
||||
}
|
||||
return `${base}${num}`;
|
||||
};
|
||||
|
||||
export const getUniqueTriggerId = (id: string, triggers: Trigger[]): string =>
|
||||
computeUniqueId(id, new Set(getTriggerIds(triggers)));
|
||||
|
||||
export interface TriggerDescription {
|
||||
target?: TargetSelector["target"];
|
||||
fields: Record<
|
||||
|
||||
@@ -87,6 +87,19 @@ const HOME_ASSISTANT_CORE_TITLE = "Home Assistant Core";
|
||||
const HOME_ASSISTANT_SUPERVISOR_TITLE = "Home Assistant Supervisor";
|
||||
const HOME_ASSISTANT_OS_TITLE = "Home Assistant Operating System";
|
||||
|
||||
// The hassio integration sets these as hard-coded `_attr_title` on the Core,
|
||||
// Operating System, and Supervisor update entities. They are not translated,
|
||||
// so a title comparison is the reliable way to identify them without depending
|
||||
// on the (lazily-fetched) entity sources.
|
||||
export const isSystemUpdate = (entity: UpdateEntity): boolean => {
|
||||
const title = entity.attributes.title || "";
|
||||
return (
|
||||
title === HOME_ASSISTANT_CORE_TITLE ||
|
||||
title === HOME_ASSISTANT_OS_TITLE ||
|
||||
title === HOME_ASSISTANT_SUPERVISOR_TITLE
|
||||
);
|
||||
};
|
||||
|
||||
export const filterUpdateEntities = (
|
||||
entities: HassEntities,
|
||||
language?: string
|
||||
@@ -133,6 +146,11 @@ export const filterUpdateEntitiesParameterized = (
|
||||
return updateCanInstall(entity, showSkipped);
|
||||
});
|
||||
|
||||
export const installUpdates = (hass: HomeAssistant, entityIds: string[]) =>
|
||||
hass.callService("update", "install", {
|
||||
entity_id: entityIds,
|
||||
});
|
||||
|
||||
export const checkForEntityUpdates = async (
|
||||
element: HTMLElement,
|
||||
hass: HomeAssistant
|
||||
|
||||