Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e5aca12d1 | |||
| 3391caffcb | |||
| 2483a917f8 | |||
| d9cae08f53 | |||
| 106b35d6cf | |||
| f12d305688 | |||
| d2326b4f62 | |||
| ea9424053a | |||
| 70ffef8807 | |||
| a32169f300 | |||
| b508760d24 | |||
| 541cab83de | |||
| 8e8f2bfa4c | |||
| bafe21ab48 | |||
| ee56d7d003 | |||
| 486b6bb561 | |||
| 9a9ceaebf2 | |||
| ff5bbf46ae | |||
| 47fb4a2def | |||
| 0e716e5078 | |||
| 9e9fdfbad6 | |||
| 5ebdb99ba7 | |||
| 1ca454cf02 | |||
| 859d23c187 | |||
| f9d205defe | |||
| 00cc4e2a5a | |||
| 6571feb556 | |||
| 4150bc0806 | |||
| 958e3f2575 | |||
| 3d3292e2ad | |||
| 75b9fb2e34 | |||
| 38f0ce306b | |||
| 1ffd19e20b | |||
| 9a216cae46 | |||
| 41e6408508 | |||
| 97e85bc06f | |||
| 5f2ad7fa01 | |||
| 7b6b70023b | |||
| 256a06e35f | |||
| 4e26c05ac6 | |||
| 04ee8ac415 | |||
| 63e144309c | |||
| 77039cda8e | |||
| ab5b4ed792 | |||
| a08905cd31 | |||
| a35349196f | |||
| dbdfdedd74 | |||
| a5c8547b2b | |||
| e373689a37 | |||
| 5edcdb8977 | |||
| 26b8921e8c | |||
| b8c201b6d3 | |||
| 4a6c23c93e | |||
| e2712cb0b0 | |||
| db52cd0d8e | |||
| 4891783c86 | |||
| b73732acdb | |||
| d950514104 | |||
| f37cf1e848 | |||
| a188ef1b7a |
@@ -41,14 +41,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
||||
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@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
uses: github/codeql-action/autobuild@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
||||
|
||||
# ℹ️ 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@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
|
||||
uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
|
||||
|
||||
@@ -13,13 +13,11 @@ jobs:
|
||||
lock:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
|
||||
- uses: dessant/lock-threads@89ae32b08ed1a541efecbab17912962a5e38981c # v6.0.2
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
process-only: "issues, prs"
|
||||
issue-lock-inactive-days: "30"
|
||||
issue-exclude-created-before: "2020-10-01T00:00:00Z"
|
||||
issue-inactive-days: "30"
|
||||
issue-lock-reason: ""
|
||||
pr-lock-inactive-days: "1"
|
||||
pr-exclude-created-before: "2020-11-01T00:00:00Z"
|
||||
pr-inactive-days: "1"
|
||||
pr-lock-reason: ""
|
||||
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Verify version
|
||||
uses: home-assistant/actions/helpers/verify-version@f6f29a7ee3fa0eccadf3620a7b9ee00ab54ec03b # master
|
||||
uses: home-assistant/actions/helpers/verify-version@868e6cb4607727d764341a158d98872cd63fa658 # 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@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||
uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
days-before-stale: 90
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.29.2",
|
||||
"@babel/runtime": "7.29.7",
|
||||
"@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.6",
|
||||
"@formatjs/intl-displaynames": "7.3.8",
|
||||
"@formatjs/intl-durationformat": "0.10.12",
|
||||
"@formatjs/intl-datetimeformat": "7.4.7",
|
||||
"@formatjs/intl-displaynames": "7.3.9",
|
||||
"@formatjs/intl-durationformat": "0.10.13",
|
||||
"@formatjs/intl-getcanonicallocales": "3.2.9",
|
||||
"@formatjs/intl-listformat": "8.3.8",
|
||||
"@formatjs/intl-listformat": "8.3.9",
|
||||
"@formatjs/intl-locale": "5.3.8",
|
||||
"@formatjs/intl-numberformat": "9.3.9",
|
||||
"@formatjs/intl-pluralrules": "6.3.8",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.8",
|
||||
"@formatjs/intl-numberformat": "9.3.10",
|
||||
"@formatjs/intl-pluralrules": "6.3.9",
|
||||
"@formatjs/intl-relativetimeformat": "12.3.9",
|
||||
"@fullcalendar/core": "6.1.20",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"@fullcalendar/interaction": "6.1.20",
|
||||
@@ -62,14 +62,13 @@
|
||||
"@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/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.21",
|
||||
"@swc/helpers": "0.5.23",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "4.0.5",
|
||||
"@tsparticles/preset-links": "4.0.5",
|
||||
@@ -126,21 +125,20 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.29.0",
|
||||
"@babel/core": "7.29.7",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.8",
|
||||
"@babel/plugin-transform-runtime": "7.29.0",
|
||||
"@babel/preset-env": "7.29.5",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.22.1",
|
||||
"@babel/plugin-transform-runtime": "7.29.7",
|
||||
"@babel/preset-env": "7.29.7",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.22.2",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@html-eslint/eslint-plugin": "0.61.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.11",
|
||||
"@rspack/core": "2.0.4",
|
||||
"@rspack/dev-server": "2.0.1",
|
||||
"@types/babel__plugin-transform-runtime": "7.9.5",
|
||||
"@rsdoctor/rspack-plugin": "1.5.12",
|
||||
"@rspack/core": "2.0.5",
|
||||
"@rspack/dev-server": "2.0.3",
|
||||
"@types/chromecast-caf-receiver": "6.0.26",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
"@types/color-name": "2.0.0",
|
||||
@@ -152,11 +150,9 @@
|
||||
"@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",
|
||||
@@ -183,7 +179,7 @@
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "29.1.1",
|
||||
"jszip": "3.10.1",
|
||||
"license-checker-rseidelsohn": "4.4.2",
|
||||
"license-checker-rseidelsohn": "5.0.1",
|
||||
"lint-staged": "17.0.5",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
@@ -195,10 +191,10 @@
|
||||
"serve": "14.2.6",
|
||||
"sinon": "22.0.0",
|
||||
"tar": "7.5.15",
|
||||
"terser-webpack-plugin": "5.6.0",
|
||||
"terser-webpack-plugin": "5.6.1",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "6.0.3",
|
||||
"typescript-eslint": "8.59.4",
|
||||
"typescript-eslint": "8.60.0",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.1.7",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
|
||||
|
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 |
|
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 |
@@ -11,6 +11,7 @@ import {
|
||||
} 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 {
|
||||
@@ -26,6 +27,28 @@ 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,
|
||||
@@ -63,27 +86,52 @@ export const consumeEntityState = (config: ConsumeEntryConfig) =>
|
||||
);
|
||||
|
||||
/**
|
||||
* 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).
|
||||
* 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.
|
||||
*/
|
||||
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;
|
||||
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;
|
||||
}
|
||||
);
|
||||
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
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
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;
|
||||
});
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
ECElementEvent,
|
||||
LegendComponentOption,
|
||||
LineSeriesOption,
|
||||
TooltipOption,
|
||||
XAXisOption,
|
||||
YAXisOption,
|
||||
} from "echarts/types/dist/shared";
|
||||
@@ -29,22 +30,59 @@ 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 } from "../../resources/echarts/echarts";
|
||||
import type {
|
||||
ECOption,
|
||||
HaECOption,
|
||||
HaECSeries,
|
||||
HaECSeriesItem,
|
||||
HaTooltipOption,
|
||||
} 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?: {
|
||||
@@ -66,9 +104,9 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public data: ECOption["series"] = [];
|
||||
@property({ attribute: false }) public data: HaECSeries = [];
|
||||
|
||||
@property({ attribute: false }) public options?: ECOption;
|
||||
@property({ attribute: false }) public options?: HaECOption;
|
||||
|
||||
@property({ type: String }) public height?: string;
|
||||
|
||||
@@ -614,7 +652,7 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
// Return an array of all IDs associated with the legend item of the primaryId
|
||||
private _getAllIdsFromLegend(
|
||||
options: ECOption | undefined,
|
||||
options: HaECOption | undefined,
|
||||
primaryId: string
|
||||
): string[] {
|
||||
if (!options) return [primaryId];
|
||||
@@ -634,7 +672,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: ECOption | undefined) {
|
||||
private _updateHiddenStatsFromOptions(options: HaECOption | undefined) {
|
||||
if (!options) return;
|
||||
const legend = ensureArray(this.options?.legend || [])[0] as
|
||||
| LegendComponentOption
|
||||
@@ -757,22 +795,34 @@ export class HaChartBase extends LitElement {
|
||||
xAxis,
|
||||
};
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
return options;
|
||||
return options as ECOption;
|
||||
}
|
||||
|
||||
private _createTheme(style: CSSStyleDeclaration) {
|
||||
@@ -960,8 +1010,12 @@ export class HaChartBase extends LitElement {
|
||||
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.sampling === "minmax") {
|
||||
if ((s as LineSeriesOption).sampling === "minmax") {
|
||||
const minX = xAxis?.min
|
||||
? xAxis.min instanceof Date
|
||||
? xAxis.min.getTime()
|
||||
@@ -976,8 +1030,8 @@ export class HaChartBase extends LitElement {
|
||||
? xAxis.max
|
||||
: undefined
|
||||
: undefined;
|
||||
return {
|
||||
...s,
|
||||
result = {
|
||||
...result,
|
||||
sampling: undefined,
|
||||
data: downSampleLineData(
|
||||
data as LineSeriesOption["data"],
|
||||
@@ -985,11 +1039,10 @@ export class HaChartBase extends LitElement {
|
||||
minX,
|
||||
maxX
|
||||
),
|
||||
};
|
||||
} as HaECSeriesItem;
|
||||
}
|
||||
}
|
||||
const name = filterXSS(String(s.name ?? s.id ?? ""));
|
||||
return { ...s, name, data };
|
||||
return processSeriesTooltipFormatter(result);
|
||||
});
|
||||
return series as ECOption["series"];
|
||||
}
|
||||
@@ -1326,8 +1379,8 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
|
||||
private _compareCustomLegendOptions(
|
||||
oldOptions: ECOption | undefined,
|
||||
newOptions: ECOption | undefined
|
||||
oldOptions: HaECOption | undefined,
|
||||
newOptions: HaECOption | undefined
|
||||
): boolean {
|
||||
const oldLegends = ensureArray(
|
||||
oldOptions?.legend || []
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
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 } from "lit";
|
||||
import type { PropertyValues, TemplateResult } 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 { ECOption } from "../../resources/echarts/echarts";
|
||||
import type { HaECOption } 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
|
||||
) => string;
|
||||
) => TemplateResult | typeof nothing | null;
|
||||
|
||||
/**
|
||||
* 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"]): ECOption => ({
|
||||
(categories?: NetworkData["categories"]): HaECOption => ({
|
||||
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 { ECOption } from "../../resources/echarts/echarts";
|
||||
import type { HaECOption } 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 = {
|
||||
const options: HaECOption = {
|
||||
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,12 +103,16 @@ export class HaSankeyChart extends LitElement {
|
||||
: data.value;
|
||||
if (data.id) {
|
||||
const node = this.data.nodes.find((n) => n.id === data.id);
|
||||
return `${params.marker} ${filterXSS(node?.label ?? data.id)}<br>${value}`;
|
||||
return html`<ha-chart-tooltip-marker
|
||||
.color=${String(params.color ?? "")}
|
||||
></ha-chart-tooltip-marker>
|
||||
${node?.label ?? data.id}<br />${value}`;
|
||||
}
|
||||
if (data.source && data.target) {
|
||||
const source = this.data.nodes.find((n) => n.id === data.source);
|
||||
const target = this.data.nodes.find((n) => n.id === data.target);
|
||||
return `${filterXSS(source?.label ?? data.source)} → ${filterXSS(target?.label ?? data.target)}<br>${value}`;
|
||||
return html`${source?.label ?? data.source} →
|
||||
${target?.label ?? data.target}<br />${value}`;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -5,10 +5,9 @@ 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 { filterXSS } from "../../common/util/xss";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HaECOption } from "../../resources/echarts/echarts";
|
||||
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");
|
||||
@@ -25,8 +24,6 @@ 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?: (
|
||||
@@ -50,13 +47,13 @@ export class HaSunburstChart extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const options = {
|
||||
const options: HaECOption = {
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
formatter: this._renderTooltip,
|
||||
appendTo: document.body,
|
||||
},
|
||||
} as ECOption;
|
||||
};
|
||||
|
||||
return html`<ha-chart-base
|
||||
.data=${this._createData(this.data)}
|
||||
@@ -71,7 +68,10 @@ export class HaSunburstChart extends LitElement {
|
||||
const value = this.valueFormatter
|
||||
? this.valueFormatter(data.value)
|
||||
: data.value;
|
||||
return `${params.marker} ${filterXSS(data.name)}<br>${value}`;
|
||||
return html`<ha-chart-tooltip-marker
|
||||
.color=${String(params.color ?? "")}
|
||||
></ha-chart-tooltip-marker>
|
||||
${data.name}<br />${value}`;
|
||||
};
|
||||
|
||||
private _createData = memoizeOne(
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
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 } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { VisualMapComponentOption } from "echarts/components";
|
||||
import type { LineSeriesOption } from "echarts/charts";
|
||||
@@ -12,8 +12,9 @@ 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 { ECOption } from "../../resources/echarts/echarts";
|
||||
import type { HaECOption } from "../../resources/echarts/echarts";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import {
|
||||
getNumberFormatOptions,
|
||||
@@ -24,7 +25,6 @@ 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?: ECOption;
|
||||
@state() private _chartOptions?: HaECOption;
|
||||
|
||||
private _hiddenStats = new Set<string>();
|
||||
|
||||
@@ -141,12 +141,11 @@ 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
|
||||
) + "<br>";
|
||||
const title = formatDateTimeWithSeconds(
|
||||
new Date(time),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
);
|
||||
const datapoints: Record<string, any>[] = [];
|
||||
this._chartData.forEach((dataset, index) => {
|
||||
if (
|
||||
@@ -177,52 +176,44 @@ export class StateHistoryChartLine extends LitElement {
|
||||
seriesName: dataset.name,
|
||||
seriesIndex: index,
|
||||
value: lastData,
|
||||
// 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>`,
|
||||
color: dataset.color,
|
||||
});
|
||||
});
|
||||
const unit = this.unit
|
||||
? `${blankBeforeUnit(this.unit, this.hass.locale)}${this.unit}`
|
||||
: "";
|
||||
|
||||
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>")
|
||||
);
|
||||
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}`;
|
||||
})}`;
|
||||
};
|
||||
|
||||
private _datasetHidden(ev: CustomEvent) {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } 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";
|
||||
@@ -15,8 +14,9 @@ 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 { ECOption } from "../../resources/echarts/echarts";
|
||||
import type { HaECOption, HaECSeries } 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?: ECOption;
|
||||
@state() private _chartOptions?: HaECOption;
|
||||
|
||||
@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 ECOption["series"]}
|
||||
.data=${this._chartData as HaECSeries}
|
||||
small-controls
|
||||
@chart-click=${this._handleChartClick}
|
||||
@chart-zoom=${this._handleDataZoom}
|
||||
@@ -132,42 +132,35 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
return rect;
|
||||
};
|
||||
|
||||
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)}`;
|
||||
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)}`;
|
||||
|
||||
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("");
|
||||
};
|
||||
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}`;
|
||||
};
|
||||
|
||||
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 } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -34,12 +34,13 @@ import {
|
||||
isExternalStatistic,
|
||||
statisticsHaveType,
|
||||
} from "../../data/recorder";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
import type { HaECOption } 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";
|
||||
|
||||
@@ -126,7 +127,7 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
@state() private _statisticIds: string[] = [];
|
||||
|
||||
@state() private _chartOptions?: ECOption;
|
||||
@state() private _chartOptions?: HaECOption;
|
||||
|
||||
@state() private _hiddenStats = new Set<string>();
|
||||
|
||||
@@ -251,91 +252,101 @@ export class StatisticsChart extends LitElement {
|
||||
const unit = this.unit
|
||||
? `${blankBeforeUnit(this.unit, this.hass.locale)}${this.unit}`
|
||||
: "";
|
||||
return params
|
||||
.map((param, index: number) => {
|
||||
if (rendered[param.seriesIndex]) return "";
|
||||
rendered[param.seriesIndex] = true;
|
||||
const rows: {
|
||||
time?: string;
|
||||
color: string;
|
||||
seriesName?: string;
|
||||
value: string;
|
||||
}[] = [];
|
||||
for (const param of params) {
|
||||
if (rendered[param.seriesIndex]) continue;
|
||||
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]);
|
||||
}
|
||||
} 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>";
|
||||
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 {
|
||||
// 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>`;
|
||||
startTime = new Date(param.value[0]);
|
||||
}
|
||||
|
||||
const options = getNumberFormatOptions(stateObj, entry) ?? {
|
||||
maximumFractionDigits: 2,
|
||||
};
|
||||
|
||||
const value = `${formatNumber(
|
||||
rawValue,
|
||||
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]),
|
||||
this.hass.locale,
|
||||
options
|
||||
)}${unit}`;
|
||||
this.hass.config
|
||||
);
|
||||
}
|
||||
|
||||
const time = index === 0 ? rawTime : "";
|
||||
return `${time}${param.marker} ${param.seriesName}: ${value}`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("<br>");
|
||||
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}`
|
||||
)}`;
|
||||
};
|
||||
|
||||
private _createOptions() {
|
||||
|
||||
@@ -107,17 +107,15 @@ export class HaDevicePicker extends LitElement {
|
||||
excludeDevices?: string[],
|
||||
value?: string
|
||||
) =>
|
||||
getDevices(
|
||||
this.hass,
|
||||
configEntryLookup,
|
||||
getDevices(this.hass, configEntryLookup, {
|
||||
includeDomains,
|
||||
excludeDomains,
|
||||
includeDeviceClasses,
|
||||
deviceFilter,
|
||||
entityFilter,
|
||||
excludeDevices,
|
||||
value
|
||||
)
|
||||
value,
|
||||
})
|
||||
);
|
||||
|
||||
protected firstUpdated(_changedProperties: PropertyValues<this>): void {
|
||||
|
||||
@@ -309,7 +309,29 @@ export class HaEntityPicker extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _getEntitiesMemoized = memoizeOne(getEntities);
|
||||
private _getEntitiesMemoized = memoizeOne(
|
||||
(
|
||||
hass: HomeAssistant,
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
includeDeviceClasses?: string[],
|
||||
includeUnitOfMeasurement?: string[],
|
||||
includeEntities?: string[],
|
||||
excludeEntities?: string[],
|
||||
value?: string
|
||||
) =>
|
||||
getEntities(hass, {
|
||||
includeDomains,
|
||||
excludeDomains,
|
||||
entityFilter,
|
||||
includeDeviceClasses,
|
||||
includeUnitOfMeasurement,
|
||||
includeEntities,
|
||||
excludeEntities,
|
||||
value,
|
||||
})
|
||||
);
|
||||
|
||||
private _getItems = () => {
|
||||
const items = this._getEntitiesMemoized(
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { mdiChartLine, mdiHelpCircleOutline, mdiShape } from "@mdi/js";
|
||||
import {
|
||||
mdiChartLine,
|
||||
mdiHelpCircleOutline,
|
||||
mdiPencil,
|
||||
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 { fireEvent } from "../../common/dom/fire_event";
|
||||
import { type HASSDomEvent, 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";
|
||||
@@ -53,6 +58,16 @@ 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;
|
||||
@@ -130,6 +145,8 @@ 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) ||
|
||||
@@ -341,6 +358,15 @@ 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}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -350,6 +376,12 @@ 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,9 +1,10 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { type HASSDomEvent, 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 {
|
||||
@@ -59,6 +60,8 @@ class HaStatisticsPicker extends LitElement {
|
||||
})
|
||||
public ignoreRestrictionsOnFirstStatistic = false;
|
||||
|
||||
@property({ attribute: "can-edit", type: Boolean }) public canEdit?;
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
@@ -99,7 +102,9 @@ 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>
|
||||
`
|
||||
@@ -122,6 +127,17 @@ 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,7 +43,6 @@ class StateInfo extends LitElement {
|
||||
)}:
|
||||
</span>
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${this.stateObj.last_changed}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
@@ -55,7 +54,6 @@ class StateInfo extends LitElement {
|
||||
)}:
|
||||
</span>
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${this.stateObj.last_updated}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
@@ -63,7 +61,6 @@ 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,18 +1,34 @@
|
||||
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 } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { transform } from "../common/decorators/transform";
|
||||
import { absoluteTime } from "../common/datetime/absolute_time";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { configContext, internationalizationContext } from "../data/context";
|
||||
import type {
|
||||
HomeAssistantConfig,
|
||||
HomeAssistantInternationalization,
|
||||
} 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 {
|
||||
@@ -62,13 +78,17 @@ class HaAbsoluteTime extends ReactiveElement {
|
||||
}
|
||||
|
||||
private _updateAbsolute(): void {
|
||||
if (!this._i18n || !this._config) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.datetime) {
|
||||
this.innerHTML = this.hass.localize("ui.components.absolute_time.never");
|
||||
this.innerHTML = this._i18n.localize("ui.components.absolute_time.never");
|
||||
} else {
|
||||
this.innerHTML = absoluteTime(
|
||||
new Date(this.datetime),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
this._i18n.locale,
|
||||
this._config
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { areaComboBoxKeys, getAreas } from "../data/area/area_picker";
|
||||
import { createAreaRegistryEntry } from "../data/area/area_registry";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { showAreaRegistryDetailDialog } from "../panels/config/areas/show-dialog-area-registry-detail";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../data/entity/entity";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-combo-box-item";
|
||||
@@ -104,7 +105,29 @@ export class HaAreaPicker extends LitElement {
|
||||
await this._picker?.open();
|
||||
}
|
||||
|
||||
private _getAreasMemoized = memoizeOne(getAreas);
|
||||
private _getAreasMemoized = memoizeOne(
|
||||
(
|
||||
haAreas: HomeAssistant["areas"],
|
||||
haFloors: HomeAssistant["floors"],
|
||||
haDevices: HomeAssistant["devices"],
|
||||
haEntities: HomeAssistant["entities"],
|
||||
haStates: HomeAssistant["states"],
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
includeDeviceClasses?: string[],
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
excludeAreas?: string[]
|
||||
) =>
|
||||
getAreas(haAreas, haFloors, haDevices, haEntities, haStates, {
|
||||
includeDomains,
|
||||
excludeDomains,
|
||||
includeDeviceClasses,
|
||||
deviceFilter,
|
||||
entityFilter,
|
||||
excludeAreas,
|
||||
})
|
||||
);
|
||||
|
||||
// Recompute value renderer when the areas change
|
||||
private _computeValueRenderer = memoizeOne(
|
||||
|
||||
@@ -20,6 +20,7 @@ export class HaCheckListItem extends CheckListItemBase {
|
||||
separateCheckboxClick = false;
|
||||
|
||||
async onChange(event) {
|
||||
event.stopPropagation();
|
||||
super.onChange(event);
|
||||
fireEvent(this, event.type);
|
||||
}
|
||||
|
||||
@@ -11,12 +11,15 @@ import {
|
||||
mdiStateMachine,
|
||||
mdiWeatherSunny,
|
||||
} from "@mdi/js";
|
||||
import { consume } from "@lit/context";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } 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";
|
||||
|
||||
@@ -36,12 +39,24 @@ 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>`;
|
||||
@@ -51,13 +66,13 @@ export class HaConditionIcon extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this.hass) {
|
||||
if (!this._connection || !this._config) {
|
||||
return this._renderFallback();
|
||||
}
|
||||
|
||||
const icon = conditionIcon(
|
||||
this.hass.connection,
|
||||
this.hass.config,
|
||||
this._connection,
|
||||
this._config,
|
||||
this.condition
|
||||
).then((icn) => {
|
||||
if (icn) {
|
||||
|
||||
@@ -3,6 +3,8 @@ 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";
|
||||
@@ -20,6 +22,10 @@ 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";
|
||||
@@ -54,7 +60,7 @@ export class HaFilterBlueprints extends LitElement {
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
${this.hass.localize("ui.panel.config.blueprint.caption")}
|
||||
${this._localize("ui.panel.config.blueprint.caption")}
|
||||
${this.value?.length
|
||||
? html`<div class="badge">${this.value?.length}</div>
|
||||
<ha-icon-button
|
||||
|
||||
@@ -4,6 +4,8 @@ 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";
|
||||
@@ -22,6 +24,10 @@ 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[] = [];
|
||||
|
||||
@@ -44,9 +50,7 @@ export class HaFilterVoiceAssistants extends LitElement {
|
||||
@expanded-changed=${this._expandedChanged}
|
||||
>
|
||||
<div slot="header" class="header">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.dashboard.voice_assistants.main"
|
||||
)}
|
||||
${this._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-start";
|
||||
| "left-end" = "bottom";
|
||||
|
||||
/** If set picker shows an add button instead of textbox when value isn't set */
|
||||
@property({ attribute: "add-button-label" }) public addButtonLabel?: string;
|
||||
|
||||
@@ -121,7 +121,6 @@ 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>
|
||||
|
||||
@@ -50,7 +50,9 @@ class HaLabel extends LitElement {
|
||||
<div class="container" .id=${this._elementId}>
|
||||
<span class="content">
|
||||
<slot name="icon"></slot>
|
||||
<slot></slot>
|
||||
<span class="label-content">
|
||||
<slot></slot>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
@@ -113,6 +115,10 @@ class HaLabel extends LitElement {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.label-content {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
:host([dense]) {
|
||||
height: 20px;
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
@@ -126,6 +132,29 @@ 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,7 +152,6 @@ 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,19 +1,23 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { parseISO } from "date-fns";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { relativeTime } from "../common/datetime/relative_time";
|
||||
import { capitalizeFirstLetter } from "../common/string/capitalize-first-letter";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { internationalizationContext } from "../data/context";
|
||||
import type { HomeAssistantInternationalization } 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 {
|
||||
@@ -57,15 +61,19 @@ class HaRelativeTime extends ReactiveElement {
|
||||
}
|
||||
|
||||
private _updateRelative(): void {
|
||||
if (!this._i18n) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.datetime) {
|
||||
this.innerHTML = this.hass.localize("ui.components.relative_time.never");
|
||||
this.innerHTML = this._i18n.localize("ui.components.relative_time.never");
|
||||
} else {
|
||||
const date =
|
||||
typeof this.datetime === "string"
|
||||
? parseISO(this.datetime)
|
||||
: this.datetime;
|
||||
|
||||
const relTime = relativeTime(date, this.hass.locale);
|
||||
const relTime = relativeTime(date, this._i18n.locale);
|
||||
this.innerHTML = this.capitalize
|
||||
? capitalizeFirstLetter(relTime)
|
||||
: relTime;
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { consumeEntityStates } from "../../common/decorators/consume-context-entry";
|
||||
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 {
|
||||
@@ -27,6 +29,10 @@ 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
|
||||
@@ -73,7 +79,7 @@ export class HaSelectorAttribute extends LitElement {
|
||||
const entityIds = ensureArray(this.context.filter_entity);
|
||||
|
||||
invalid = !entityIds.some((entityId) => {
|
||||
const stateObj = this.hass.states[entityId];
|
||||
const stateObj = this._filterEntityStates?.[entityId];
|
||||
return (
|
||||
stateObj &&
|
||||
this.value in stateObj.attributes &&
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
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,14 +1,26 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } 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 type { HomeAssistant, ToggleButton } from "../../types";
|
||||
import { internationalizationContext } from "../../data/context";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import type {
|
||||
HomeAssistantInternationalization,
|
||||
ToggleButton,
|
||||
} from "../../types";
|
||||
import "../ha-button-toggle-group";
|
||||
|
||||
@customElement("ha-selector-button_toggle")
|
||||
export class HaButtonToggleSelector 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!: ButtonToggleSelector;
|
||||
|
||||
@@ -48,11 +60,7 @@ export class HaButtonToggleSelector extends LitElement {
|
||||
|
||||
if (this.selector.button_toggle?.sort) {
|
||||
options.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(
|
||||
a.label,
|
||||
b.label,
|
||||
this.hass.locale.language
|
||||
)
|
||||
caseInsensitiveStringCompare(a.label, b.label, this._locale.language)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,12 @@ 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";
|
||||
|
||||
@@ -28,6 +30,9 @@ 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 {
|
||||
@@ -62,7 +67,7 @@ export class HaChooseSelector extends LitElement {
|
||||
.buttons=${this._toggleButtons(
|
||||
this.selector.choose.choices,
|
||||
this.selector.choose.translation_key,
|
||||
this.hass.localize
|
||||
this._localize
|
||||
)}
|
||||
.active=${this._activeChoice}
|
||||
@value-changed=${this._choiceChanged}
|
||||
@@ -83,7 +88,7 @@ export class HaChooseSelector extends LitElement {
|
||||
(
|
||||
choices: ChooseSelector["choose"]["choices"],
|
||||
translationKey?: string,
|
||||
_localize?: HomeAssistant["localize"]
|
||||
_localize?: LocalizeFunc
|
||||
) =>
|
||||
Object.keys(choices).map((choice) => ({
|
||||
label:
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import type { DateSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { internationalizationContext } from "../../data/context";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import type { HomeAssistantInternationalization } from "../../types";
|
||||
import "../ha-date-input";
|
||||
import type { HaDateInput } from "../ha-date-input";
|
||||
|
||||
@customElement("ha-selector-date")
|
||||
export class HaDateSelector 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!: DateSelector;
|
||||
|
||||
@@ -31,7 +40,7 @@ export class HaDateSelector extends LitElement {
|
||||
return html`
|
||||
<ha-date-input
|
||||
.label=${this.label}
|
||||
.locale=${this.hass.locale}
|
||||
.locale=${this._locale}
|
||||
.disabled=${this.disabled}
|
||||
.value=${typeof this.value === "string" ? this.value : undefined}
|
||||
.required=${this.required}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import type { DateTimeSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { internationalizationContext } from "../../data/context";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import type { HomeAssistantInternationalization } from "../../types";
|
||||
import "../ha-date-input";
|
||||
import type { HaDateInput } from "../ha-date-input";
|
||||
import "../ha-time-input";
|
||||
@@ -11,7 +15,12 @@ import type { HaTimeInput } from "../ha-time-input";
|
||||
|
||||
@customElement("ha-selector-datetime")
|
||||
export class HaDateTimeSelector 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!: DateTimeSelector;
|
||||
|
||||
@@ -41,7 +50,7 @@ export class HaDateTimeSelector extends LitElement {
|
||||
<div class="input">
|
||||
<ha-date-input
|
||||
.label=${this.label}
|
||||
.locale=${this.hass.locale}
|
||||
.locale=${this._locale}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.value=${values?.[0]}
|
||||
@@ -51,7 +60,7 @@ export class HaDateTimeSelector extends LitElement {
|
||||
<ha-time-input
|
||||
enable-second
|
||||
.value=${values?.[1] || "00:00:00"}
|
||||
.locale=${this.hass.locale}
|
||||
.locale=${this._locale}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
@value-changed=${this._valueChanged}
|
||||
|
||||
@@ -2,14 +2,11 @@ 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,10 +3,12 @@ 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")
|
||||
@@ -25,6 +27,9 @@ 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;
|
||||
@@ -42,7 +47,7 @@ export class HaFileSelector extends LitElement {
|
||||
.uploading=${this._busy}
|
||||
.value=${this.value
|
||||
? this._filename?.name ||
|
||||
this.hass.localize("ui.components.selectors.file.unknown_file")
|
||||
this._localize!("ui.components.selectors.file.unknown_file")
|
||||
: undefined}
|
||||
@file-picked=${this._uploadFile}
|
||||
@change=${this._removeFile}
|
||||
@@ -72,7 +77,7 @@ export class HaFileSelector extends LitElement {
|
||||
fireEvent(this, "value-changed", { value: fileId });
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize("ui.components.selectors.file.upload_failed", {
|
||||
text: this._localize!("ui.components.selectors.file.upload_failed", {
|
||||
reason: err.message || err,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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";
|
||||
@@ -41,6 +42,9 @@ export class HaPeriodSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@consumeLocalize()
|
||||
protected _localize?: LocalizeFunc;
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(
|
||||
selectedPeriodKey: PeriodKey | undefined,
|
||||
@@ -78,7 +82,7 @@ export class HaPeriodSelector extends LitElement {
|
||||
const schema = this._schema(
|
||||
typeof data.period === "string" ? (data.period as PeriodKey) : undefined,
|
||||
this.selector,
|
||||
this.hass.localize
|
||||
this._localize!
|
||||
);
|
||||
|
||||
return html`
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
import { mdiDragHorizontalVariant } from "@mdi/js";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } 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 { HomeAssistant } from "../../types";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
HomeAssistantInternationalization,
|
||||
} from "../../types";
|
||||
import "../chips/ha-chip-set";
|
||||
import "../chips/ha-input-chip";
|
||||
import "../ha-checkbox";
|
||||
@@ -25,6 +32,13 @@ 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[];
|
||||
@@ -75,11 +89,7 @@ export class HaSelectSelector extends LitElement {
|
||||
|
||||
if (this.selector.select?.sort) {
|
||||
options.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(
|
||||
a.label,
|
||||
b.label,
|
||||
this.hass.locale.language
|
||||
)
|
||||
caseInsensitiveStringCompare(a.label, b.label, this._locale.language)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ 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,
|
||||
@@ -168,6 +169,9 @@ 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>) {
|
||||
@@ -236,7 +240,7 @@ export class HaSelectorSelector extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
const schema = this._schema(type, this.hass.localize);
|
||||
const schema = this._schema(type, this._localize!);
|
||||
|
||||
return html`<div>
|
||||
<p>${this.label ? this.label : ""}</p>
|
||||
@@ -290,7 +294,7 @@ export class HaSelectorSelector extends LitElement {
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (schema: any): string =>
|
||||
this.hass.localize(
|
||||
this._localize!(
|
||||
`ui.components.selectors.selector.${schema.name}` as LocalizeKeys
|
||||
) || schema.name;
|
||||
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { transform } from "../../common/decorators/transform";
|
||||
import type { TimeSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { internationalizationContext } from "../../data/context";
|
||||
import type { FrontendLocaleData } from "../../data/translation";
|
||||
import type { HomeAssistantInternationalization } from "../../types";
|
||||
import "../ha-time-input";
|
||||
import type { HaTimeInput } from "../ha-time-input";
|
||||
|
||||
@customElement("ha-selector-time")
|
||||
export class HaTimeSelector 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!: TimeSelector;
|
||||
|
||||
@@ -31,7 +40,7 @@ export class HaTimeSelector extends LitElement {
|
||||
return html`
|
||||
<ha-time-input
|
||||
.value=${typeof this.value === "string" ? this.value : undefined}
|
||||
.locale=${this.hass.locale}
|
||||
.locale=${this._locale}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
clearable
|
||||
|
||||
@@ -1,24 +1,39 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } 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>`;
|
||||
@@ -28,20 +43,18 @@ export class HaServiceIcon extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this.hass) {
|
||||
if (!this._connection || !this._config) {
|
||||
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>`;
|
||||
const icon = serviceIcon(this._connection, this._config, this.service).then(
|
||||
(icn) => {
|
||||
if (icn) {
|
||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||
}
|
||||
return this._renderFallback();
|
||||
}
|
||||
return this._renderFallback();
|
||||
});
|
||||
);
|
||||
|
||||
return html`${until(icon)}`;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,36 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import type { HomeAssistant } from "../types";
|
||||
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 "./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>`;
|
||||
@@ -25,13 +40,13 @@ export class HaServiceSectionIcon extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this.hass) {
|
||||
if (!this._connection || !this._config) {
|
||||
return this._renderFallback();
|
||||
}
|
||||
|
||||
const icon = serviceSectionIcon(
|
||||
this.hass.connection,
|
||||
this.hass.config,
|
||||
this._connection,
|
||||
this._config,
|
||||
this.service,
|
||||
this.section
|
||||
).then((icn) => {
|
||||
|
||||
@@ -539,11 +539,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
rtl: isRTL,
|
||||
})}
|
||||
>
|
||||
<ha-user-badge
|
||||
slot="start"
|
||||
.user=${this.hass.user}
|
||||
.hass=${this.hass}
|
||||
></ha-user-badge>
|
||||
<ha-user-badge slot="start" .user=${this.hass.user}></ha-user-badge>
|
||||
<span class="item-text" slot="headline"
|
||||
>${this.hass.user ? this.hass.user.name : ""}</span
|
||||
>
|
||||
|
||||
@@ -130,11 +130,56 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
private _newTarget?: TargetItem;
|
||||
|
||||
private _getDevicesMemoized = memoizeOne(getDevices);
|
||||
private _getDevicesMemoized = memoizeOne(
|
||||
(
|
||||
hass: HomeAssistant,
|
||||
configEntryLookup: Record<string, ConfigEntry>,
|
||||
includeDomains?: string[],
|
||||
includeDeviceClasses?: string[],
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
excludeDevices?: string[],
|
||||
value?: string,
|
||||
idPrefix?: string
|
||||
) =>
|
||||
getDevices(hass, configEntryLookup, {
|
||||
includeDomains,
|
||||
includeDeviceClasses,
|
||||
deviceFilter,
|
||||
entityFilter,
|
||||
excludeDevices,
|
||||
value,
|
||||
idPrefix,
|
||||
})
|
||||
);
|
||||
|
||||
private _getLabelsMemoized = memoizeOne(getLabels);
|
||||
|
||||
private _getEntitiesMemoized = memoizeOne(getEntities);
|
||||
private _getEntitiesMemoized = memoizeOne(
|
||||
(
|
||||
hass: HomeAssistant,
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
includeDeviceClasses?: string[],
|
||||
includeUnitOfMeasurement?: string[],
|
||||
includeEntities?: string[],
|
||||
excludeEntities?: string[],
|
||||
value?: string,
|
||||
idPrefix?: string
|
||||
) =>
|
||||
getEntities(hass, {
|
||||
includeDomains,
|
||||
excludeDomains,
|
||||
entityFilter,
|
||||
includeDeviceClasses,
|
||||
includeUnitOfMeasurement,
|
||||
includeEntities,
|
||||
excludeEntities,
|
||||
value,
|
||||
idPrefix,
|
||||
})
|
||||
);
|
||||
|
||||
private _getAreasAndFloorsMemoized = memoizeOne(getAreasAndFloors);
|
||||
|
||||
@@ -919,7 +964,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
this.hass,
|
||||
configEntryLookup,
|
||||
includeDomains,
|
||||
undefined,
|
||||
includeDeviceClasses,
|
||||
deviceFilter,
|
||||
entityFilter,
|
||||
|
||||
@@ -82,7 +82,6 @@ export class HaThemePicker extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
@value-changed=${this._changed}
|
||||
popover-placement="bottom"
|
||||
></ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ export const haTopAppBarFixedStyles = css`
|
||||
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;
|
||||
|
||||
@@ -17,13 +17,16 @@ import {
|
||||
mdiWeatherSunny,
|
||||
mdiWebhook,
|
||||
} from "@mdi/js";
|
||||
import { consume } from "@lit/context";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } 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";
|
||||
|
||||
@@ -50,12 +53,24 @@ 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>`;
|
||||
@@ -65,20 +80,18 @@ export class HaTriggerIcon extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this.hass) {
|
||||
if (!this._connection || !this._config) {
|
||||
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>`;
|
||||
const icon = triggerIcon(this._connection, this._config, this.trigger).then(
|
||||
(icn) => {
|
||||
if (icn) {
|
||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||
}
|
||||
return this._renderFallback();
|
||||
}
|
||||
return this._renderFallback();
|
||||
});
|
||||
);
|
||||
|
||||
return html`${until(icon)}`;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
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";
|
||||
@@ -17,6 +19,9 @@ export class HaTraceLogbook extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public logbookEntries!: LogbookEntry[];
|
||||
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return this.logbookEntries.length
|
||||
? html`
|
||||
@@ -26,13 +31,10 @@ export class HaTraceLogbook extends LitElement {
|
||||
.entries=${this.logbookEntries}
|
||||
.narrow=${this.narrow}
|
||||
></ha-logbook-renderer>
|
||||
<hat-logbook-note
|
||||
.hass=${this.hass}
|
||||
.domain=${this.trace.domain}
|
||||
></hat-logbook-note>
|
||||
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
|
||||
`
|
||||
: html`<div class="padded-box">
|
||||
${this.hass.localize(
|
||||
${this._localize(
|
||||
"ui.panel.config.automation.trace.path.no_logbook_entries"
|
||||
)}
|
||||
</div>`;
|
||||
|
||||
@@ -374,10 +374,7 @@ export class HaTracePathDetails extends LitElement {
|
||||
.entries=${entries}
|
||||
.narrow=${this.narrow}
|
||||
></ha-logbook-renderer>
|
||||
<hat-logbook-note
|
||||
.hass=${this.hass}
|
||||
.domain=${this.trace.domain}
|
||||
></hat-logbook-note>
|
||||
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
|
||||
`
|
||||
: html`<div class="padded-box">
|
||||
${this.hass!.localize(
|
||||
|
||||
@@ -28,10 +28,7 @@ export class HaTraceTimeline extends LitElement {
|
||||
allow-pick
|
||||
>
|
||||
</hat-trace-timeline>
|
||||
<hat-logbook-note
|
||||
.hass=${this.hass}
|
||||
.domain=${this.trace.domain}
|
||||
></hat-logbook-note>
|
||||
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import { css, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
|
||||
@customElement("hat-logbook-note")
|
||||
class HatLogbookNote extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public domain: "automation" | "script" = "automation";
|
||||
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
render() {
|
||||
if (this.domain === "script") {
|
||||
return this.hass.localize(
|
||||
return this._localize(
|
||||
"ui.panel.config.automation.trace.messages.not_all_entries_are_related_script_note"
|
||||
);
|
||||
}
|
||||
return this.hass.localize(
|
||||
return this._localize(
|
||||
"ui.panel.config.automation.trace.messages.not_all_entries_are_related_automation_note"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
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 { styleMap } from "lit/directives/style-map";
|
||||
import type { BasePerson } from "../../data/person";
|
||||
import { computeUserInitials } from "../../data/user";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { connectionContext } from "../../data/context";
|
||||
|
||||
@customElement("ha-person-badge")
|
||||
class PersonBadge extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public person?: BasePerson;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
private _connection?: ContextType<typeof connectionContext>;
|
||||
|
||||
protected render() {
|
||||
if (!this.person) {
|
||||
return nothing;
|
||||
@@ -19,10 +22,10 @@ class PersonBadge extends LitElement {
|
||||
|
||||
const picture = this.person.picture;
|
||||
|
||||
if (picture) {
|
||||
if (picture && this._connection) {
|
||||
return html`<div
|
||||
style=${styleMap({
|
||||
backgroundImage: `url(${this.hass.hassUrl(picture)})`,
|
||||
backgroundImage: `url(${this._connection.hassUrl(picture)})`,
|
||||
})}
|
||||
class="picture"
|
||||
></div>`;
|
||||
|
||||
@@ -1,57 +1,62 @@
|
||||
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 type { CurrentUser, HomeAssistant } from "../../types";
|
||||
import { connectionContext, statesContext } from "../../data/context";
|
||||
import type { CurrentUser } from "../../types";
|
||||
|
||||
@customElement("ha-user-badge")
|
||||
class UserBadge extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public user?: User | CurrentUser;
|
||||
|
||||
@state() private _personPicture?: string;
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
private _connection?: ContextType<typeof connectionContext>;
|
||||
|
||||
private _personEntityId?: string;
|
||||
@state()
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states?: HassEntities;
|
||||
|
||||
public willUpdate(changedProps: PropertyValues<this>) {
|
||||
@state() private _personEntityId?: string;
|
||||
|
||||
@state()
|
||||
@consumeEntityState({ entityIdPath: ["_personEntityId"] })
|
||||
private _personState?: HassEntity;
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("user")) {
|
||||
this._getPersonPicture();
|
||||
return;
|
||||
}
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
// 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 (
|
||||
this._personEntityId &&
|
||||
oldHass &&
|
||||
this.hass.states[this._personEntityId] !==
|
||||
oldHass.states[this._personEntityId]
|
||||
changedProps.has("user") ||
|
||||
(changedProps.has("_states") &&
|
||||
(!this._personEntityId || !this._states?.[this._personEntityId]))
|
||||
) {
|
||||
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();
|
||||
this._updatePersonEntityId();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this.user) {
|
||||
if (!this.user) {
|
||||
return nothing;
|
||||
}
|
||||
const picture = this._personPicture;
|
||||
const picture =
|
||||
this._personEntityId &&
|
||||
(this._personState?.attributes.entity_picture as string | undefined);
|
||||
|
||||
if (picture) {
|
||||
if (picture && this._connection) {
|
||||
return html`<div
|
||||
style=${styleMap({
|
||||
backgroundImage: `url(${this.hass.hassUrl(picture)})`,
|
||||
backgroundImage: `url(${this._connection.hassUrl(picture)})`,
|
||||
})}
|
||||
class="picture"
|
||||
></div>`;
|
||||
@@ -64,20 +69,18 @@ class UserBadge extends LitElement {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _getPersonPicture() {
|
||||
private _updatePersonEntityId() {
|
||||
this._personEntityId = undefined;
|
||||
this._personPicture = undefined;
|
||||
if (!this.hass || !this.user) {
|
||||
if (!this.user || !this._states) {
|
||||
return;
|
||||
}
|
||||
for (const entity of Object.values(this.hass.states)) {
|
||||
for (const entity of Object.values(this._states)) {
|
||||
if (
|
||||
entity.attributes.user_id === this.user.id &&
|
||||
computeStateDomain(entity) === "person"
|
||||
) {
|
||||
this._personEntityId = entity.entity_id;
|
||||
this._personPicture = entity.attributes.entity_picture;
|
||||
break;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,11 +64,7 @@ class HaUserPicker extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-user-badge
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.user=${user}
|
||||
></ha-user-badge>
|
||||
<ha-user-badge slot="start" .user=${user}></ha-user-badge>
|
||||
<span slot="headline">${user.name}</span>
|
||||
`;
|
||||
};
|
||||
@@ -94,11 +90,7 @@ class HaUserPicker extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-combo-box-item type="button" compact>
|
||||
<ha-user-badge
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.user=${item.user}
|
||||
></ha-user-badge>
|
||||
<ha-user-badge slot="start" .user=${item.user}></ha-user-badge>
|
||||
<span slot="headline">${item.primary}</span>
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
|
||||
@@ -15,20 +15,33 @@ import {
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../entity/entity";
|
||||
import type { EntityRegistryDisplayEntry } from "../entity/entity_registry";
|
||||
|
||||
export interface GetAreasOptions {
|
||||
includeDomains?: string[];
|
||||
excludeDomains?: string[];
|
||||
includeDeviceClasses?: string[];
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||
excludeAreas?: string[];
|
||||
idPrefix?: string;
|
||||
}
|
||||
|
||||
export const getAreas = (
|
||||
haAreas: HomeAssistant["areas"],
|
||||
haFloors: HomeAssistant["floors"],
|
||||
haDevices: HomeAssistant["devices"],
|
||||
haEntities: HomeAssistant["entities"],
|
||||
haStates: HomeAssistant["states"],
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
includeDeviceClasses?: string[],
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
excludeAreas?: string[],
|
||||
idPrefix = ""
|
||||
options?: GetAreasOptions
|
||||
): PickerComboBoxItem[] => {
|
||||
const {
|
||||
includeDomains,
|
||||
excludeDomains,
|
||||
includeDeviceClasses,
|
||||
deviceFilter,
|
||||
entityFilter,
|
||||
excludeAreas,
|
||||
idPrefix = "",
|
||||
} = options ?? {};
|
||||
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
|
||||
|
||||
@@ -256,6 +256,7 @@ 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,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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,
|
||||
@@ -10,10 +11,12 @@ 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
|
||||
@@ -94,6 +97,11 @@ 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
|
||||
|
||||
/**
|
||||
@@ -162,3 +170,30 @@ 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
|
||||
|
||||
@@ -32,6 +32,17 @@ export interface DeviceAreaLabel {
|
||||
viaDeviceAreaName?: string;
|
||||
}
|
||||
|
||||
export interface GetDevicesOptions {
|
||||
includeDomains?: string[];
|
||||
excludeDomains?: string[];
|
||||
includeDeviceClasses?: string[];
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||
excludeDevices?: string[];
|
||||
value?: string;
|
||||
idPrefix?: string;
|
||||
}
|
||||
|
||||
export const computeDeviceAreaLabel = (
|
||||
device: DeviceRegistryEntry,
|
||||
areas: HomeAssistant["areas"],
|
||||
@@ -96,15 +107,19 @@ export const deviceComboBoxKeys: FuseWeightedKey[] = [
|
||||
export const getDevices = (
|
||||
hass: HomeAssistant,
|
||||
configEntryLookup: Record<string, ConfigEntry>,
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
includeDeviceClasses?: string[],
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
excludeDevices?: string[],
|
||||
value?: string,
|
||||
idPrefix = ""
|
||||
options?: GetDevicesOptions
|
||||
): DevicePickerItem[] => {
|
||||
const {
|
||||
includeDomains,
|
||||
excludeDomains,
|
||||
includeDeviceClasses,
|
||||
deviceFilter,
|
||||
entityFilter,
|
||||
excludeDevices,
|
||||
value,
|
||||
idPrefix = "",
|
||||
} = options ?? {};
|
||||
|
||||
const devices = Object.values(hass.devices);
|
||||
const entities = Object.values(hass.entities);
|
||||
|
||||
|
||||
@@ -41,18 +41,34 @@ export const entityComboBoxKeys: FuseWeightedKey[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export interface GetEntitiesOptions {
|
||||
includeDomains?: string[];
|
||||
excludeDomains?: string[];
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||
includeDeviceClasses?: string[];
|
||||
includeUnitOfMeasurement?: string[];
|
||||
includeEntities?: string[];
|
||||
excludeEntities?: string[];
|
||||
value?: string;
|
||||
idPrefix?: string;
|
||||
}
|
||||
|
||||
export const getEntities = (
|
||||
hass: HomeAssistant,
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
includeDeviceClasses?: string[],
|
||||
includeUnitOfMeasurement?: string[],
|
||||
includeEntities?: string[],
|
||||
excludeEntities?: string[],
|
||||
value?: string,
|
||||
idPrefix = ""
|
||||
options?: GetEntitiesOptions
|
||||
): EntityComboBoxItem[] => {
|
||||
const {
|
||||
includeDomains,
|
||||
excludeDomains,
|
||||
entityFilter,
|
||||
includeDeviceClasses,
|
||||
includeUnitOfMeasurement,
|
||||
includeEntities,
|
||||
excludeEntities,
|
||||
value,
|
||||
idPrefix = "",
|
||||
} = options ?? {};
|
||||
|
||||
let items: EntityComboBoxItem[];
|
||||
|
||||
let entityIds = Object.keys(hass.states);
|
||||
|
||||
@@ -36,11 +36,11 @@ export type ItemType =
|
||||
| "script_blueprint";
|
||||
|
||||
export const findRelated = (
|
||||
hass: HomeAssistant,
|
||||
hass: Pick<HomeAssistant, "callWS">,
|
||||
itemType: ItemType,
|
||||
itemId: string
|
||||
): Promise<RelatedResult> =>
|
||||
hass.callWS({
|
||||
hass.callWS<RelatedResult>({
|
||||
type: "search/related",
|
||||
item_type: itemType,
|
||||
item_id: itemId,
|
||||
|
||||
@@ -54,14 +54,12 @@ export class HaMoreInfoStateHeader extends LitElement {
|
||||
${this._absoluteTime
|
||||
? html`
|
||||
<ha-absolute-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${this.changedOverride ??
|
||||
this.stateObj.last_changed}
|
||||
></ha-absolute-time>
|
||||
`
|
||||
: html`
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${this.changedOverride ??
|
||||
this.stateObj.last_changed}
|
||||
capitalize
|
||||
|
||||
@@ -23,7 +23,6 @@ class MoreInfoAutomation extends LitElement {
|
||||
<div class="flex">
|
||||
<div>${this.hass.localize("ui.card.automation.last_triggered")}:</div>
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${this.stateObj.attributes.last_triggered}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
|
||||
@@ -36,7 +36,6 @@ class MoreInfoSun extends LitElement {
|
||||
)}</span
|
||||
>
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${item === "ris" ? risingDate : settingDate}
|
||||
></ha-relative-time>
|
||||
</div>
|
||||
|
||||
@@ -201,7 +201,6 @@ class MoreInfoWeather extends LitElement {
|
||||
<div class="time-ago">
|
||||
<ha-relative-time
|
||||
id="relative-time"
|
||||
.hass=${this.hass}
|
||||
.datetime=${this.stateObj.last_changed}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
@@ -213,7 +212,6 @@ class MoreInfoWeather extends LitElement {
|
||||
)}:
|
||||
</span>
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${this.stateObj.last_changed}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
@@ -225,7 +223,6 @@ class MoreInfoWeather extends LitElement {
|
||||
)}:
|
||||
</span>
|
||||
<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${this.stateObj.last_updated}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
|
||||
@@ -30,7 +30,6 @@ export class HuiPersistentNotificationItem extends LitElement {
|
||||
<span>
|
||||
<ha-relative-time
|
||||
id="relative-time"
|
||||
.hass=${this.hass}
|
||||
.datetime=${this.notification.created_at}
|
||||
capitalize
|
||||
></ha-relative-time>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { mdiDevices } from "@mdi/js";
|
||||
import { consume } from "@lit/context";
|
||||
import Fuse from "fuse.js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
@@ -47,7 +48,9 @@ import {
|
||||
type ActionCommandComboBoxItem,
|
||||
type NavigationComboBoxItem,
|
||||
} from "../../data/quick_bar";
|
||||
import type { RelatedResult } from "../../data/search";
|
||||
import type { RelatedIdSets } from "../../common/search/related-context";
|
||||
import { sortRelatedFirst } from "../../common/search/related-context";
|
||||
import { relatedContext } from "../../data/context";
|
||||
import {
|
||||
multiTermSortedSearch,
|
||||
type FuseWeightedKey,
|
||||
@@ -70,6 +73,10 @@ const SEPARATOR = "________";
|
||||
export class QuickBar extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state()
|
||||
@consume({ context: relatedContext, subscribe: true })
|
||||
private _relatedIdSets?: RelatedIdSets;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _loading = true;
|
||||
@@ -80,8 +87,6 @@ export class QuickBar extends LitElement {
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@state() private _relatedResult?: RelatedResult;
|
||||
|
||||
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
|
||||
|
||||
private get _showEntityId() {
|
||||
@@ -108,8 +113,6 @@ export class QuickBar extends LitElement {
|
||||
this._selectedSection = effectiveQuickBarMode(this.hass.user, params.mode);
|
||||
this._showHint = params.showHint ?? false;
|
||||
|
||||
this._relatedResult = params.contextItem ? params.related : undefined;
|
||||
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
@@ -432,7 +435,7 @@ export class QuickBar extends LitElement {
|
||||
this._selectedSection = section as QuickBarSection | undefined;
|
||||
return this._getItemsMemoized(
|
||||
this._configEntryLookup,
|
||||
this._relatedResult,
|
||||
this._relatedIdSets,
|
||||
searchString,
|
||||
this._selectedSection
|
||||
);
|
||||
@@ -441,12 +444,11 @@ export class QuickBar extends LitElement {
|
||||
private _getItemsMemoized = memoizeOne(
|
||||
(
|
||||
configEntryLookup: Record<string, ConfigEntry>,
|
||||
relatedResult: RelatedResult | undefined,
|
||||
relatedIdSets: RelatedIdSets | undefined,
|
||||
filter?: string,
|
||||
section?: QuickBarSection
|
||||
) => {
|
||||
const items: (string | PickerComboBoxItem)[] = [];
|
||||
const relatedIdSets = this._getRelatedIdSets(relatedResult);
|
||||
|
||||
if (!section || section === "navigate") {
|
||||
let navigateItems = this._generateNavigationCommandsMemoized(
|
||||
@@ -498,7 +500,7 @@ export class QuickBar extends LitElement {
|
||||
let entityItems = this._getEntitiesMemoized(this.hass);
|
||||
|
||||
// Mark related items
|
||||
if (relatedIdSets.entities.size > 0) {
|
||||
if (relatedIdSets?.entities.size) {
|
||||
entityItems = entityItems.map((item) => ({
|
||||
...item,
|
||||
isRelated: relatedIdSets.entities.has(
|
||||
@@ -508,7 +510,7 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
entityItems = this._sortRelatedFirst(
|
||||
entityItems = sortRelatedFirst(
|
||||
this._filterGroup(
|
||||
"entity",
|
||||
entityItems,
|
||||
@@ -537,7 +539,7 @@ export class QuickBar extends LitElement {
|
||||
);
|
||||
|
||||
// Mark related items
|
||||
if (relatedIdSets.devices.size > 0) {
|
||||
if (relatedIdSets?.devices.size) {
|
||||
deviceItems = deviceItems.map((item) => {
|
||||
const deviceId = item.id.split(SEPARATOR)[1];
|
||||
return {
|
||||
@@ -548,7 +550,7 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
deviceItems = this._sortRelatedFirst(
|
||||
deviceItems = sortRelatedFirst(
|
||||
this._filterGroup("device", deviceItems, filter, deviceComboBoxKeys)
|
||||
);
|
||||
} else {
|
||||
@@ -569,7 +571,7 @@ export class QuickBar extends LitElement {
|
||||
let areaItems = this._getAreasMemoized(this.hass);
|
||||
|
||||
// Mark related items
|
||||
if (relatedIdSets.areas.size > 0) {
|
||||
if (relatedIdSets?.areas.size) {
|
||||
areaItems = areaItems.map((item) => {
|
||||
const areaId = item.id.split(SEPARATOR)[1];
|
||||
return {
|
||||
@@ -580,7 +582,7 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
areaItems = this._sortRelatedFirst(
|
||||
areaItems = sortRelatedFirst(
|
||||
this._filterGroup("area", areaItems, filter, areaComboBoxKeys)
|
||||
);
|
||||
} else {
|
||||
@@ -601,41 +603,13 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _getRelatedIdSets = memoizeOne((related?: RelatedResult) => ({
|
||||
entities: new Set(related?.entity || []),
|
||||
devices: new Set(related?.device || []),
|
||||
areas: new Set(related?.area || []),
|
||||
}));
|
||||
|
||||
private _getEntitiesMemoized = memoizeOne((hass: HomeAssistant) =>
|
||||
getEntities(
|
||||
hass,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
`entity${SEPARATOR}`
|
||||
)
|
||||
getEntities(hass, { idPrefix: `entity${SEPARATOR}` })
|
||||
);
|
||||
|
||||
private _getDevicesMemoized = memoizeOne(
|
||||
(hass: HomeAssistant, configEntryLookup: Record<string, ConfigEntry>) =>
|
||||
getDevices(
|
||||
hass,
|
||||
configEntryLookup,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
`device${SEPARATOR}`
|
||||
)
|
||||
getDevices(hass, configEntryLookup, { idPrefix: `device${SEPARATOR}` })
|
||||
);
|
||||
|
||||
private _getAreasMemoized = memoizeOne((hass: HomeAssistant) =>
|
||||
@@ -645,13 +619,9 @@ export class QuickBar extends LitElement {
|
||||
hass.devices,
|
||||
hass.entities,
|
||||
hass.states,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
`area${SEPARATOR}`
|
||||
{
|
||||
idPrefix: `area${SEPARATOR}`,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@@ -705,10 +675,13 @@ export class QuickBar extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _sortBySortingLabel = (entityA, entityB) =>
|
||||
private _sortBySortingLabel = (
|
||||
entityA: PickerComboBoxItem,
|
||||
entityB: PickerComboBoxItem
|
||||
) =>
|
||||
caseInsensitiveStringCompare(
|
||||
(entityA as PickerComboBoxItem).sorting_label!,
|
||||
(entityB as PickerComboBoxItem).sorting_label!,
|
||||
entityA.sorting_label!,
|
||||
entityB.sorting_label!,
|
||||
this.hass.locale.language
|
||||
);
|
||||
|
||||
@@ -719,16 +692,6 @@ export class QuickBar extends LitElement {
|
||||
return this._sortBySortingLabel(a, b);
|
||||
});
|
||||
|
||||
private _sortRelatedFirst = (items: PickerComboBoxItem[]) =>
|
||||
[...items].sort((a, b) => {
|
||||
const aRelated = Boolean(a.isRelated);
|
||||
const bRelated = Boolean(b.isRelated);
|
||||
if (aRelated === bRelated) {
|
||||
return 0;
|
||||
}
|
||||
return aRelated ? -1 : 1;
|
||||
});
|
||||
|
||||
// #endregion data
|
||||
|
||||
// #region interaction
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { ItemType, RelatedResult } from "../../data/search";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { closeDialog } from "../make-dialog-manager";
|
||||
|
||||
@@ -10,17 +9,10 @@ export type QuickBarSection =
|
||||
| "navigate"
|
||||
| "command";
|
||||
|
||||
export interface QuickBarContextItem {
|
||||
itemType: ItemType;
|
||||
itemId: string;
|
||||
}
|
||||
|
||||
export interface QuickBarParams {
|
||||
entityFilter?: string;
|
||||
mode?: QuickBarSection;
|
||||
showHint?: boolean;
|
||||
contextItem?: QuickBarContextItem;
|
||||
related?: RelatedResult;
|
||||
}
|
||||
|
||||
/** Non-admin users cannot scope the bar to command, device, or area (those sections are admin-only). */
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ContextProvider } from "@lit/context";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -7,6 +8,7 @@ import { listenMediaQuery } from "../common/dom/media_query";
|
||||
import { toggleAttribute } from "../common/dom/toggle_attribute";
|
||||
import { computeRTLDirection } from "../common/util/compute_rtl";
|
||||
import "../components/ha-drawer";
|
||||
import { narrowViewportContext } from "../data/context";
|
||||
import { showNotificationDrawer } from "../dialogs/notifications/show-notification-drawer";
|
||||
import type { HomeAssistant, Route } from "../types";
|
||||
import "./partial-panel-resolver";
|
||||
@@ -36,6 +38,11 @@ export class HomeAssistantMain extends LitElement {
|
||||
|
||||
@state() private _drawerOpen = false;
|
||||
|
||||
private _narrowViewportProvider = new ContextProvider(this, {
|
||||
context: narrowViewportContext,
|
||||
initialValue: this.narrow,
|
||||
});
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
listenMediaQuery("(max-width: 870px)", (matches) => {
|
||||
@@ -66,7 +73,6 @@ export class HomeAssistantMain extends LitElement {
|
||||
></ha-sidebar>
|
||||
${isPanelReady
|
||||
? html`<partial-panel-resolver
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
.route=${this.route}
|
||||
slot="appContent"
|
||||
@@ -121,6 +127,10 @@ export class HomeAssistantMain extends LitElement {
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues<this>) {
|
||||
if (changedProps.has("narrow")) {
|
||||
this._narrowViewportProvider.setValue(this.narrow);
|
||||
}
|
||||
|
||||
if (changedProps.has("route") && this._sidebarNarrow) {
|
||||
this._drawerOpen = false;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
STATE_NOT_RUNNING,
|
||||
STATE_RUNNING,
|
||||
STATE_STARTING,
|
||||
} from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { deepActiveElement } from "../common/dom/deep-active-element";
|
||||
import { deepEqual } from "../common/util/deep-equal";
|
||||
import { narrowViewportContext } from "../data/context";
|
||||
import { getDefaultPanel } from "../data/panel";
|
||||
import type { CustomPanelInfo } from "../data/panel_custom";
|
||||
import type { HomeAssistant, Panels } from "../types";
|
||||
@@ -43,7 +45,9 @@ const COMPONENTS = {
|
||||
class PartialPanelResolver extends HassRouterPage {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
@state()
|
||||
@consume({ context: narrowViewportContext, subscribe: true })
|
||||
private _narrow = false;
|
||||
|
||||
private _waitForStart = false;
|
||||
|
||||
@@ -92,7 +96,7 @@ class PartialPanelResolver extends HassRouterPage {
|
||||
const el = super.createLoadingScreen();
|
||||
el.rootnav = true;
|
||||
el.hass = this.hass;
|
||||
el.narrow = this.narrow;
|
||||
el.narrow = this._narrow;
|
||||
return el;
|
||||
}
|
||||
|
||||
@@ -100,7 +104,7 @@ class PartialPanelResolver extends HassRouterPage {
|
||||
const hass = this.hass;
|
||||
|
||||
el.hass = hass;
|
||||
el.narrow = this.narrow;
|
||||
el.narrow = this._narrow;
|
||||
el.route = this.routeTail;
|
||||
el.panel = hass.panels[this._currentPage];
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { goBack } from "../../common/navigate";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
@@ -15,6 +14,7 @@ import type { Lovelace } from "../lovelace/types";
|
||||
import "../lovelace/views/hui-view";
|
||||
import "../lovelace/views/hui-view-background";
|
||||
import "../lovelace/views/hui-view-container";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
|
||||
const CLIMATE_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = {
|
||||
strategy: {
|
||||
@@ -97,38 +97,36 @@ class PanelClimate extends LitElement {
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<div class="header ${classMap({ narrow: this.narrow })}">
|
||||
<div class="toolbar">
|
||||
${this._searchParams.has("historyBack")
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._back}
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`
|
||||
<ha-menu-button
|
||||
slot="navigationIcon"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-menu-button>
|
||||
`}
|
||||
<div class="main-title">${this.hass.localize("panel.climate")}</div>
|
||||
</div>
|
||||
</div>
|
||||
${this._lovelace
|
||||
? html`
|
||||
<hui-view-container .hass=${this.hass}>
|
||||
<hui-view-background .hass=${this.hass}> </hui-view-background>
|
||||
<hui-view
|
||||
<ha-top-app-bar-fixed .narrow=${this.narrow}>
|
||||
${this._searchParams.has("historyBack")
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._back}
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`
|
||||
<ha-menu-button
|
||||
slot="navigationIcon"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
></hui-view
|
||||
></hui-view-container>
|
||||
`
|
||||
: nothing}
|
||||
></ha-menu-button>
|
||||
`}
|
||||
<div slot="title">${this.hass.localize("panel.climate")}</div>
|
||||
${this._lovelace
|
||||
? html`
|
||||
<hui-view-container .hass=${this.hass}>
|
||||
<hui-view-background .hass=${this.hass}> </hui-view-background>
|
||||
<hui-view
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
></hui-view
|
||||
></hui-view-container>
|
||||
`
|
||||
: nothing}
|
||||
</ha-top-app-bar-fixed>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -169,78 +167,11 @@ class PanelClimate extends LitElement {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
.header {
|
||||
background-color: var(--app-header-background-color);
|
||||
color: var(--app-header-text-color, white);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: calc(
|
||||
var(--ha-top-app-bar-width, 100%) - var(
|
||||
--safe-area-inset-right,
|
||||
0px
|
||||
)
|
||||
);
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
z-index: 4;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
-webkit-backdrop-filter: var(--app-header-backdrop-filter, none);
|
||||
backdrop-filter: var(--app-header-backdrop-filter, none);
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
}
|
||||
:host([narrow]) .header {
|
||||
width: calc(
|
||||
var(--ha-top-app-bar-width, 100%) - var(
|
||||
--safe-area-inset-left,
|
||||
0px
|
||||
) - var(--safe-area-inset-right, 0px)
|
||||
);
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
}
|
||||
:host([scrolled]) .header {
|
||||
box-shadow: var(
|
||||
--bar-box-shadow,
|
||||
0px 2px 4px -1px rgba(0, 0, 0, 0.2),
|
||||
0px 4px 5px 0px rgba(0, 0, 0, 0.14),
|
||||
0px 1px 10px 0px rgba(0, 0, 0, 0.12)
|
||||
);
|
||||
}
|
||||
.toolbar {
|
||||
height: var(--header-height);
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
font-size: var(--ha-font-size-xl);
|
||||
padding: 0px 12px;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
box-sizing: border-box;
|
||||
border-bottom: var(--app-header-border-bottom, none);
|
||||
}
|
||||
:host([narrow]) .toolbar {
|
||||
padding: 0 4px;
|
||||
}
|
||||
.main-title {
|
||||
margin-inline-start: var(--ha-space-6);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
flex-grow: 1;
|
||||
}
|
||||
.narrow .main-title {
|
||||
margin-inline-start: var(--ha-space-2);
|
||||
}
|
||||
hui-view-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
padding-top: calc(var(--header-height) + var(--safe-area-inset-top));
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
padding-inline-end: var(--safe-area-inset-right);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
}
|
||||
:host([narrow]) hui-view-container {
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
padding-inline-start: var(--safe-area-inset-left);
|
||||
}
|
||||
hui-view {
|
||||
flex: 1 1 100%;
|
||||
|
||||
@@ -185,23 +185,25 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
|
||||
private _renderInfoCard() {
|
||||
const systemManaged = this._isSystemManaged(this._currentAddon);
|
||||
|
||||
return html`<ha-card outlined>
|
||||
return html` <ha-card outlined>
|
||||
<div class="card-content">
|
||||
<div class="addon-header">
|
||||
${this._currentAddon.logo
|
||||
? html`
|
||||
<img
|
||||
class="logo"
|
||||
alt=""
|
||||
src="/api/hassio/addons/${this._currentAddon.slug}/logo"
|
||||
/>
|
||||
`
|
||||
: nothing}
|
||||
<div class="title">
|
||||
${getAppDisplayName(
|
||||
this._currentAddon.name,
|
||||
this._currentAddon.stage
|
||||
)}
|
||||
${this._currentAddon.logo
|
||||
? html`
|
||||
<img
|
||||
class="logo"
|
||||
alt=""
|
||||
src="/api/hassio/addons/${this._currentAddon.slug}/logo"
|
||||
/>
|
||||
`
|
||||
: nothing}
|
||||
${!this.narrow
|
||||
? getAppDisplayName(
|
||||
this._currentAddon.name,
|
||||
this._currentAddon.stage
|
||||
)
|
||||
: nothing}
|
||||
<div class="description">
|
||||
${this._currentAddon.version
|
||||
? html`
|
||||
@@ -239,17 +241,7 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
|
||||
? html`<supervisor-apps-state
|
||||
.state=${this._currentAddon.state}
|
||||
></supervisor-apps-state>`
|
||||
: html`
|
||||
<ha-progress-button
|
||||
.disabled=${!this._currentAddon.available}
|
||||
@click=${this._installClicked}
|
||||
.iconPath=${mdiApplicationImport}
|
||||
>
|
||||
${this.i18n.localize(
|
||||
"ui.panel.config.apps.dashboard.install"
|
||||
)}
|
||||
</ha-progress-button>
|
||||
`}
|
||||
: nothing}
|
||||
</div>
|
||||
|
||||
<ha-chip-set class="capabilities">
|
||||
@@ -513,7 +505,8 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
|
||||
</div>
|
||||
${(this._currentAddon.update_available && this._updateEntityId) ||
|
||||
this._computeShowWebUI ||
|
||||
this._computeShowIngressUI
|
||||
this._computeShowIngressUI ||
|
||||
!this._currentAddon.version
|
||||
? html`
|
||||
<div class="card-actions">
|
||||
${this._currentAddon.update_available && this._updateEntityId
|
||||
@@ -549,6 +542,19 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
${!this._currentAddon.version
|
||||
? html`
|
||||
<ha-progress-button
|
||||
.disabled=${!this._currentAddon.available}
|
||||
@click=${this._installClicked}
|
||||
.iconPath=${mdiApplicationImport}
|
||||
>
|
||||
${this.i18n.localize(
|
||||
"ui.panel.config.apps.dashboard.install"
|
||||
)}
|
||||
</ha-progress-button>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
@@ -1497,16 +1503,17 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
|
||||
}
|
||||
.addon-header {
|
||||
display: flex;
|
||||
padding-inline-start: var(--ha-space-2);
|
||||
padding-inline-end: initial;
|
||||
font-size: var(--ha-font-size-2xl);
|
||||
color: var(--ha-card-header-color, var(--primary-text-color));
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.addon-header .title {
|
||||
flex: 1;
|
||||
margin-inline-end: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.addon-header .title .description {
|
||||
@@ -1525,17 +1532,15 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
|
||||
color: var(--error-color);
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
.description {
|
||||
margin-bottom: var(--ha-space-4);
|
||||
}
|
||||
.description a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
img.logo {
|
||||
max-width: 100%;
|
||||
max-height: 60px;
|
||||
max-height: 40px;
|
||||
display: block;
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
ha-assist-chip {
|
||||
--md-sys-color-primary: var(--text-primary-color);
|
||||
|
||||
@@ -48,8 +48,6 @@ import type { AreaRegistryDetailDialogParams } from "./show-dialog-area-registry
|
||||
|
||||
const cropOptions: CropOptions = {
|
||||
round: false,
|
||||
type: "image/jpeg",
|
||||
quality: 0.75,
|
||||
};
|
||||
|
||||
const SENSOR_DOMAINS = ["sensor"];
|
||||
|
||||
@@ -334,13 +334,15 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
? this._renderTargets(
|
||||
target,
|
||||
actionHasTarget && !this._isNew,
|
||||
serviceTargetSpec
|
||||
serviceTargetSpec,
|
||||
type !== "device_id"
|
||||
)
|
||||
: nothing}
|
||||
${noteTooltipText
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
id="note-icon"
|
||||
tabindex="0"
|
||||
.path=${mdiCommentTextOutline}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.note.label"
|
||||
@@ -721,13 +723,14 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
(
|
||||
target?: HassServiceTarget,
|
||||
targetRequired = false,
|
||||
targetSpec?: TargetSelector["target"]
|
||||
targetSpec?: TargetSelector["target"],
|
||||
interactive = false
|
||||
) =>
|
||||
html`<ha-automation-row-targets
|
||||
.hass=${this.hass}
|
||||
.target=${target}
|
||||
.targetRequired=${targetRequired}
|
||||
.selector=${targetSpec ? { target: targetSpec } : undefined}
|
||||
.interactive=${interactive}
|
||||
></ha-automation-row-targets>`
|
||||
);
|
||||
|
||||
|
||||
@@ -30,10 +30,10 @@ import type {
|
||||
LocalizeFunc,
|
||||
LocalizeKeys,
|
||||
} from "../../../common/translations/localize";
|
||||
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import { deepEqual } from "../../../common/util/deep-equal";
|
||||
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
|
||||
import "../../../components/entity/state-badge";
|
||||
import "../../../components/ha-bottom-sheet";
|
||||
import "../../../components/ha-button";
|
||||
@@ -134,8 +134,8 @@ import {
|
||||
ADD_AUTOMATION_ELEMENT_DEVICE_TARGET_PARAM,
|
||||
ADD_AUTOMATION_ELEMENT_ENTITY_TARGET_PARAM,
|
||||
ADD_AUTOMATION_ELEMENT_QUERY_PARAM,
|
||||
getAddAutomationElementTargetFromQuery,
|
||||
PASTE_VALUE,
|
||||
getAddAutomationElementTargetFromQuery,
|
||||
} from "./show-add-automation-element-dialog";
|
||||
import { getTargetText } from "./target/get_target_text";
|
||||
|
||||
@@ -795,37 +795,33 @@ class DialogAddAutomationElement
|
||||
class="paste"
|
||||
@click=${this._paste}
|
||||
>
|
||||
<div class="shortcut-label">
|
||||
<div class="label">
|
||||
<div>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.${automationElementType}s.paste`
|
||||
)}
|
||||
</div>
|
||||
<div class="supporting-text">
|
||||
${this.hass.localize(
|
||||
// @ts-ignore
|
||||
`ui.panel.config.automation.editor.${automationElementType}s.type.${this._params.clipboardItem}.label`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
${!this._narrow
|
||||
? html`<span class="shortcut">
|
||||
<span
|
||||
>${isMac
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiAppleKeyboardCommand}
|
||||
></ha-svg-icon>`
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.ctrl"
|
||||
)}</span
|
||||
>
|
||||
<span>+</span>
|
||||
<span>V</span>
|
||||
</span>`
|
||||
: nothing}
|
||||
<div slot="headline" class="label">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.${automationElementType}s.paste`
|
||||
)}
|
||||
</div>
|
||||
<div slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
// @ts-ignore
|
||||
`ui.panel.config.automation.editor.${automationElementType}s.type.${this._params.clipboardItem}.label`
|
||||
)}
|
||||
</div>
|
||||
${!this._narrow
|
||||
? html`<span slot="end" class="shortcut">
|
||||
<span
|
||||
>${isMac
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiAppleKeyboardCommand}
|
||||
></ha-svg-icon>`
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.ctrl"
|
||||
)}</span
|
||||
>
|
||||
<span>+</span>
|
||||
<span>V</span>
|
||||
</span>`
|
||||
: nothing}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiContentPaste}
|
||||
@@ -2546,23 +2542,16 @@ class DialogAddAutomationElement
|
||||
ha-svg-icon.plus {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.shortcut-label {
|
||||
display: flex;
|
||||
gap: var(--ha-space-3);
|
||||
justify-content: space-between;
|
||||
}
|
||||
.shortcut-label .supporting-text {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: var(--ha-font-size-s);
|
||||
}
|
||||
.shortcut-label .shortcut {
|
||||
|
||||
.shortcut {
|
||||
--mdc-icon-size: var(--ha-space-3);
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
margin-right: var(--ha-space-4);
|
||||
}
|
||||
.shortcut-label .shortcut span {
|
||||
.shortcut span {
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-family: var(--ha-font-family-code);
|
||||
color: var(--ha-color-text-secondary);
|
||||
|
||||
@@ -20,6 +20,10 @@ import type {
|
||||
LocalizeKeys,
|
||||
} from "../../../../common/translations/localize";
|
||||
import { computeRTL } from "../../../../common/util/compute_rtl";
|
||||
import {
|
||||
sortRelatedFirst,
|
||||
type RelatedIdSets,
|
||||
} from "../../../../common/search/related-context";
|
||||
import "../../../../components/chips/ha-chip-set";
|
||||
import "../../../../components/chips/ha-filter-chip";
|
||||
import "../../../../components/entity/state-badge";
|
||||
@@ -40,7 +44,7 @@ import {
|
||||
} from "../../../../data/area_floor_picker";
|
||||
import { CONDITION_BUILDING_BLOCKS_GROUP } from "../../../../data/condition";
|
||||
import type { ConfigEntry } from "../../../../data/config_entries";
|
||||
import { labelsContext } from "../../../../data/context";
|
||||
import { labelsContext, relatedContext } from "../../../../data/context";
|
||||
import {
|
||||
deviceComboBoxKeys,
|
||||
getDevices,
|
||||
@@ -129,6 +133,10 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
| "condition"
|
||||
| "action";
|
||||
|
||||
@state()
|
||||
@consume({ context: relatedContext, subscribe: true })
|
||||
private _relatedIdSets?: RelatedIdSets;
|
||||
|
||||
@state() private _searchSectionTitle?: string;
|
||||
|
||||
@state() private _selectedSearchSection?: SearchSection;
|
||||
@@ -141,11 +149,19 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
|
||||
@query("lit-virtualizer") private _virtualizerElement?: LitVirtualizer;
|
||||
|
||||
private _getDevicesMemoized = memoizeOne(getDevices);
|
||||
private _getDevicesMemoized = memoizeOne(
|
||||
(
|
||||
hass: HomeAssistant,
|
||||
configEntryLookup: Record<string, ConfigEntry>,
|
||||
idPrefix: string
|
||||
) => getDevices(hass, configEntryLookup, { idPrefix })
|
||||
);
|
||||
|
||||
private _getLabelsMemoized = memoizeOne(getLabels);
|
||||
|
||||
private _getEntitiesMemoized = memoizeOne(getEntities);
|
||||
private _getEntitiesMemoized = memoizeOne(
|
||||
(hass: HomeAssistant, idPrefix: string) => getEntities(hass, { idPrefix })
|
||||
);
|
||||
|
||||
private _getAreasAndFloorsMemoized = memoizeOne(getAreasAndFloors);
|
||||
|
||||
@@ -194,7 +210,8 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
this.configEntryLookup,
|
||||
this.items,
|
||||
this.newTriggersAndConditions,
|
||||
this._selectedSearchSection
|
||||
this._selectedSearchSection,
|
||||
this._relatedIdSets
|
||||
);
|
||||
|
||||
let emptySearchTranslation: string | undefined;
|
||||
@@ -487,7 +504,8 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
configEntryLookup: Record<string, ConfigEntry>,
|
||||
automationItems: AddAutomationElementListItem[],
|
||||
newTriggersAndConditions: boolean,
|
||||
selectedSection?: SearchSection
|
||||
selectedSection?: SearchSection,
|
||||
relatedIdSets?: RelatedIdSets
|
||||
) => {
|
||||
const resultItems: (
|
||||
| string
|
||||
@@ -557,24 +575,29 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
if (!selectedSection || selectedSection === "entity") {
|
||||
let entityItems = this._getEntitiesMemoized(
|
||||
this.hass,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
`entity${TARGET_SEPARATOR}`
|
||||
);
|
||||
|
||||
if (relatedIdSets?.entities.size) {
|
||||
entityItems = entityItems.map((item) => ({
|
||||
...item,
|
||||
isRelated: relatedIdSets.entities.has(
|
||||
(item as EntityComboBoxItem).stateObj?.entity_id || ""
|
||||
),
|
||||
})) as EntityComboBoxItem[];
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
entityItems = this._filterGroup(
|
||||
"entity",
|
||||
entityItems,
|
||||
searchTerm,
|
||||
entityComboBoxKeys
|
||||
entityItems = sortRelatedFirst(
|
||||
this._filterGroup(
|
||||
"entity",
|
||||
entityItems,
|
||||
searchTerm,
|
||||
entityComboBoxKeys
|
||||
)
|
||||
) as EntityComboBoxItem[];
|
||||
} else if (relatedIdSets?.entities.size) {
|
||||
entityItems = sortRelatedFirst(entityItems) as EntityComboBoxItem[];
|
||||
}
|
||||
|
||||
if (!selectedSection && entityItems.length) {
|
||||
@@ -591,23 +614,29 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
let deviceItems = this._getDevicesMemoized(
|
||||
this.hass,
|
||||
configEntryLookup,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
`device${TARGET_SEPARATOR}`
|
||||
);
|
||||
|
||||
if (relatedIdSets?.devices.size) {
|
||||
deviceItems = deviceItems.map((item) => ({
|
||||
...item,
|
||||
isRelated: relatedIdSets.devices.has(
|
||||
item.id.split(TARGET_SEPARATOR)[1] || ""
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
deviceItems = this._filterGroup(
|
||||
"device",
|
||||
deviceItems,
|
||||
searchTerm,
|
||||
deviceComboBoxKeys
|
||||
deviceItems = sortRelatedFirst(
|
||||
this._filterGroup(
|
||||
"device",
|
||||
deviceItems,
|
||||
searchTerm,
|
||||
deviceComboBoxKeys
|
||||
)
|
||||
);
|
||||
} else if (relatedIdSets?.devices.size) {
|
||||
deviceItems = sortRelatedFirst(deviceItems);
|
||||
}
|
||||
|
||||
if (!selectedSection && deviceItems.length) {
|
||||
@@ -639,13 +668,31 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
undefined
|
||||
);
|
||||
|
||||
if (relatedIdSets?.areas.size) {
|
||||
areasAndFloors = areasAndFloors.map((item) => ({
|
||||
...item,
|
||||
isRelated:
|
||||
item.type === "area"
|
||||
? relatedIdSets.areas.has(
|
||||
item.id.split(TARGET_SEPARATOR)[1] || ""
|
||||
)
|
||||
: false,
|
||||
})) as FloorComboBoxItem[];
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
areasAndFloors = this._filterGroup(
|
||||
"area",
|
||||
areasAndFloors,
|
||||
searchTerm,
|
||||
areaFloorComboBoxKeys,
|
||||
false
|
||||
areasAndFloors = sortRelatedFirst(
|
||||
this._filterGroup(
|
||||
"area",
|
||||
areasAndFloors,
|
||||
searchTerm,
|
||||
areaFloorComboBoxKeys,
|
||||
false
|
||||
)
|
||||
) as FloorComboBoxItem[];
|
||||
} else if (relatedIdSets?.areas.size) {
|
||||
areasAndFloors = sortRelatedFirst(
|
||||
areasAndFloors
|
||||
) as FloorComboBoxItem[];
|
||||
}
|
||||
|
||||
|
||||
@@ -224,13 +224,15 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
? this._renderTargets(
|
||||
target,
|
||||
descriptionHasTarget && !this._isNew,
|
||||
conditionTargetSpec
|
||||
conditionTargetSpec,
|
||||
this.condition.condition !== "device"
|
||||
)
|
||||
: nothing}
|
||||
${this.condition.note?.trim()
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
id="note-icon"
|
||||
tabindex="0"
|
||||
.path=${mdiCommentTextOutline}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.note.label"
|
||||
@@ -573,13 +575,14 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
(
|
||||
target?: HassServiceTarget,
|
||||
targetRequired = false,
|
||||
targetSpec?: TargetSelector["target"]
|
||||
targetSpec?: TargetSelector["target"],
|
||||
interactive = false
|
||||
) =>
|
||||
html`<ha-automation-row-targets
|
||||
.hass=${this.hass}
|
||||
.target=${target}
|
||||
.targetRequired=${targetRequired}
|
||||
.selector=${targetSpec ? { target: targetSpec } : undefined}
|
||||
.interactive=${interactive}
|
||||
></ha-automation-row-targets>`
|
||||
);
|
||||
|
||||
|
||||
@@ -40,37 +40,6 @@ export class HaSunCondition extends LitElement implements ConditionElement {
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc, formType: FormType) =>
|
||||
[
|
||||
...(["between", "before"].includes(formType)
|
||||
? [
|
||||
{
|
||||
name: "before",
|
||||
type: "select",
|
||||
default: BEFORE_DEFAULT,
|
||||
options: [
|
||||
[
|
||||
"sunrise",
|
||||
localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.sun.sunrise"
|
||||
),
|
||||
],
|
||||
[
|
||||
"sunset",
|
||||
localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.sun.sunset"
|
||||
),
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "before_offset",
|
||||
selector: {
|
||||
duration: {
|
||||
allow_negative: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(["between", "after"].includes(formType)
|
||||
? [
|
||||
{
|
||||
@@ -102,6 +71,37 @@ export class HaSunCondition extends LitElement implements ConditionElement {
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(["between", "before"].includes(formType)
|
||||
? [
|
||||
{
|
||||
name: "before",
|
||||
type: "select",
|
||||
default: BEFORE_DEFAULT,
|
||||
options: [
|
||||
[
|
||||
"sunrise",
|
||||
localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.sun.sunrise"
|
||||
),
|
||||
],
|
||||
[
|
||||
"sunset",
|
||||
localize(
|
||||
"ui.panel.config.automation.editor.conditions.type.sun.sunset"
|
||||
),
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "before_offset",
|
||||
selector: {
|
||||
duration: {
|
||||
allow_negative: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
: []),
|
||||
] as const
|
||||
);
|
||||
|
||||
|
||||
@@ -116,10 +116,8 @@ import { showCategoryRegistryDetailDialog } from "../category/show-dialog-catego
|
||||
import {
|
||||
getAreaTableColumn,
|
||||
getCategoryTableColumn,
|
||||
getCreatedAtTableColumn,
|
||||
getEntityIdHiddenTableColumn,
|
||||
getLabelsTableColumn,
|
||||
getModifiedAtTableColumn,
|
||||
getTriggeredAtTableColumn,
|
||||
} from "../common/data-table-columns";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
@@ -141,8 +139,6 @@ type AutomationItem = AutomationEntity & {
|
||||
labels: string[]; // search only
|
||||
assistants: string[];
|
||||
assistants_sortable_key: string | undefined;
|
||||
created_at: number | undefined;
|
||||
modified_at: number | undefined;
|
||||
};
|
||||
|
||||
@customElement("ha-automation-picker")
|
||||
@@ -289,8 +285,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
labels: label_entries.map((lbl) => lbl.name),
|
||||
assistants,
|
||||
assistants_sortable_key: getAssistantsSortableKey(assistants),
|
||||
created_at: entityRegEntry?.created_at,
|
||||
modified_at: entityRegEntry?.modified_at,
|
||||
selectable: entityRegEntry !== undefined,
|
||||
};
|
||||
});
|
||||
@@ -341,8 +335,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
category: getCategoryTableColumn(localize),
|
||||
labels: getLabelsTableColumn(),
|
||||
last_triggered: getTriggeredAtTableColumn(localize, this.hass),
|
||||
created_at: getCreatedAtTableColumn(localize, this.hass),
|
||||
modified_at: getModifiedAtTableColumn(localize, this.hass),
|
||||
formatted_state: {
|
||||
minWidth: "82px",
|
||||
maxWidth: "82px",
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { CSSResult, LitElement, TemplateResult } from "lit";
|
||||
import type {
|
||||
CSSResult,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { css, html } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { transform } from "../../../common/decorators/transform";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { goBack, navigate } from "../../../common/navigate";
|
||||
import { afterNextRender } from "../../../common/util/render-status";
|
||||
import "../../../components/animation/ha-fade-in";
|
||||
@@ -136,6 +142,21 @@ export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
|
||||
value: PromiseLike<EntityRegistryEntry> | EntityRegistryEntry
|
||||
) => void;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("registryEntry")) {
|
||||
const areaId = this.registryEntry?.area_id;
|
||||
if (areaId) {
|
||||
fireEvent(this, "hass-related-context", {
|
||||
itemType: "area",
|
||||
itemId: areaId,
|
||||
});
|
||||
} else {
|
||||
fireEvent(this, "hass-related-context", undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected renderLoading(): TemplateResult {
|
||||
return html`
|
||||
<ha-fade-in .delay=${500}>
|
||||
|
||||
@@ -161,6 +161,7 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
id="note-icon"
|
||||
tabindex="0"
|
||||
.path=${mdiCommentTextOutline}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.note.label"
|
||||
|
||||
@@ -60,6 +60,9 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public selector?: TargetSelector;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public interactive = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
@@ -89,7 +92,12 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states!: ContextType<typeof statesContext>;
|
||||
|
||||
private _countCache = new Map<string, Promise<number | undefined>>();
|
||||
private _countCache = new Map<
|
||||
string,
|
||||
Promise<number | undefined> | number | undefined
|
||||
>();
|
||||
|
||||
private _rerenderCount = true;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
@@ -98,10 +106,15 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
changedProps.has("selector") ||
|
||||
changedProps.has("_registries")
|
||||
) {
|
||||
this._countCache.clear();
|
||||
this._rerenderCount = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
this._rerenderCount = false;
|
||||
}
|
||||
|
||||
private _countMatchingEntities(referencedEntities: string[]): number {
|
||||
const targetSelector = this.selector;
|
||||
const hasEntityFilter = !!targetSelector?.target?.entity;
|
||||
@@ -148,7 +161,11 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
targetId: string
|
||||
) {
|
||||
const key = `${targetType}:${targetId}`;
|
||||
if (!this._countCache.has(key)) {
|
||||
let fallback = " (-)";
|
||||
if (!this._countCache.has(key) || this._rerenderCount) {
|
||||
if (typeof this._countCache.get(key) === "number") {
|
||||
fallback = ` (${this._countCache.get(key)})`;
|
||||
}
|
||||
this._countCache.set(
|
||||
key,
|
||||
extractFromTarget(
|
||||
@@ -162,15 +179,30 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
.then((result) =>
|
||||
this._countMatchingEntities(result.referenced_entities)
|
||||
)
|
||||
.catch(() => undefined)
|
||||
.catch((err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error counting target entities", err);
|
||||
return undefined;
|
||||
})
|
||||
);
|
||||
}
|
||||
return until(
|
||||
this._countCache
|
||||
.get(key)!
|
||||
.then((count) => (count === undefined ? nothing : html` (${count})`)),
|
||||
"(-)"
|
||||
);
|
||||
|
||||
if (this._countCache.get(key) instanceof Promise) {
|
||||
return until(
|
||||
(this._countCache.get(key) as Promise<number | undefined>)!.then(
|
||||
(count) => {
|
||||
this._countCache.set(key, count);
|
||||
return count === undefined ? nothing : html` (${count})`;
|
||||
}
|
||||
),
|
||||
fallback
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof this._countCache.get(key) === "number") {
|
||||
return ` (${this._countCache.get(key)})`;
|
||||
}
|
||||
return nothing;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -249,8 +281,9 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
<ha-dropdown
|
||||
@wa-select=${this._handleTargetSelect}
|
||||
@click=${stopPropagation}
|
||||
@keydown=${stopPropagation}
|
||||
>
|
||||
<span slot="trigger" class="target interactive">
|
||||
<button slot="trigger" class="target">
|
||||
<ha-svg-icon .path=${mdiFormatListBulleted}></ha-svg-icon>
|
||||
<div class="label">
|
||||
${this._i18n.localize(
|
||||
@@ -261,7 +294,7 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
<ha-svg-icon .path=${mdiMenuDown}></ha-svg-icon>
|
||||
</span>
|
||||
</button>
|
||||
${rows.map(([targetType, targetId]) => {
|
||||
const content = html`${lastTargetType !== null &&
|
||||
lastTargetType !== targetType
|
||||
@@ -316,21 +349,37 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
targetType?: string,
|
||||
countTemplate: unknown = nothing
|
||||
) {
|
||||
return html`<div
|
||||
if (!this.interactive || !targetId || !targetType) {
|
||||
return html`<div
|
||||
class=${classMap({
|
||||
target: true,
|
||||
warning,
|
||||
error,
|
||||
})}
|
||||
.targetId=${targetId}
|
||||
.targetType=${targetType}
|
||||
.label=${label}
|
||||
>
|
||||
${icon}
|
||||
<div class="label">${label}${countTemplate}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return html`<button
|
||||
class=${classMap({
|
||||
target: true,
|
||||
warning,
|
||||
error,
|
||||
interactive: targetId && targetType,
|
||||
})}
|
||||
.targetId=${targetId}
|
||||
.targetType=${targetType}
|
||||
.label=${label}
|
||||
@click=${this._handleTargetClick}
|
||||
@keydown=${this._handleTargetKeydown}
|
||||
>
|
||||
${icon}
|
||||
<div class="label">${label}${countTemplate}</div>
|
||||
</div>`;
|
||||
</button>`;
|
||||
}
|
||||
|
||||
private _renderTarget(
|
||||
@@ -384,7 +433,7 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
targetId,
|
||||
this._getLabel
|
||||
);
|
||||
if (targetType !== "entity") {
|
||||
if (targetType !== "entity" && this.interactive) {
|
||||
countTemplate = this._renderCount(targetType, targetId);
|
||||
}
|
||||
}
|
||||
@@ -444,6 +493,13 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
this._showTargetInfo(target.targetId, target.targetType, target.label, ev);
|
||||
}
|
||||
|
||||
private _handleTargetKeydown(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter" || ev.key === " ") {
|
||||
ev.preventDefault();
|
||||
this._handleTargetClick(ev);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleTargetSelect(
|
||||
ev: HaDropdownSelectEvent<{
|
||||
targetId?: string;
|
||||
@@ -533,10 +589,10 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.target.interactive {
|
||||
button.target {
|
||||
cursor: pointer;
|
||||
}
|
||||
.target.interactive:hover {
|
||||
button.target:hover {
|
||||
background: var(--ha-color-fill-neutral-normal-hover);
|
||||
}
|
||||
|
||||
|
||||
@@ -249,13 +249,15 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
? this._renderTargets(
|
||||
target,
|
||||
descriptionHasTarget && !this._isNew,
|
||||
triggerTargetSpec
|
||||
triggerTargetSpec,
|
||||
type !== "device"
|
||||
)
|
||||
: nothing}
|
||||
${type !== "list" &&
|
||||
(this.trigger as Exclude<Trigger, TriggerList>).note?.trim()
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
tabindex="0"
|
||||
id="note-icon"
|
||||
.path=${mdiCommentTextOutline}
|
||||
.label=${this.hass.localize(
|
||||
@@ -557,13 +559,14 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
(
|
||||
target?: HassServiceTarget,
|
||||
targetRequired = false,
|
||||
targetSpec?: TargetSelector["target"]
|
||||
targetSpec?: TargetSelector["target"],
|
||||
interactive = false
|
||||
) =>
|
||||
html`<ha-automation-row-targets
|
||||
.hass=${this.hass}
|
||||
.target=${target}
|
||||
.targetRequired=${targetRequired}
|
||||
.selector=${targetSpec ? { target: targetSpec } : undefined}
|
||||
.interactive=${interactive}
|
||||
></ha-automation-row-targets>`
|
||||
);
|
||||
|
||||
|
||||
@@ -217,7 +217,7 @@ export class CloudRegister extends LitElement {
|
||||
|
||||
try {
|
||||
await cloudRegister(this.hass, email, password);
|
||||
this._verificationEmailSent(email);
|
||||
this._verificationEmailSent(email, "account_created");
|
||||
} catch (err: any) {
|
||||
this._password = "";
|
||||
this._requestInProgress = false;
|
||||
@@ -238,15 +238,18 @@ export class CloudRegister extends LitElement {
|
||||
|
||||
const email = emailField.value || "";
|
||||
|
||||
this._requestInProgress = true;
|
||||
|
||||
const doResend = async (username: string) => {
|
||||
try {
|
||||
await cloudResendVerification(this.hass, username);
|
||||
this._verificationEmailSent(username);
|
||||
this._verificationEmailSent(username, "verification_email_sent");
|
||||
} catch (err: any) {
|
||||
const errCode = err && err.body && err.body.code;
|
||||
if (errCode === "usernotfound" && username !== username.toLowerCase()) {
|
||||
await doResend(username.toLowerCase());
|
||||
} else {
|
||||
this._requestInProgress = false;
|
||||
this._error =
|
||||
err && err.body && err.body.message
|
||||
? err.body.message
|
||||
@@ -258,13 +261,16 @@ export class CloudRegister extends LitElement {
|
||||
await doResend(email);
|
||||
}
|
||||
|
||||
private _verificationEmailSent(email: string) {
|
||||
private _verificationEmailSent(
|
||||
email: string,
|
||||
messageKey: "account_created" | "verification_email_sent"
|
||||
) {
|
||||
this._requestInProgress = false;
|
||||
this._password = "";
|
||||
fireEvent(this, "cloud-email-changed", { value: email });
|
||||
fireEvent(this, "cloud-done", {
|
||||
flashMessage: this.hass.localize(
|
||||
"ui.panel.config.cloud.register.account_created"
|
||||
`ui.panel.config.cloud.register.${messageKey}`
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -299,9 +299,10 @@ class DialogNewDashboard extends LitElement implements HassDialog {
|
||||
const options: IFuseOptions<CustomStrategyEntry> = {
|
||||
keys: ["type", "name", "description"],
|
||||
isCaseSensitive: false,
|
||||
threshold: 0.3,
|
||||
ignoreLocation: true,
|
||||
minMatchCharLength: Math.min(filter.length, 2),
|
||||
threshold: 0.3,
|
||||
ignoreDiacritics: true,
|
||||
ignoreLocation: true,
|
||||
};
|
||||
const fuse = new Fuse(strategies, options);
|
||||
return fuse.search(filter).map((result) => result.item);
|
||||
|
||||
@@ -379,7 +379,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
if (changedProps.has("deviceId")) {
|
||||
this._findRelated();
|
||||
// Broadcast device context for quick bar
|
||||
fireEvent(this, "hass-quick-bar-context", {
|
||||
fireEvent(this, "hass-related-context", {
|
||||
itemType: "device",
|
||||
itemId: this.deviceId,
|
||||
});
|
||||
|
||||
@@ -142,26 +142,6 @@ export class DialogEnergyBatterySettings
|
||||
>
|
||||
${this._error ? html`<p class="error">${this._error}</p>` : nothing}
|
||||
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.helpMissingEntityUrl=${energyStatisticHelpUrl}
|
||||
.includeUnitClass=${energyUnitClasses}
|
||||
.value=${this._source.stat_energy_to}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.battery.dialog.energy_into_battery"
|
||||
)}
|
||||
.excludeStatistics=${[
|
||||
...(this._excludeList || []),
|
||||
this._source.stat_energy_from,
|
||||
]}
|
||||
@value-changed=${this._statisticToChanged}
|
||||
.helper=${this.hass.localize(
|
||||
"ui.panel.config.energy.battery.dialog.energy_helper_into",
|
||||
{ unit: this._energy_units?.join(", ") || "" }
|
||||
)}
|
||||
autofocus
|
||||
></ha-statistic-picker>
|
||||
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.helpMissingEntityUrl=${energyStatisticHelpUrl}
|
||||
@@ -179,6 +159,26 @@ export class DialogEnergyBatterySettings
|
||||
"ui.panel.config.energy.battery.dialog.energy_helper_out",
|
||||
{ unit: this._energy_units?.join(", ") || "" }
|
||||
)}
|
||||
autofocus
|
||||
></ha-statistic-picker>
|
||||
|
||||
<ha-statistic-picker
|
||||
.hass=${this.hass}
|
||||
.helpMissingEntityUrl=${energyStatisticHelpUrl}
|
||||
.includeUnitClass=${energyUnitClasses}
|
||||
.value=${this._source.stat_energy_to}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.energy.battery.dialog.energy_into_battery"
|
||||
)}
|
||||
.excludeStatistics=${[
|
||||
...(this._excludeList || []),
|
||||
this._source.stat_energy_from,
|
||||
]}
|
||||
@value-changed=${this._statisticToChanged}
|
||||
.helper=${this.hass.localize(
|
||||
"ui.panel.config.energy.battery.dialog.energy_helper_into",
|
||||
{ unit: this._energy_units?.join(", ") || "" }
|
||||
)}
|
||||
></ha-statistic-picker>
|
||||
|
||||
<ha-input
|
||||
|
||||
@@ -774,7 +774,11 @@ export class HaConfigEntities extends LitElement {
|
||||
.checked=${selected}
|
||||
.indeterminate=${partial}
|
||||
></ha-checkbox>
|
||||
<ha-label .color=${label.color} .description=${label.description}>
|
||||
<ha-label
|
||||
.color=${label.color}
|
||||
.description=${label.description}
|
||||
class="text-ellipsis"
|
||||
>
|
||||
${label.icon
|
||||
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
|
||||
: nothing}
|
||||
@@ -1674,10 +1678,14 @@ ${rejected
|
||||
ha-assist-chip {
|
||||
--ha-assist-chip-container-shape: 10px;
|
||||
}
|
||||
ha-dropdown::part(menu),
|
||||
ha-dropdown::part(submenu) {
|
||||
ha-dropdown::part(menu) {
|
||||
--auto-size-available-width: calc(50vw - var(--ha-space-4));
|
||||
}
|
||||
ha-dropdown-item::part(submenu) {
|
||||
max-width: calc(60vw - var(--ha-space-8));
|
||||
max-height: calc(100vh - var(--ha-space-8));
|
||||
overflow-y: auto;
|
||||
}
|
||||
ha-dropdown ha-assist-chip {
|
||||
--md-assist-chip-trailing-space: 8px;
|
||||
}
|
||||
|
||||
@@ -173,7 +173,6 @@ export class BluetoothAdvertisementMonitorPanel extends LitElement {
|
||||
defaultHidden: false,
|
||||
template: (ad) =>
|
||||
html`<ha-relative-time
|
||||
.hass=${this.hass}
|
||||
.datetime=${ad.datetime}
|
||||
capitalize
|
||||
></ha-relative-time>`,
|
||||
|
||||
@@ -330,42 +330,48 @@ export class BluetoothNetworkVisualization extends LitElement {
|
||||
return rssi > -33 ? 3 : rssi > -66 ? 2 : 1;
|
||||
}
|
||||
|
||||
private _tooltipFormatter = (params: TopLevelFormatterParams): string => {
|
||||
private _tooltipFormatter = (params: TopLevelFormatterParams) => {
|
||||
const { dataType, data } = params as CallbackDataParams;
|
||||
let tooltipText = "";
|
||||
if (dataType === "edge") {
|
||||
const { source, target, value } = data as any;
|
||||
const sourceName = this._getBluetoothDeviceName(source);
|
||||
const targetName = this._getBluetoothDeviceName(target);
|
||||
tooltipText = `${sourceName} → ${targetName}`;
|
||||
if (source !== CORE_SOURCE_ID) {
|
||||
tooltipText += ` <b>${this.hass.localize("ui.panel.config.bluetooth.rssi")}:</b> ${value}`;
|
||||
}
|
||||
} else {
|
||||
const { id: address } = data as any;
|
||||
const name = this._getBluetoothDeviceName(address);
|
||||
const btDevice = this._data.find((d) => d.address === address);
|
||||
if (btDevice) {
|
||||
tooltipText = `<b>${name}</b><br><b>${this.hass.localize("ui.panel.config.bluetooth.address")}:</b> ${address}<br><b>${this.hass.localize("ui.panel.config.bluetooth.rssi")}:</b> ${btDevice.rssi}<br><b>${this.hass.localize("ui.panel.config.bluetooth.source")}:</b> ${btDevice.source}<br><b>${this.hass.localize("ui.panel.config.bluetooth.updated")}:</b> ${relativeTime(new Date(btDevice.time * 1000), this.hass.locale)}`;
|
||||
const device = this._sourceDevices[address];
|
||||
if (device) {
|
||||
const area = getDeviceArea(device, this.hass.areas);
|
||||
if (area) {
|
||||
tooltipText += `<br><b>${this.hass.localize("ui.panel.config.bluetooth.area")}: </b>${area.name}`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const device = this._sourceDevices[address];
|
||||
if (device) {
|
||||
tooltipText = `<b>${name}</b><br><b>${this.hass.localize("ui.panel.config.bluetooth.address")}:</b> ${address}`;
|
||||
const area = getDeviceArea(device, this.hass.areas);
|
||||
if (area) {
|
||||
tooltipText += `<br><b>${this.hass.localize("ui.panel.config.bluetooth.area")}: </b>${area.name}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
return html`${sourceName} →
|
||||
${targetName}${source !== CORE_SOURCE_ID
|
||||
? html` <b>${this.hass.localize("ui.panel.config.bluetooth.rssi")}:</b>
|
||||
${value}`
|
||||
: nothing}`;
|
||||
}
|
||||
return tooltipText;
|
||||
const { id: address } = data as any;
|
||||
const name = this._getBluetoothDeviceName(address);
|
||||
const btDevice = this._data.find((d) => d.address === address);
|
||||
const device = this._sourceDevices[address];
|
||||
const area = device ? getDeviceArea(device, this.hass.areas) : undefined;
|
||||
const areaLine = area
|
||||
? html`<br /><b
|
||||
>${this.hass.localize("ui.panel.config.bluetooth.area")}: </b
|
||||
>${area.name}`
|
||||
: nothing;
|
||||
if (btDevice) {
|
||||
return html`<b>${name}</b><br />
|
||||
<b>${this.hass.localize("ui.panel.config.bluetooth.address")}:</b>
|
||||
${address}<br />
|
||||
<b>${this.hass.localize("ui.panel.config.bluetooth.rssi")}:</b>
|
||||
${btDevice.rssi}<br />
|
||||
<b>${this.hass.localize("ui.panel.config.bluetooth.source")}:</b>
|
||||
${btDevice.source}<br />
|
||||
<b>${this.hass.localize("ui.panel.config.bluetooth.updated")}:</b>
|
||||
${relativeTime(
|
||||
new Date(btDevice.time * 1000),
|
||||
this.hass.locale
|
||||
)}${areaLine}`;
|
||||
}
|
||||
if (device) {
|
||||
return html`<b>${name}</b><br />
|
||||
<b>${this.hass.localize("ui.panel.config.bluetooth.address")}:</b>
|
||||
${address}${areaLine}`;
|
||||
}
|
||||
return nothing;
|
||||
};
|
||||
|
||||
private _handleChartClick(e: CustomEvent): void {
|
||||
|
||||
@@ -131,7 +131,7 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
this._searchFilter = (ev.target as HaInputSearch).value ?? "";
|
||||
}
|
||||
|
||||
private _tooltipFormatter = (params: TopLevelFormatterParams): string => {
|
||||
private _tooltipFormatter = (params: TopLevelFormatterParams) => {
|
||||
const { dataType, data, name } = params as CallbackDataParams;
|
||||
if (dataType === "edge") {
|
||||
const { source, target, value } = data as any;
|
||||
@@ -141,40 +141,45 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
const sourceName = this._networkData.nodes.find(
|
||||
(node) => node.id === source
|
||||
)!.name;
|
||||
const tooltipText = `${sourceName} → ${targetName}${value ? ` <b>LQI:</b> ${value}` : ""}`;
|
||||
|
||||
const reverseValue = this._networkData.links.find(
|
||||
(link) => link.source === source && link.target === target
|
||||
)?.reverseValue;
|
||||
if (reverseValue) {
|
||||
return `${tooltipText}<br>${targetName} → ${sourceName} <b>LQI:</b> ${reverseValue}`;
|
||||
}
|
||||
return tooltipText;
|
||||
return html`${sourceName} →
|
||||
${targetName}${value
|
||||
? html` <b>LQI:</b> ${value}`
|
||||
: nothing}${reverseValue
|
||||
? html`<br />${targetName} → ${sourceName} <b>LQI:</b> ${reverseValue}`
|
||||
: nothing}`;
|
||||
}
|
||||
const device = this._devices.find((d) => d.ieee === (data as any).id);
|
||||
if (!device) {
|
||||
return name;
|
||||
}
|
||||
let label = `<b>IEEE: </b>${device.ieee}`;
|
||||
label += `<br><b>${this.hass.localize("ui.panel.config.zha.visualization.device_type")}: </b>${device.device_type.replace("_", " ")}`;
|
||||
if (device.nwk != null) {
|
||||
label += `<br><b>NWK: </b>${formatAsPaddedHex(device.nwk)}`;
|
||||
}
|
||||
if (device.manufacturer != null && device.model != null) {
|
||||
label += `<br><b>${this.hass.localize("ui.panel.config.zha.visualization.device")}: </b>${device.manufacturer} ${device.model}`;
|
||||
} else {
|
||||
label += `<br><b>${this.hass.localize("ui.panel.config.zha.visualization.device_not_in_db")}</b>`;
|
||||
return html`${name}`;
|
||||
}
|
||||
const haDevice = this.hass.devices[device.device_reg_id] as
|
||||
| DeviceRegistryEntry
|
||||
| undefined;
|
||||
if (haDevice) {
|
||||
const area = getDeviceArea(haDevice, this.hass.areas);
|
||||
if (area) {
|
||||
label += `<br><b>${this.hass.localize("ui.panel.config.zha.visualization.area")}: </b>${area.name}`;
|
||||
}
|
||||
}
|
||||
return label;
|
||||
const area = haDevice
|
||||
? getDeviceArea(haDevice, this.hass.areas)
|
||||
: undefined;
|
||||
return html`<b>IEEE: </b>${device.ieee}<br /><b
|
||||
>${this.hass.localize("ui.panel.config.zha.visualization.device_type")}: </b
|
||||
>${device.device_type.replace("_", " ")}${device.nwk != null
|
||||
? html`<br /><b>NWK: </b>${formatAsPaddedHex(device.nwk)}`
|
||||
: nothing}${device.manufacturer != null && device.model != null
|
||||
? html`<br /><b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.visualization.device"
|
||||
)}: </b
|
||||
>${device.manufacturer} ${device.model}`
|
||||
: html`<br /><b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.visualization.device_not_in_db"
|
||||
)}</b
|
||||
>`}${area
|
||||
? html`<br /><b
|
||||
>${this.hass.localize("ui.panel.config.zha.visualization.area")}: </b
|
||||
>${area.name}`
|
||||
: nothing}`;
|
||||
};
|
||||
|
||||
private async _refreshTopology(): Promise<void> {
|
||||
|
||||
@@ -9,6 +9,7 @@ import { getDeviceArea } from "../../../../../common/entity/context/get_device_c
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import { debounce } from "../../../../../common/util/debounce";
|
||||
import "../../../../../components/chart/ha-network-graph";
|
||||
import "../../../../../components/chart/ha-chart-tooltip-marker";
|
||||
import type {
|
||||
NetworkData,
|
||||
NetworkLink,
|
||||
@@ -150,7 +151,7 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
|
||||
this._searchFilter = (ev.target as HaInputSearch).value ?? "";
|
||||
}
|
||||
|
||||
private _tooltipFormatter = (params: TopLevelFormatterParams): string => {
|
||||
private _tooltipFormatter = (params: TopLevelFormatterParams) => {
|
||||
const { dataType, data } = params as CallbackDataParams;
|
||||
if (dataType === "edge") {
|
||||
const { source, target, value } = data as any;
|
||||
@@ -160,39 +161,66 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
|
||||
sourceDevice?.name_by_user ?? sourceDevice?.name ?? source;
|
||||
const targetName =
|
||||
targetDevice?.name_by_user ?? targetDevice?.name ?? target;
|
||||
let tip = `${sourceName} → ${targetName}`;
|
||||
const route =
|
||||
this._nodeStatistics[source]?.lwr || this._nodeStatistics[source]?.nlwr;
|
||||
if (route?.protocol_data_rate) {
|
||||
tip += `<br><b>${this.hass.localize("ui.panel.config.zwave_js.visualization.data_rate")}:</b> ${this.hass.localize(`ui.panel.config.zwave_js.protocol_data_rate.${route.protocol_data_rate}`)}`;
|
||||
}
|
||||
if (value) {
|
||||
tip += `<br><b>RSSI:</b> ${value}`;
|
||||
}
|
||||
return tip;
|
||||
return html`${sourceName} →
|
||||
${targetName}${route?.protocol_data_rate
|
||||
? html`<br /><b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.visualization.data_rate"
|
||||
)}:</b
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.zwave_js.protocol_data_rate.${route.protocol_data_rate}` as any
|
||||
)}`
|
||||
: nothing}${value ? html`<br /><b>RSSI:</b> ${value}` : nothing}`;
|
||||
}
|
||||
const { id, name } = data as any;
|
||||
const device = this._devices[id] as DeviceRegistryEntry | undefined;
|
||||
const nodeStatus = this._nodeStatuses[id];
|
||||
let tip = `${(params as any).marker} ${name}`;
|
||||
tip += `<br><b>${this.hass.localize("ui.panel.config.zwave_js.visualization.node_id")}:</b> ${id}`;
|
||||
if (device) {
|
||||
tip += `<br><b>${this.hass.localize("ui.panel.config.zwave_js.visualization.manufacturer")}:</b> ${device.manufacturer || "-"}`;
|
||||
tip += `<br><b>${this.hass.localize("ui.panel.config.zwave_js.visualization.model")}:</b> ${device.model || "-"}`;
|
||||
}
|
||||
if (nodeStatus) {
|
||||
tip += `<br><b>${this.hass.localize("ui.panel.config.zwave_js.visualization.status")}:</b> ${this.hass.localize(`ui.panel.config.zwave_js.node_status.${nodeStatus.status}`)}`;
|
||||
if (nodeStatus.zwave_plus_version) {
|
||||
tip += `<br><b>Z-Wave Plus:</b> ${this.hass.localize("ui.panel.config.zwave_js.visualization.version")} ${nodeStatus.zwave_plus_version}`;
|
||||
}
|
||||
}
|
||||
if (device) {
|
||||
const area = getDeviceArea(device, this.hass.areas);
|
||||
if (area) {
|
||||
tip += `<br><b>${this.hass.localize("ui.panel.config.zwave_js.visualization.area")}:</b> ${area.name}`;
|
||||
}
|
||||
}
|
||||
return tip;
|
||||
const area = device ? getDeviceArea(device, this.hass.areas) : undefined;
|
||||
return html`<ha-chart-tooltip-marker
|
||||
.color=${String((params as CallbackDataParams).color ?? "")}
|
||||
></ha-chart-tooltip-marker>
|
||||
${name}<br /><b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.visualization.node_id"
|
||||
)}:</b
|
||||
>
|
||||
${id}${device
|
||||
? html`<br /><b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.visualization.manufacturer"
|
||||
)}:</b
|
||||
>
|
||||
${device.manufacturer || "-"}<br /><b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.visualization.model"
|
||||
)}:</b
|
||||
>
|
||||
${device.model || "-"}`
|
||||
: nothing}${nodeStatus
|
||||
? html`<br /><b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.visualization.status"
|
||||
)}:</b
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.zwave_js.node_status.${nodeStatus.status}` as any
|
||||
)}${nodeStatus.zwave_plus_version
|
||||
? html`<br /><b>Z-Wave Plus:</b> ${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.visualization.version"
|
||||
)}
|
||||
${nodeStatus.zwave_plus_version}`
|
||||
: nothing}`
|
||||
: nothing}${area
|
||||
? html`<br /><b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.visualization.area"
|
||||
)}:</b
|
||||
>
|
||||
${area.name}`
|
||||
: nothing}`;
|
||||
};
|
||||
|
||||
private _getNetworkData = memoizeOne(
|
||||
|
||||
@@ -102,7 +102,6 @@ export class HaConfigPerson extends LitElement {
|
||||
.entry=${entry}
|
||||
>
|
||||
<ha-person-badge
|
||||
.hass=${this.hass}
|
||||
.person=${entry}
|
||||
slot="graphic"
|
||||
></ha-person-badge>
|
||||
@@ -139,7 +138,6 @@ export class HaConfigPerson extends LitElement {
|
||||
(entry) => html`
|
||||
<ha-list-item graphic="avatar">
|
||||
<ha-person-badge
|
||||
.hass=${this.hass}
|
||||
.person=${entry}
|
||||
slot="graphic"
|
||||
></ha-person-badge>
|
||||
|
||||