Compare commits

..

38 Commits

Author SHA1 Message Date
Bram Kragten ba26e9f491 Bumped version to 20260527.4 2026-06-03 12:03:26 +02:00
Paul Bottein 8778fe8577 Restore search field autofocus in card and badge pickers (#52387) 2026-06-03 12:03:12 +02:00
Aidan Timson 6801aaea30 Fix automation building block action icon style (#52382) 2026-06-03 12:03:12 +02:00
Wendelin c3f5b6693a Landingpage download progress (#52359)
* Simplify and improve landingpage

* add core download progress

* reduce to 2 seconds

* Use round to display full integer as progress percentage

* Use find to get the job object

* Don't show progress label when progress is at 0

Before download starts, progress is at 0. At this point we may trying
to reach a server (and error out), so we aren't really in downloading
phase just yet. Simply treat 0 as "not started" and hide the progress
label until we have a real progress value.

---------

Co-authored-by: Stefan Agner <stefan@agner.ch>
2026-06-03 12:03:10 +02:00
Bram Kragten 68f75c82eb Bumped version to 20260527.3 2026-06-02 23:55:02 +02:00
Bram Kragten 6660e4799c Add tags in app store too, plus show if addon is installed already (#52373) 2026-06-02 23:54:24 +02:00
Petar Petrov 08bfafea21 Fix raw div tag showing in Sankey chart tooltips (#52365)
Fix raw div tag showing in sankey chart tooltips
2026-06-02 23:54:23 +02:00
Bram Kragten 5677e60fcc Matter add device: change how main entity is found (#52361)
Don't search for a entity based on main entity but use entity_category
2026-06-02 23:54:22 +02:00
Bram Kragten 73557e6464 Migrate trigger behavior (#52360)
* Migrate trigger behavior

* Apply suggestions from code review

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>

---------

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2026-06-02 23:54:21 +02:00
Marcin Bauer e9e6c60d8b Move live-test indicator to badge on condition icon (#52352)
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Wendelin <w@pe8.at>
2026-06-02 23:54:20 +02:00
Aidan Timson 1651c210be Improve messaging and consolidate add to dialogs (#52330) 2026-06-02 23:54:19 +02:00
Bram Kragten 927c036454 Bumped version to 20260527.2 2026-06-01 19:52:36 +02:00
Paul Bottein 0fefcf809f Fix vacuum and lawn mower features not showing default buttons (#52343) 2026-06-01 19:52:19 +02:00
Bram Kragten a176f3c1ef Allow to set refresh url while dialog is open, use for matter device (#52341)
Allow to set refresh dialog while dialog is open, use for matter device
2026-06-01 19:52:18 +02:00
Wendelin c5152c3472 App-Info: Hide app title on narrow (#52337)
Hide app title on narrow
2026-06-01 19:52:17 +02:00
Wendelin 0150337522 Fix picker default popover-placement (#52336) 2026-06-01 19:52:16 +02:00
Paul Bottein 5d55d543b1 Respect backend order for floors and areas in entity tree (#52329) 2026-06-01 19:52:14 +02:00
George Caliment 4805b22289 Fixed filter flex direction on mobile + removed unused classes (#52327)
* Fixed filter flex direction on mobile + removed unused classes

* Removed hard-coded height to fill all viewport
2026-06-01 19:52:13 +02:00
Simon Lamon 8de411abc3 Show all counter actions if none specified (#52317)
Show all actions if none specified
2026-06-01 19:52:12 +02:00
Jan-Philipp Benecke e455d4384a Use right token for topbar shadow transition (#52306) 2026-06-01 19:52:11 +02:00
karwosts b0dbd825c8 Fix behavior for move view left/right (#52300) 2026-06-01 19:52:10 +02:00
karwosts 69d0fcb666 Fix untracked legend in detail graph card (#52299) 2026-06-01 19:52:09 +02:00
Simon Lamon f7c3ed3b77 Ignore location in description (#52297) 2026-06-01 19:52:08 +02:00
Jan-Philipp Benecke 5ee5b5120e Add box-shadow transition to top app bar (#52292) 2026-06-01 19:52:07 +02:00
karwosts 58fc8160fd Fix missing location data in calendar (#52291) 2026-06-01 19:52:06 +02:00
Bram Kragten 30930e18ab Bumped version to 20260527.1 2026-05-28 16:47:56 +02:00
Paul Bottein 8d0978817d Don't lowercase translated default action label (#52283) 2026-05-28 16:45:20 +02:00
Paul Bottein fc684218ce Preserve PNG transparency on area pictures (#52282) 2026-05-28 16:45:18 +02:00
Paul Bottein 22f29b7561 Fix sun condition Between description showing reversed values (#52279) 2026-05-28 16:45:16 +02:00
Wendelin c7d48aba44 Fix automation add TCA paste (#52276)
Fix automation add paste
2026-05-28 16:45:15 +02:00
Wendelin aeb2285f30 App details improve mobile and icon (#52275)
* icon instead of logo, enable wrap

* Keep logo

* revert test url
2026-05-28 16:45:14 +02:00
Wendelin c692d7cd4e Card visibility-status use ha-alert (#52271) 2026-05-28 16:45:12 +02:00
Wendelin f2d7021a7d Fix automation note keyboard a11y (#52270) 2026-05-28 16:45:11 +02:00
Wendelin 3a649fba22 Fix automation behavior img file names (#52247)
fix behavior img names
2026-05-28 16:45:09 +02:00
Simon Lamon 5362b8f853 Don't redispatch the original event in a checklist item (#52242) 2026-05-28 16:45:08 +02:00
Wendelin d05800bda6 Fix ha-radio-option checked theming (#52237)
Update ha-radio-option theming to use checked-icon-color for text and border
2026-05-28 16:45:07 +02:00
Wendelin d67530ea37 Fix row target count flickering, keyboard nav, type device (#52236)
* Fix row target count flickering

* Add noninteractive for device, fix keyboard nav

* Noninteractive action, conditon

* Remove unsued hass

* invert noninteractive
2026-05-28 16:45:05 +02:00
Petar Petrov bbd7ef676e Render echarts tooltips with Lit templates (#52235)
* Render echarts tooltips with Lit templates

Replace raw HTML string interpolation in echarts tooltip formatters with Lit templates so user-controlled fields (entity friendly_name, device names, node labels) are auto-escaped instead of relying on per-string filterXSS. ha-chart-base now wraps any function tooltip.formatter into a stable per-formatter container and handles Lit TemplateResult / nothing / null returns; the public HaECOption type lets charts express Lit-returning formatters without per-callsite casts.

* Simplify

* Refactor _getSeries

* Small fix

* Fix merge mistake

* Marker component and wrapper test
2026-05-28 16:45:04 +02:00
151 changed files with 2220 additions and 3100 deletions
+3 -3
View File
@@ -41,14 +41,14 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
uses: github/codeql-action/init@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
with:
languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
uses: github/codeql-action/autobuild@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -62,4 +62,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0
uses: github/codeql-action/analyze@9e0d7b8d25671d64c341c19c0152d693099fb5ba # v4.35.5
+5 -3
View File
@@ -13,11 +13,13 @@ jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@89ae32b08ed1a541efecbab17912962a5e38981c # v6.0.2
- uses: dessant/lock-threads@7266a7ce5c1df01b1c6db85bf8cd86c737dadbe7 # v6.0.0
with:
github-token: ${{ github.token }}
process-only: "issues, prs"
issue-inactive-days: "30"
issue-lock-inactive-days: "30"
issue-exclude-created-before: "2020-10-01T00:00:00Z"
issue-lock-reason: ""
pr-inactive-days: "1"
pr-lock-inactive-days: "1"
pr-exclude-created-before: "2020-11-01T00:00:00Z"
pr-lock-reason: ""
+3 -16
View File
@@ -36,7 +36,7 @@ jobs:
python-version: ${{ env.PYTHON_VERSION }}
- name: Verify version
uses: home-assistant/actions/helpers/verify-version@868e6cb4607727d764341a158d98872cd63fa658 # master
uses: home-assistant/actions/helpers/verify-version@f6f29a7ee3fa0eccadf3620a7b9ee00ab54ec03b # master
- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
@@ -77,22 +77,9 @@ jobs:
env:
GITHUB_REF: ${{ github.ref }}
run: |
# Sleep to give pypi time to populate the new version across mirrors
sleep 240
version=$(echo "$GITHUB_REF" | awk -F"/" '{print $NF}' )
# Wait for the package to become available on PyPI
echo "Waiting for home-assistant-frontend==$version to appear on PyPI..."
for i in $(seq 1 30); do
status=$(curl -s -o /dev/null -w "%{http_code}" "https://pypi.org/pypi/home-assistant-frontend/$version/json")
if [ "$status" = "200" ]; then
echo "Package is available on PyPI!"
break
fi
if [ "$i" = "30" ]; then
echo "Timed out waiting for package to appear on PyPI"
exit 1
fi
echo "Not available yet (HTTP $status), retrying in 30 seconds... ($i/30)"
sleep 30
done
echo "home-assistant-frontend==$version" > ./requirements.txt
# home-assistant/wheels doesn't support SHA pinning
+1 -1
View File
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 90 days stale policy
uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-stale: 90
+1 -3
View File
@@ -57,9 +57,7 @@ gulp.task("gather-gallery-pages", async function gatherPages() {
if (descriptionContent === "") {
hasDescription = false;
} else {
descriptionContent = marked(descriptionContent)
.replace(/\\/g, "\\\\")
.replace(/`/g, "\\`");
descriptionContent = marked(descriptionContent).replace(/`/g, "\\`");
fs.mkdirSync(path.resolve(galleryBuild, category), { recursive: true });
fs.writeFileSync(
path.resolve(galleryBuild, `${pageId}-description.ts`),
@@ -13,7 +13,7 @@ Our dialogs are based on the latest version of Material Design. Please note that
- Dialogs have a max width of 560px. Alert and confirmation dialogs have a fixed width of 320px. If you need more width, consider a dedicated page instead.
- The close X-icon is on the top left, on all screen sizes. Except for alert and confirmation dialogs, they only have buttons and no X-icon. This is different compared to the Material guidelines.
- Dialogs can't be closed with ESC or clicked outside of the dialog when there is a form that the user **has made changes to**. Instead it will animate "no" by a little shake.
- Dialogs can't be closed with ESC or clicked outside of the dialog when there is a form that the user needs to fill out. Instead it will animate "no" by a little shake.
- Extra icon buttons are on the top right, for example help, settings and expand dialog. More than 2 icon buttons, they will be in an overflow menu.
- The submit button is grouped with a cancel button at the bottom right, on all screen sizes. Fullscreen mobile dialogs have them sticky at the bottom.
- Keep the labels short, for example `Save`, `Delete`, `Enable`.
+29 -25
View File
@@ -27,7 +27,7 @@
"license": "Apache-2.0",
"type": "module",
"dependencies": {
"@babel/runtime": "7.29.7",
"@babel/runtime": "7.29.2",
"@braintree/sanitize-url": "7.1.2",
"@codemirror/autocomplete": "6.20.2",
"@codemirror/commands": "6.10.3",
@@ -40,15 +40,15 @@
"@codemirror/view": "6.43.0",
"@date-fns/tz": "1.5.0",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "7.4.7",
"@formatjs/intl-displaynames": "7.3.9",
"@formatjs/intl-durationformat": "0.10.13",
"@formatjs/intl-datetimeformat": "7.4.6",
"@formatjs/intl-displaynames": "7.3.8",
"@formatjs/intl-durationformat": "0.10.12",
"@formatjs/intl-getcanonicallocales": "3.2.9",
"@formatjs/intl-listformat": "8.3.9",
"@formatjs/intl-listformat": "8.3.8",
"@formatjs/intl-locale": "5.3.8",
"@formatjs/intl-numberformat": "9.3.10",
"@formatjs/intl-pluralrules": "6.3.9",
"@formatjs/intl-relativetimeformat": "12.3.9",
"@formatjs/intl-numberformat": "9.3.9",
"@formatjs/intl-pluralrules": "6.3.8",
"@formatjs/intl-relativetimeformat": "12.3.8",
"@fullcalendar/core": "6.1.20",
"@fullcalendar/daygrid": "6.1.20",
"@fullcalendar/interaction": "6.1.20",
@@ -62,16 +62,17 @@
"@lit-labs/virtualizer": "2.1.1",
"@lit/context": "1.1.6",
"@lit/reactive-element": "2.1.2",
"@material/mwc-base": "0.27.0",
"@material/mwc-formfield": "patch:@material/mwc-formfield@npm%3A0.27.0#~/.yarn/patches/@material-mwc-formfield-npm-0.27.0-9528cb60f6.patch",
"@material/mwc-list": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
"@material/web": "2.4.1",
"@mdi/js": "7.4.47",
"@mdi/svg": "7.4.47",
"@replit/codemirror-indentation-markers": "6.5.3",
"@swc/helpers": "0.5.23",
"@swc/helpers": "0.5.21",
"@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "4.1.1",
"@tsparticles/preset-links": "4.1.1",
"@tsparticles/engine": "4.0.5",
"@tsparticles/preset-links": "4.0.5",
"@vibrant/color": "4.0.4",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
"@webcomponents/webcomponentsjs": "2.8.0",
@@ -82,13 +83,13 @@
"core-js": "3.49.0",
"cropperjs": "1.6.2",
"culori": "4.0.2",
"date-fns": "4.4.0",
"date-fns": "4.3.0",
"deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1",
"dialog-polyfill": "0.5.6",
"echarts": "6.1.0",
"element-internals-polyfill": "3.0.2",
"fuse.js": "7.4.0",
"fuse.js": "7.3.0",
"google-timezones-json": "1.2.0",
"gulp-zopfli-green": "7.0.0",
"hls.js": "1.6.16",
@@ -125,20 +126,21 @@
"xss": "1.0.15"
},
"devDependencies": {
"@babel/core": "7.29.7",
"@babel/core": "7.29.0",
"@babel/helper-define-polyfill-provider": "0.6.8",
"@babel/plugin-transform-runtime": "7.29.7",
"@babel/preset-env": "7.29.7",
"@bundle-stats/plugin-webpack-filter": "4.22.2",
"@babel/plugin-transform-runtime": "7.29.0",
"@babel/preset-env": "7.29.5",
"@bundle-stats/plugin-webpack-filter": "4.22.1",
"@eslint/js": "10.0.1",
"@html-eslint/eslint-plugin": "0.61.0",
"@lokalise/node-api": "16.0.0",
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.1.0",
"@octokit/rest": "22.0.1",
"@rsdoctor/rspack-plugin": "1.5.12",
"@rspack/core": "2.0.5",
"@rspack/dev-server": "2.0.3",
"@rsdoctor/rspack-plugin": "1.5.11",
"@rspack/core": "2.0.4",
"@rspack/dev-server": "2.0.1",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.26",
"@types/chromecast-caf-sender": "1.0.11",
"@types/color-name": "2.0.0",
@@ -150,15 +152,17 @@
"@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",
"browserslist-useragent-regexp": "4.1.4",
"del": "8.0.1",
"eslint": "10.4.1",
"eslint": "10.4.0",
"eslint-config-prettier": "10.1.8",
"eslint-import-resolver-webpack": "0.13.11",
"eslint-plugin-import-x": "4.16.2",
@@ -179,8 +183,8 @@
"husky": "9.1.7",
"jsdom": "29.1.1",
"jszip": "3.10.1",
"license-checker-rseidelsohn": "5.0.1",
"lint-staged": "17.0.7",
"license-checker-rseidelsohn": "4.4.2",
"lint-staged": "17.0.5",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.18.1",
@@ -191,10 +195,10 @@
"serve": "14.2.6",
"sinon": "22.0.0",
"tar": "7.5.15",
"terser-webpack-plugin": "5.6.1",
"terser-webpack-plugin": "5.6.0",
"ts-lit-plugin": "2.0.2",
"typescript": "6.0.3",
"typescript-eslint": "8.60.0",
"typescript-eslint": "8.59.4",
"vite-tsconfig-paths": "6.1.1",
"vitest": "4.1.7",
"webpack-stats-plugin": "1.1.3",
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20260527.0"
version = "20260527.4"
license = "Apache-2.0"
license-files = ["LICENSE*"]
description = "The Home Assistant frontend"
+19 -67
View File
@@ -11,7 +11,6 @@ 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 {
@@ -27,28 +26,6 @@ const resolveAtPath = (host: unknown, path: readonly string[]) => {
return cur;
};
/** Reuse `previous` when every entry still references the same `HassEntity`. */
export const preserveUnchangedEntityStatesRecord = <
T extends Record<string, HassEntity | undefined>,
>(
previous: T | undefined,
next: T
): T => {
if (!previous) {
return next;
}
const nextKeys = Object.keys(next);
if (Object.keys(previous).length !== nextKeys.length) {
return next;
}
for (const key of nextKeys) {
if (previous[key] !== next[key]) {
return next;
}
}
return previous;
};
const composeDecorator = <T, V>(
context: Parameters<typeof consume>[0]["context"],
watchKey: string | undefined,
@@ -86,52 +63,27 @@ export const consumeEntityState = (config: ConsumeEntryConfig) =>
);
/**
* Like {@link consumeEntityState} but for one or more entity IDs at
* `entityIdPath` (a string or string array; wrapped with {@link ensureArray}).
* Resolves to a record keyed by entity ID containing the currently-available
* entities (missing entities and non-string IDs are filtered out). Returns the
* previous record when none of the selected entities changed.
* Like {@link consumeEntityState} but for an array of entity IDs at
* `entityIdPath`. Resolves to a `HassEntity[]` containing one entry per
* currently-available entity (missing entities and non-string IDs are
* filtered out; original order is preserved).
*/
export const consumeEntityStates = (config: ConsumeEntryConfig) => {
const watchKey = config.entityIdPath[0];
const buildRecord = function (this: unknown, states: HassEntities) {
const ids = ensureArray(resolveAtPath(this, config.entityIdPath));
if (!ids || !states) return undefined;
const result: Record<string, HassEntity> = {};
for (const id of ids) {
if (typeof id !== "string") continue;
const state = states[id];
if (state !== undefined) result[id] = state;
export const consumeEntityStates = (config: ConsumeEntryConfig) =>
composeDecorator<HassEntities, HassEntity[]>(
statesContext,
config.entityIdPath[0],
function (states) {
const ids = resolveAtPath(this, config.entityIdPath);
if (!Array.isArray(ids) || !states) return undefined;
const result: HassEntity[] = [];
for (const id of ids) {
if (typeof id !== "string") continue;
const state = states[id];
if (state !== undefined) result.push(state);
}
return result;
}
return result;
};
return (proto: unknown, propertyKey: string) => {
const key = String(propertyKey);
const transformDec = transform<
HassEntities,
Record<string, HassEntity> | undefined
>({
transformer: function (this: unknown, states: HassEntities) {
const next = buildRecord.call(this, states);
if (next === undefined) {
return undefined;
}
const previous = (this as Record<string, unknown>)[
`__transform_${key}`
] as Record<string, HassEntity> | undefined;
return preserveUnchangedEntityStatesRecord(previous, next);
},
watch: watchKey ? [watchKey] : [],
});
const consumeDec = consume<any>({
context: statesContext,
subscribe: true,
});
transformDec(proto as never, propertyKey);
consumeDec(proto as never, propertyKey);
};
};
);
/**
* Consumes `entitiesContext` and narrows it to the
-37
View File
@@ -1,37 +0,0 @@
import type { PickerComboBoxItem } from "../../components/ha-picker-combo-box";
import type { RelatedResult } from "../../data/search";
export interface RelatedIdSets {
areas: Set<string>;
devices: Set<string>;
entities: Set<string>;
}
/**
* Build a set of related IDs for a given related result.
* @param related - The related result to build the sets from.
* @returns The related ID sets.
*/
export const buildRelatedIdSets = (related?: RelatedResult): RelatedIdSets => ({
areas: new Set(related?.area || []),
devices: new Set(related?.device || []),
entities: new Set(related?.entity || []),
});
/**
* Stable partition sort: related items float to the top,
* preserving relative order (e.g. Fuse score) within each group.
* @param items - The items to sort.
* @returns The sorted items.
*/
export const sortRelatedFirst = (
items: PickerComboBoxItem[]
): PickerComboBoxItem[] =>
[...items].sort((a, b) => {
const aRelated = Boolean(a.isRelated);
const bRelated = Boolean(b.isRelated);
if (aRelated === bRelated) {
return 0;
}
return aRelated ? -1 : 1;
});
@@ -6,6 +6,7 @@ import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { getGraphColorByIndex } from "../../common/color/colors";
import type { HaECOption } from "../../resources/echarts/echarts";
import type { HomeAssistant } from "../../types";
import "./ha-chart-base";
import "./ha-chart-tooltip-marker";
@@ -24,6 +25,8 @@ export interface SunburstNode {
@customElement("ha-sunburst-chart")
export class HaSunburstChart extends LitElement {
public hass!: HomeAssistant;
@property({ attribute: false }) public data?: SunburstNode;
@property({ attribute: false }) public valueFormatter?: (
+5 -3
View File
@@ -107,15 +107,17 @@ 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 {
+1 -23
View File
@@ -309,29 +309,7 @@ export class HaEntityPicker extends LitElement {
}
);
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 _getEntitiesMemoized = memoizeOne(getEntities);
private _getItems = () => {
const items = this._getEntitiesMemoized(
+2 -34
View File
@@ -1,16 +1,11 @@
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import {
mdiChartLine,
mdiHelpCircleOutline,
mdiPencil,
mdiShape,
} from "@mdi/js";
import { mdiChartLine, mdiHelpCircleOutline, mdiShape } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { type HASSDomEvent, fireEvent } from "../../common/dom/fire_event";
import { fireEvent } from "../../common/dom/fire_event";
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
import { computeStateName } from "../../common/entity/compute_state_name";
import { computeRTL } from "../../common/util/compute_rtl";
@@ -58,16 +53,6 @@ const SEARCH_KEYS = [
{ name: "id", weight: 2 },
];
export interface StatisticElementChangedEvent {
statisticId: string;
}
declare global {
interface HASSDomEvents {
"edit-statistics-element": StatisticElementChangedEvent;
}
}
@customElement("ha-statistic-picker")
export class HaStatisticPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -145,8 +130,6 @@ export class HaStatisticPicker extends LitElement {
@query("ha-generic-picker") private _picker?: HaGenericPicker;
@property({ attribute: "can-edit", type: Boolean }) public canEdit?: boolean;
public willUpdate(changedProps: PropertyValues<this>) {
if (
(!this.hasUpdated && !this.statisticIds) ||
@@ -358,15 +341,6 @@ export class HaStatisticPicker extends LitElement {
${item.secondary
? html`<span slot="supporting-text">${item.secondary}</span>`
: nothing}
${this.canEdit
? html`<ha-icon-button
slot="end"
.value=${statisticId}
.label=${this.hass.localize("ui.common.edit")}
.path=${mdiPencil}
@click=${this._editItem}
></ha-icon-button>`
: nothing}
`;
}
@@ -376,12 +350,6 @@ export class HaStatisticPicker extends LitElement {
private _valueRenderer: PickerValueRenderer = this._makeValueRenderer();
private _editItem(ev: HASSDomEvent<StatisticElementChangedEvent>) {
ev.stopPropagation();
const statisticId = (ev.currentTarget as any).value;
fireEvent(this, "edit-statistics-element", { statisticId });
}
private _computeItem(statisticId: string): StatisticComboBoxItem {
const stateObj = this.hass.states[statisticId];
+1 -17
View File
@@ -1,10 +1,9 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { type HASSDomEvent, fireEvent } from "../../common/dom/fire_event";
import { fireEvent } from "../../common/dom/fire_event";
import type { ValueChangedEvent, HomeAssistant } from "../../types";
import "./ha-statistic-picker";
import type { StatisticElementChangedEvent } from "./ha-statistic-picker";
@customElement("ha-statistics-picker")
class HaStatisticsPicker extends LitElement {
@@ -60,8 +59,6 @@ class HaStatisticsPicker extends LitElement {
})
public ignoreRestrictionsOnFirstStatistic = false;
@property({ attribute: "can-edit", type: Boolean }) public canEdit?;
protected render() {
if (!this.hass) {
return nothing;
@@ -102,9 +99,7 @@ class HaStatisticsPicker extends LitElement {
.statisticIds=${this.statisticIds}
.excludeStatistics=${this.value}
.allowCustomEntity=${this.allowCustomEntity}
.canEdit=${this.canEdit}
@value-changed=${this._statisticChanged}
@edit-statistics-element=${this._editItem}
></ha-statistic-picker>
</div>
`
@@ -127,17 +122,6 @@ class HaStatisticsPicker extends LitElement {
`;
}
private _editItem(ev: HASSDomEvent<StatisticElementChangedEvent>) {
const statisticId = ev.detail.statisticId;
const index = this._currentStatistics!.findIndex((e) => e === statisticId);
fireEvent(this, "edit-detail-element", {
subElementConfig: {
index,
type: "row",
},
});
}
private get _currentStatistics() {
return this.value || [];
}
+3
View File
@@ -43,6 +43,7 @@ class StateInfo extends LitElement {
)}:
</span>
<ha-relative-time
.hass=${this.hass}
.datetime=${this.stateObj.last_changed}
capitalize
></ha-relative-time>
@@ -54,6 +55,7 @@ class StateInfo extends LitElement {
)}:
</span>
<ha-relative-time
.hass=${this.hass}
.datetime=${this.stateObj.last_updated}
capitalize
></ha-relative-time>
@@ -61,6 +63,7 @@ class StateInfo extends LitElement {
</ha-tooltip>
<ha-relative-time
id="relative-time"
.hass=${this.hass}
.datetime=${this.stateObj.last_changed}
capitalize
></ha-relative-time>
+7 -27
View File
@@ -1,34 +1,18 @@
import { consume } from "@lit/context";
import { addDays, differenceInMilliseconds, startOfDay } from "date-fns";
import type { HassConfig } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { ReactiveElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { transform } from "../common/decorators/transform";
import { customElement, property } from "lit/decorators";
import { absoluteTime } from "../common/datetime/absolute_time";
import { configContext, internationalizationContext } from "../data/context";
import type {
HomeAssistantConfig,
HomeAssistantInternationalization,
} from "../types";
import type { HomeAssistant } from "../types";
const SAFE_MARGIN = 5 * 1000;
@customElement("ha-absolute-time")
class HaAbsoluteTime extends ReactiveElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public datetime?: string | Date;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n?: HomeAssistantInternationalization;
@state()
@consume({ context: configContext, subscribe: true })
@transform<HomeAssistantConfig, HassConfig>({
transformer: ({ config }) => config,
})
private _config?: HassConfig;
private _timeout?: number;
public disconnectedCallback(): void {
@@ -78,17 +62,13 @@ class HaAbsoluteTime extends ReactiveElement {
}
private _updateAbsolute(): void {
if (!this._i18n || !this._config) {
return;
}
if (!this.datetime) {
this.innerHTML = this._i18n.localize("ui.components.absolute_time.never");
this.innerHTML = this.hass.localize("ui.components.absolute_time.never");
} else {
this.innerHTML = absoluteTime(
new Date(this.datetime),
this._i18n.locale,
this._config
this.hass.locale,
this.hass.config
);
}
}
+1 -24
View File
@@ -12,7 +12,6 @@ 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";
@@ -105,29 +104,7 @@ export class HaAreaPicker extends LitElement {
await this._picker?.open();
}
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,
})
);
private _getAreasMemoized = memoizeOne(getAreas);
// Recompute value renderer when the areas change
private _computeValueRenderer = memoizeOne(
+7 -22
View File
@@ -11,15 +11,12 @@ import {
mdiStateMachine,
mdiWeatherSunny,
} from "@mdi/js";
import { consume } from "@lit/context";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { until } from "lit/directives/until";
import type { HassConfig, Connection } from "home-assistant-js-websocket";
import { computeDomain } from "../common/entity/compute_domain";
import { transform } from "../common/decorators/transform";
import { configContext, connectionContext } from "../data/context";
import { conditionIcon, FALLBACK_DOMAIN_ICONS } from "../data/icons";
import type { HomeAssistant } from "../types";
import "./ha-icon";
import "./ha-svg-icon";
@@ -39,24 +36,12 @@ export const CONDITION_ICONS = {
@customElement("ha-condition-icon")
export class HaConditionIcon extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public condition?: string;
@property() public icon?: string;
@state()
@consume({ context: configContext, subscribe: true })
@transform<{ config: HassConfig }, HassConfig>({
transformer: ({ config }) => config,
})
private _config?: HassConfig;
@state()
@consume({ context: connectionContext, subscribe: true })
@transform<{ connection: Connection }, Connection>({
transformer: ({ connection }) => connection,
})
private _connection?: Connection;
protected render() {
if (this.icon) {
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
@@ -66,13 +51,13 @@ export class HaConditionIcon extends LitElement {
return nothing;
}
if (!this._connection || !this._config) {
if (!this.hass) {
return this._renderFallback();
}
const icon = conditionIcon(
this._connection,
this._config,
this.hass.connection,
this.hass.config,
this.condition
).then((icn) => {
if (icn) {
+1 -7
View File
@@ -3,8 +3,6 @@ import { mdiFilterVariantRemove } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../common/translations/localize";
import { fireEvent } from "../common/dom/fire_event";
import { deepEqual } from "../common/util/deep-equal";
import type { Blueprints } from "../data/blueprint";
@@ -22,10 +20,6 @@ import "./ha-list";
export class HaFilterBlueprints extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ attribute: false }) public value?: string[];
@property() public type?: "automation" | "script";
@@ -60,7 +54,7 @@ export class HaFilterBlueprints extends LitElement {
@expanded-changed=${this._expandedChanged}
>
<div slot="header" class="header">
${this._localize("ui.panel.config.blueprint.caption")}
${this.hass.localize("ui.panel.config.blueprint.caption")}
${this.value?.length
? html`<div class="badge">${this.value?.length}</div>
<ha-icon-button
+3 -7
View File
@@ -4,8 +4,6 @@ import type { CSSResultGroup, PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../common/translations/localize";
import { fireEvent } from "../common/dom/fire_event";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
@@ -24,10 +22,6 @@ import "../panels/config/voice-assistants/expose/expose-assistant-icon";
export class HaFilterVoiceAssistants extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
// the list of selected voiceAssistantIds
@property({ attribute: false }) public value: string[] = [];
@@ -50,7 +44,9 @@ export class HaFilterVoiceAssistants extends LitElement {
@expanded-changed=${this._expandedChanged}
>
<div slot="header" class="header">
${this._localize("ui.panel.config.dashboard.voice_assistants.main")}
${this.hass.localize(
"ui.panel.config.dashboard.voice_assistants.main"
)}
${this.value?.length
? html`<div class="badge">${this.value?.length}</div>
<ha-icon-button
+4 -7
View File
@@ -3,12 +3,13 @@ import type { TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { mainWindow } from "../common/dom/get_main_window";
import type { HomeAssistant } from "../types";
import "./ha-icon-button";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../common/translations/localize";
@customElement("ha-icon-button-arrow-prev")
export class HaIconButtonArrowPrev extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public disabled = false;
@property() public label?: string;
@@ -24,15 +25,11 @@ export class HaIconButtonArrowPrev extends LitElement {
@state() private _icon =
mainWindow.document.dir === "rtl" ? mdiArrowRight : mdiArrowLeft;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
protected render(): TemplateResult {
return html`
<ha-icon-button
.disabled=${this.disabled}
.label=${this.label || this._localize("ui.common.back") || "Back"}
.label=${this.label || this.hass?.localize("ui.common.back") || "Back"}
.path=${this._icon}
.href=${this.href}
.target=${this.target}
+1 -30
View File
@@ -50,9 +50,7 @@ class HaLabel extends LitElement {
<div class="container" .id=${this._elementId}>
<span class="content">
<slot name="icon"></slot>
<span class="label-content">
<slot></slot>
</span>
<slot></slot>
</span>
</div>
`;
@@ -115,10 +113,6 @@ class HaLabel extends LitElement {
display: inline-flex;
}
.label-content {
display: contents;
}
:host([dense]) {
height: 20px;
border-radius: var(--ha-border-radius-md);
@@ -132,29 +126,6 @@ class HaLabel extends LitElement {
margin-inline-start: -4px;
margin-inline-end: 4px;
}
:host(.text-ellipsis) {
max-width: 100%;
min-width: 0;
}
:host(.text-ellipsis) .container {
min-width: 0;
overflow: hidden;
}
:host(.text-ellipsis) span.content {
display: flex;
width: 100%;
min-width: 0;
}
:host(.text-ellipsis) .label-content {
display: block;
flex: 1;
min-width: 0;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
`,
];
}
+6 -14
View File
@@ -1,23 +1,19 @@
import { consume } from "@lit/context";
import { parseISO } from "date-fns";
import type { PropertyValues } from "lit";
import { ReactiveElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { relativeTime } from "../common/datetime/relative_time";
import { capitalizeFirstLetter } from "../common/string/capitalize-first-letter";
import { internationalizationContext } from "../data/context";
import type { HomeAssistantInternationalization } from "../types";
import type { HomeAssistant } from "../types";
@customElement("ha-relative-time")
class HaRelativeTime extends ReactiveElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public datetime?: string | Date;
@property({ type: Boolean }) public capitalize = false;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n?: HomeAssistantInternationalization;
private _interval?: number;
public disconnectedCallback(): void {
@@ -61,19 +57,15 @@ class HaRelativeTime extends ReactiveElement {
}
private _updateRelative(): void {
if (!this._i18n) {
return;
}
if (!this.datetime) {
this.innerHTML = this._i18n.localize("ui.components.relative_time.never");
this.innerHTML = this.hass.localize("ui.components.relative_time.never");
} else {
const date =
typeof this.datetime === "string"
? parseISO(this.datetime)
: this.datetime;
const relTime = relativeTime(date, this._i18n.locale);
const relTime = relativeTime(date, this.hass.locale);
this.innerHTML = this.capitalize
? capitalizeFirstLetter(relTime)
: relTime;
@@ -1,13 +1,11 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ensureArray } from "../../common/array/ensure-array";
import { consumeEntityStates } from "../../common/decorators/consume-context-entry";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { AttributeSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../entity/ha-entity-attribute-picker";
import { ensureArray } from "../../common/array/ensure-array";
@customElement("ha-selector-attribute")
export class HaSelectorAttribute extends LitElement {
@@ -29,10 +27,6 @@ export class HaSelectorAttribute extends LitElement {
filter_entity?: string | string[];
};
@state()
@consumeEntityStates({ entityIdPath: ["context", "filter_entity"] })
private _filterEntityStates?: Record<string, HassEntity>;
protected render() {
return html`
<ha-entity-attribute-picker
@@ -79,7 +73,7 @@ export class HaSelectorAttribute extends LitElement {
const entityIds = ensureArray(this.context.filter_entity);
invalid = !entityIds.some((entityId) => {
const stateObj = this._filterEntityStates?.[entityId];
const stateObj = this.hass.states[entityId];
return (
stateObj &&
this.value in stateObj.attributes &&
@@ -1,12 +1,15 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { HomeAssistant } from "../../types";
import "../ha-formfield";
import "../ha-switch";
import "../ha-input-helper-text";
@customElement("ha-selector-boolean")
export class HaBooleanSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public value = false;
@property() public placeholder?: any;
@@ -1,26 +1,14 @@
import { consume } from "@lit/context";
import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { transform } from "../../common/decorators/transform";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import type { ButtonToggleSelector, SelectOption } from "../../data/selector";
import { internationalizationContext } from "../../data/context";
import type { FrontendLocaleData } from "../../data/translation";
import type {
HomeAssistantInternationalization,
ToggleButton,
} from "../../types";
import type { HomeAssistant, ToggleButton } from "../../types";
import "../ha-button-toggle-group";
@customElement("ha-selector-button_toggle")
export class HaButtonToggleSelector extends LitElement {
@state()
@consume({ context: internationalizationContext, subscribe: true })
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
transformer: ({ locale }) => locale,
})
private _locale!: FrontendLocaleData;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: ButtonToggleSelector;
@@ -60,7 +48,11 @@ export class HaButtonToggleSelector extends LitElement {
if (this.selector.button_toggle?.sort) {
options.sort((a, b) =>
caseInsensitiveStringCompare(a.label, b.label, this._locale.language)
caseInsensitiveStringCompare(
a.label,
b.label,
this.hass.locale.language
)
);
}
@@ -2,12 +2,10 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
import { fireEvent } from "../../common/dom/fire_event";
import { isTemplate } from "../../common/string/has-template";
import type { ChooseSelector, Selector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import type { LocalizeFunc } from "../../common/translations/localize";
import "../ha-button-toggle-group";
import "./ha-selector";
@@ -30,9 +28,6 @@ export class HaChooseSelector extends LitElement {
@property({ type: Boolean }) public required = true;
@consumeLocalize()
protected _localize?: LocalizeFunc;
@state() public _activeChoice?: string;
protected willUpdate(changedProperties: PropertyValues<this>): void {
@@ -67,7 +62,7 @@ export class HaChooseSelector extends LitElement {
.buttons=${this._toggleButtons(
this.selector.choose.choices,
this.selector.choose.translation_key,
this._localize
this.hass.localize
)}
.active=${this._activeChoice}
@value-changed=${this._choiceChanged}
@@ -88,7 +83,7 @@ export class HaChooseSelector extends LitElement {
(
choices: ChooseSelector["choose"]["choices"],
translationKey?: string,
_localize?: LocalizeFunc
_localize?: HomeAssistant["localize"]
) =>
Object.keys(choices).map((choice) => ({
label:
+4 -13
View File
@@ -1,22 +1,13 @@
import { consume } from "@lit/context";
import { html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { transform } from "../../common/decorators/transform";
import { customElement, property, query } from "lit/decorators";
import type { DateSelector } from "../../data/selector";
import { internationalizationContext } from "../../data/context";
import type { FrontendLocaleData } from "../../data/translation";
import type { HomeAssistantInternationalization } from "../../types";
import type { HomeAssistant } from "../../types";
import "../ha-date-input";
import type { HaDateInput } from "../ha-date-input";
@customElement("ha-selector-date")
export class HaDateSelector extends LitElement {
@state()
@consume({ context: internationalizationContext, subscribe: true })
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
transformer: ({ locale }) => locale,
})
private _locale!: FrontendLocaleData;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: DateSelector;
@@ -40,7 +31,7 @@ export class HaDateSelector extends LitElement {
return html`
<ha-date-input
.label=${this.label}
.locale=${this._locale}
.locale=${this.hass.locale}
.disabled=${this.disabled}
.value=${typeof this.value === "string" ? this.value : undefined}
.required=${this.required}
@@ -1,12 +1,8 @@
import { consume } from "@lit/context";
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { transform } from "../../common/decorators/transform";
import type { DateTimeSelector } from "../../data/selector";
import { internationalizationContext } from "../../data/context";
import type { FrontendLocaleData } from "../../data/translation";
import type { HomeAssistantInternationalization } from "../../types";
import type { HomeAssistant } from "../../types";
import "../ha-date-input";
import type { HaDateInput } from "../ha-date-input";
import "../ha-time-input";
@@ -15,12 +11,7 @@ import type { HaTimeInput } from "../ha-time-input";
@customElement("ha-selector-datetime")
export class HaDateTimeSelector extends LitElement {
@state()
@consume({ context: internationalizationContext, subscribe: true })
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
transformer: ({ locale }) => locale,
})
private _locale!: FrontendLocaleData;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: DateTimeSelector;
@@ -50,7 +41,7 @@ export class HaDateTimeSelector extends LitElement {
<div class="input">
<ha-date-input
.label=${this.label}
.locale=${this._locale}
.locale=${this.hass.locale}
.disabled=${this.disabled}
.required=${this.required}
.value=${values?.[0]}
@@ -60,7 +51,7 @@ export class HaDateTimeSelector extends LitElement {
<ha-time-input
enable-second
.value=${values?.[1] || "00:00:00"}
.locale=${this._locale}
.locale=${this.hass.locale}
.disabled=${this.disabled}
.required=${this.required}
@value-changed=${this._valueChanged}
@@ -2,11 +2,14 @@ import { html, LitElement } from "lit";
import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import type { DurationSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-duration-input";
import type { HaDurationData, HaDurationInput } from "../ha-duration-input";
@customElement("ha-selector-duration")
export class HaTimeDuration extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: DurationSelector;
@property({ attribute: false }) public value?:
@@ -3,12 +3,10 @@ import type { PropertyValues } from "lit";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
import { removeFile, uploadFile } from "../../data/file_upload";
import type { FileSelector } from "../../data/selector";
import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../types";
import type { LocalizeFunc } from "../../common/translations/localize";
import "../ha-file-upload";
@customElement("ha-selector-file")
@@ -27,9 +25,6 @@ export class HaFileSelector extends LitElement {
@property({ type: Boolean }) public required = true;
@consumeLocalize()
protected _localize?: LocalizeFunc;
@state() private _filename?: { fileId: string; name: string };
@state() private _busy = false;
@@ -47,7 +42,7 @@ export class HaFileSelector extends LitElement {
.uploading=${this._busy}
.value=${this.value
? this._filename?.name ||
this._localize!("ui.components.selectors.file.unknown_file")
this.hass.localize("ui.components.selectors.file.unknown_file")
: undefined}
@file-picked=${this._uploadFile}
@change=${this._removeFile}
@@ -77,7 +72,7 @@ export class HaFileSelector extends LitElement {
fireEvent(this, "value-changed", { value: fileId });
} catch (err: any) {
showAlertDialog(this, {
text: this._localize!("ui.components.selectors.file.upload_failed", {
text: this.hass.localize("ui.components.selectors.file.upload_failed", {
reason: err.message || err,
}),
});
@@ -1,7 +1,6 @@
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
import { fireEvent } from "../../common/dom/fire_event";
import type { PeriodKey, PeriodSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
@@ -42,9 +41,6 @@ export class HaPeriodSelector extends LitElement {
@property({ type: Boolean }) public required = true;
@consumeLocalize()
protected _localize?: LocalizeFunc;
private _schema = memoizeOne(
(
selectedPeriodKey: PeriodKey | undefined,
@@ -82,7 +78,7 @@ export class HaPeriodSelector extends LitElement {
const schema = this._schema(
typeof data.period === "string" ? (data.period as PeriodKey) : undefined,
this.selector,
this._localize!
this.hass.localize
);
return html`
@@ -1,20 +1,13 @@
import { mdiDragHorizontalVariant } from "@mdi/js";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { consume } from "@lit/context";
import { ensureArray } from "../../common/array/ensure-array";
import { transform } from "../../common/decorators/transform";
import { fireEvent } from "../../common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { internationalizationContext } from "../../data/context";
import type { SelectOption, SelectSelector } from "../../data/selector";
import type { FrontendLocaleData } from "../../data/translation";
import type {
HomeAssistant,
HomeAssistantInternationalization,
} from "../../types";
import type { HomeAssistant } from "../../types";
import "../chips/ha-chip-set";
import "../chips/ha-input-chip";
import "../ha-checkbox";
@@ -32,13 +25,6 @@ import "../radio/ha-radio-option";
export class HaSelectSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consume({ context: internationalizationContext, subscribe: true })
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
transformer: ({ locale }) => locale,
})
private _locale!: FrontendLocaleData;
@property({ attribute: false }) public selector!: SelectSelector;
@property() public value?: string | string[];
@@ -89,7 +75,11 @@ export class HaSelectSelector extends LitElement {
if (this.selector.select?.sort) {
options.sort((a, b) =>
caseInsensitiveStringCompare(a.label, b.label, this._locale.language)
caseInsensitiveStringCompare(
a.label,
b.label,
this.hass.locale.language
)
);
}
@@ -2,7 +2,6 @@ import type { PropertyValues } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
import { fireEvent } from "../../common/dom/fire_event";
import type {
LocalizeFunc,
@@ -169,9 +168,6 @@ export class HaSelectorSelector extends LitElement {
@property({ type: Boolean, reflect: true }) public required = true;
@consumeLocalize()
protected _localize?: LocalizeFunc;
private _yamlMode = false;
protected shouldUpdate(changedProps: PropertyValues<this>) {
@@ -240,7 +236,7 @@ export class HaSelectorSelector extends LitElement {
};
}
const schema = this._schema(type, this._localize!);
const schema = this._schema(type, this.hass.localize);
return html`<div>
<p>${this.label ? this.label : ""}</p>
@@ -294,7 +290,7 @@ export class HaSelectorSelector extends LitElement {
}
private _computeLabelCallback = (schema: any): string =>
this._localize!(
this.hass.localize(
`ui.components.selectors.selector.${schema.name}` as LocalizeKeys
) || schema.name;
+4 -13
View File
@@ -1,22 +1,13 @@
import { consume } from "@lit/context";
import { html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { transform } from "../../common/decorators/transform";
import { customElement, property, query } from "lit/decorators";
import type { TimeSelector } from "../../data/selector";
import { internationalizationContext } from "../../data/context";
import type { FrontendLocaleData } from "../../data/translation";
import type { HomeAssistantInternationalization } from "../../types";
import type { HomeAssistant } from "../../types";
import "../ha-time-input";
import type { HaTimeInput } from "../ha-time-input";
@customElement("ha-selector-time")
export class HaTimeSelector extends LitElement {
@state()
@consume({ context: internationalizationContext, subscribe: true })
@transform<HomeAssistantInternationalization, FrontendLocaleData>({
transformer: ({ locale }) => locale,
})
private _locale!: FrontendLocaleData;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: TimeSelector;
@@ -40,7 +31,7 @@ export class HaTimeSelector extends LitElement {
return html`
<ha-time-input
.value=${typeof this.value === "string" ? this.value : undefined}
.locale=${this._locale}
.locale=${this.hass.locale}
.disabled=${this.disabled}
.required=${this.required}
clearable
+14 -27
View File
@@ -1,39 +1,24 @@
import { consume } from "@lit/context";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { until } from "lit/directives/until";
import type { Connection, HassConfig } from "home-assistant-js-websocket";
import { computeDomain } from "../common/entity/compute_domain";
import { transform } from "../common/decorators/transform";
import { configContext, connectionContext } from "../data/context";
import {
DEFAULT_SERVICE_ICON,
FALLBACK_DOMAIN_ICONS,
serviceIcon,
} from "../data/icons";
import type { HomeAssistant } from "../types";
import "./ha-icon";
import "./ha-svg-icon";
@customElement("ha-service-icon")
export class HaServiceIcon extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public service?: string;
@property() public icon?: string;
@state()
@consume({ context: configContext, subscribe: true })
@transform<{ config: HassConfig }, HassConfig>({
transformer: ({ config }) => config,
})
private _config?: HassConfig;
@state()
@consume({ context: connectionContext, subscribe: true })
@transform<{ connection: Connection }, Connection>({
transformer: ({ connection }) => connection,
})
private _connection?: Connection;
protected render() {
if (this.icon) {
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
@@ -43,18 +28,20 @@ export class HaServiceIcon extends LitElement {
return nothing;
}
if (!this._connection || !this._config) {
if (!this.hass) {
return this._renderFallback();
}
const icon = serviceIcon(this._connection, this._config, this.service).then(
(icn) => {
if (icn) {
return html`<ha-icon .icon=${icn}></ha-icon>`;
}
return this._renderFallback();
const icon = serviceIcon(
this.hass.connection,
this.hass.config,
this.service
).then((icn) => {
if (icn) {
return html`<ha-icon .icon=${icn}></ha-icon>`;
}
);
return this._renderFallback();
});
return html`${until(icon)}`;
}
+8 -23
View File
@@ -1,36 +1,21 @@
import { consume } from "@lit/context";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { until } from "lit/directives/until";
import type { Connection, HassConfig } from "home-assistant-js-websocket";
import { transform } from "../common/decorators/transform";
import { configContext, connectionContext } from "../data/context";
import { serviceSectionIcon } from "../data/icons";
import type { HomeAssistant } from "../types";
import "./ha-icon";
import "./ha-svg-icon";
import { serviceSectionIcon } from "../data/icons";
@customElement("ha-service-section-icon")
export class HaServiceSectionIcon extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public service?: string;
@property() public section?: string;
@property() public icon?: string;
@state()
@consume({ context: configContext, subscribe: true })
@transform<{ config: HassConfig }, HassConfig>({
transformer: ({ config }) => config,
})
private _config?: HassConfig;
@state()
@consume({ context: connectionContext, subscribe: true })
@transform<{ connection: Connection }, Connection>({
transformer: ({ connection }) => connection,
})
private _connection?: Connection;
protected render() {
if (this.icon) {
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
@@ -40,13 +25,13 @@ export class HaServiceSectionIcon extends LitElement {
return nothing;
}
if (!this._connection || !this._config) {
if (!this.hass) {
return this._renderFallback();
}
const icon = serviceSectionIcon(
this._connection,
this._config,
this.hass.connection,
this.hass.config,
this.service,
this.section
).then((icn) => {
+5 -1
View File
@@ -539,7 +539,11 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
rtl: isRTL,
})}
>
<ha-user-badge slot="start" .user=${this.hass.user}></ha-user-badge>
<ha-user-badge
slot="start"
.user=${this.hass.user}
.hass=${this.hass}
></ha-user-badge>
<span class="item-text" slot="headline"
>${this.hass.user ? this.hass.user.name : ""}</span
>
+3 -47
View File
@@ -130,56 +130,11 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
private _newTarget?: TargetItem;
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 _getDevicesMemoized = memoizeOne(getDevices);
private _getLabelsMemoized = memoizeOne(getLabels);
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 _getEntitiesMemoized = memoizeOne(getEntities);
private _getAreasAndFloorsMemoized = memoizeOne(getAreasAndFloors);
@@ -964,6 +919,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
this.hass,
configEntryLookup,
includeDomains,
undefined,
includeDeviceClasses,
deviceFilter,
entityFilter,
+14 -27
View File
@@ -17,16 +17,13 @@ import {
mdiWeatherSunny,
mdiWebhook,
} from "@mdi/js";
import { consume } from "@lit/context";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { until } from "lit/directives/until";
import type { Connection, HassConfig } from "home-assistant-js-websocket";
import { computeDomain } from "../common/entity/compute_domain";
import { transform } from "../common/decorators/transform";
import { configContext, connectionContext } from "../data/context";
import { FALLBACK_DOMAIN_ICONS, triggerIcon } from "../data/icons";
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
import type { HomeAssistant } from "../types";
import "./ha-icon";
import "./ha-svg-icon";
@@ -53,24 +50,12 @@ export const TRIGGER_ICONS = {
@customElement("ha-trigger-icon")
export class HaTriggerIcon extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public trigger?: string;
@property() public icon?: string;
@state()
@consume({ context: configContext, subscribe: true })
@transform<{ config: HassConfig }, HassConfig>({
transformer: ({ config }) => config,
})
private _config?: HassConfig;
@state()
@consume({ context: connectionContext, subscribe: true })
@transform<{ connection: Connection }, Connection>({
transformer: ({ connection }) => connection,
})
private _connection?: Connection;
protected render() {
if (this.icon) {
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
@@ -80,18 +65,20 @@ export class HaTriggerIcon extends LitElement {
return nothing;
}
if (!this._connection || !this._config) {
if (!this.hass) {
return this._renderFallback();
}
const icon = triggerIcon(this._connection, this._config, this.trigger).then(
(icn) => {
if (icn) {
return html`<ha-icon .icon=${icn}></ha-icon>`;
}
return this._renderFallback();
const icon = triggerIcon(
this.hass.connection,
this.hass.config,
this.trigger
).then((icn) => {
if (icn) {
return html`<ha-icon .icon=${icn}></ha-icon>`;
}
);
return this._renderFallback();
});
return html`${until(icon)}`;
}
+5 -7
View File
@@ -1,8 +1,6 @@
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../../common/translations/localize";
import type { LogbookEntry } from "../../data/logbook";
import type { HomeAssistant } from "../../types";
import "./hat-logbook-note";
@@ -19,9 +17,6 @@ export class HaTraceLogbook extends LitElement {
@property({ attribute: false }) public logbookEntries!: LogbookEntry[];
@consumeLocalize()
private _localize!: LocalizeFunc;
protected render(): TemplateResult {
return this.logbookEntries.length
? html`
@@ -31,10 +26,13 @@ export class HaTraceLogbook extends LitElement {
.entries=${this.logbookEntries}
.narrow=${this.narrow}
></ha-logbook-renderer>
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
<hat-logbook-note
.hass=${this.hass}
.domain=${this.trace.domain}
></hat-logbook-note>
`
: html`<div class="padded-box">
${this._localize(
${this.hass.localize(
"ui.panel.config.automation.trace.path.no_logbook_entries"
)}
</div>`;
@@ -374,7 +374,10 @@ export class HaTracePathDetails extends LitElement {
.entries=${entries}
.narrow=${this.narrow}
></ha-logbook-renderer>
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
<hat-logbook-note
.hass=${this.hass}
.domain=${this.trace.domain}
></hat-logbook-note>
`
: html`<div class="padded-box">
${this.hass!.localize(
+4 -1
View File
@@ -28,7 +28,10 @@ export class HaTraceTimeline extends LitElement {
allow-pick
>
</hat-trace-timeline>
<hat-logbook-note .domain=${this.trace.domain}></hat-logbook-note>
<hat-logbook-note
.hass=${this.hass}
.domain=${this.trace.domain}
></hat-logbook-note>
`;
}
+5 -7
View File
@@ -1,22 +1,20 @@
import { css, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../../common/translations/localize";
import type { HomeAssistant } from "../../types";
@customElement("hat-logbook-note")
class HatLogbookNote extends LitElement {
@property() public domain: "automation" | "script" = "automation";
@property({ attribute: false }) public hass!: HomeAssistant;
@consumeLocalize()
private _localize!: LocalizeFunc;
@property() public domain: "automation" | "script" = "automation";
render() {
if (this.domain === "script") {
return this._localize(
return this.hass.localize(
"ui.panel.config.automation.trace.messages.not_all_entries_are_related_script_note"
);
}
return this._localize(
return this.hass.localize(
"ui.panel.config.automation.trace.messages.not_all_entries_are_related_automation_note"
);
}
+6 -9
View File
@@ -1,19 +1,16 @@
import { consume, type ContextType } from "@lit/context";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import type { BasePerson } from "../../data/person";
import { computeUserInitials } from "../../data/user";
import { connectionContext } from "../../data/context";
import type { HomeAssistant } from "../../types";
@customElement("ha-person-badge")
class PersonBadge extends LitElement {
@property({ attribute: false }) public person?: BasePerson;
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consume({ context: connectionContext, subscribe: true })
private _connection?: ContextType<typeof connectionContext>;
@property({ attribute: false }) public person?: BasePerson;
protected render() {
if (!this.person) {
@@ -22,10 +19,10 @@ class PersonBadge extends LitElement {
const picture = this.person.picture;
if (picture && this._connection) {
if (picture) {
return html`<div
style=${styleMap({
backgroundImage: `url(${this._connection.hassUrl(picture)})`,
backgroundImage: `url(${this.hass.hassUrl(picture)})`,
})}
class="picture"
></div>`;
+33 -36
View File
@@ -1,62 +1,57 @@
import type { HassEntities, HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { consume, type ContextType } from "@lit/context";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { consumeEntityState } from "../../common/decorators/consume-context-entry";
import type { User } from "../../data/user";
import { computeUserInitials } from "../../data/user";
import { connectionContext, statesContext } from "../../data/context";
import type { CurrentUser } from "../../types";
import type { CurrentUser, HomeAssistant } from "../../types";
@customElement("ha-user-badge")
class UserBadge extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public user?: User | CurrentUser;
@state()
@consume({ context: connectionContext, subscribe: true })
private _connection?: ContextType<typeof connectionContext>;
@state() private _personPicture?: string;
@state()
@consume({ context: statesContext, subscribe: true })
private _states?: HassEntities;
private _personEntityId?: string;
@state() private _personEntityId?: string;
@state()
@consumeEntityState({ entityIdPath: ["_personEntityId"] })
private _personState?: HassEntity;
public willUpdate(changedProps: PropertyValues) {
public willUpdate(changedProps: PropertyValues<this>) {
super.willUpdate(changedProps);
// Re-scan for the user's person entity when the user changes, or when the
// states change while we don't have a (still-present) person entity. Once
// resolved, `_personState` keeps the picture up to date via
// `consumeEntityState`, so there's no need to rescan on every state update.
if (changedProps.has("user")) {
this._getPersonPicture();
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (
changedProps.has("user") ||
(changedProps.has("_states") &&
(!this._personEntityId || !this._states?.[this._personEntityId]))
this._personEntityId &&
oldHass &&
this.hass.states[this._personEntityId] !==
oldHass.states[this._personEntityId]
) {
this._updatePersonEntityId();
const entityState = this.hass.states[this._personEntityId];
if (entityState) {
this._personPicture = entityState.attributes.entity_picture;
} else {
this._getPersonPicture();
}
} else if (!this._personEntityId && oldHass) {
this._getPersonPicture();
}
}
protected render() {
if (!this.user) {
if (!this.hass || !this.user) {
return nothing;
}
const picture =
this._personEntityId &&
(this._personState?.attributes.entity_picture as string | undefined);
const picture = this._personPicture;
if (picture && this._connection) {
if (picture) {
return html`<div
style=${styleMap({
backgroundImage: `url(${this._connection.hassUrl(picture)})`,
backgroundImage: `url(${this.hass.hassUrl(picture)})`,
})}
class="picture"
></div>`;
@@ -69,18 +64,20 @@ class UserBadge extends LitElement {
</div>`;
}
private _updatePersonEntityId() {
private _getPersonPicture() {
this._personEntityId = undefined;
if (!this.user || !this._states) {
this._personPicture = undefined;
if (!this.hass || !this.user) {
return;
}
for (const entity of Object.values(this._states)) {
for (const entity of Object.values(this.hass.states)) {
if (
entity.attributes.user_id === this.user.id &&
computeStateDomain(entity) === "person"
) {
this._personEntityId = entity.entity_id;
return;
this._personPicture = entity.attributes.entity_picture;
break;
}
}
}
+10 -2
View File
@@ -64,7 +64,11 @@ class HaUserPicker extends LitElement {
}
return html`
<ha-user-badge slot="start" .user=${user}></ha-user-badge>
<ha-user-badge
slot="start"
.hass=${this.hass}
.user=${user}
></ha-user-badge>
<span slot="headline">${user.name}</span>
`;
};
@@ -90,7 +94,11 @@ class HaUserPicker extends LitElement {
return html`
<ha-combo-box-item type="button" compact>
<ha-user-badge slot="start" .user=${item.user}></ha-user-badge>
<ha-user-badge
slot="start"
.hass=${this.hass}
.user=${item.user}
></ha-user-badge>
<span slot="headline">${item.primary}</span>
</ha-combo-box-item>
`;
+7 -20
View File
@@ -15,33 +15,20 @@ 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"],
options?: GetAreasOptions
includeDomains?: string[],
excludeDomains?: string[],
includeDeviceClasses?: string[],
deviceFilter?: HaDevicePickerDeviceFilterFunc,
entityFilter?: HaEntityPickerEntityFilterFunc,
excludeAreas?: string[],
idPrefix = ""
): PickerComboBoxItem[] => {
const {
includeDomains,
excludeDomains,
includeDeviceClasses,
deviceFilter,
entityFilter,
excludeAreas,
idPrefix = "",
} = options ?? {};
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
let inputDevices: DeviceRegistryEntry[] | undefined;
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
-35
View File
@@ -1,6 +1,5 @@
import { createContext } from "@lit/context";
import type { HassConfig } from "home-assistant-js-websocket";
import type { HASSDomEvent } from "../../common/dom/fire_event";
import type {
HomeAssistant,
HomeAssistantApi,
@@ -11,12 +10,10 @@ import type {
HomeAssistantRegistries,
HomeAssistantUI,
} from "../../types";
import type { RelatedIdSets } from "../../common/search/related-context";
import type { ConfigEntry } from "../config_entries";
import type { EntityRegistryEntry } from "../entity/entity_registry";
import type { DomainManifestLookup } from "../integration";
import type { LabelRegistryEntry } from "../label/label_registry";
import type { ItemType } from "../search";
/**
* Entity, device, area, and floor registries
@@ -97,11 +94,6 @@ export const areasContext = createContext<HomeAssistant["areas"]>("areas");
*/
export const floorsContext = createContext<HomeAssistant["floors"]>("floors");
/**
* Whether the main Home Assistant viewport is using the narrow layout.
*/
export const narrowViewportContext = createContext<boolean>("narrowViewport");
// #region lazy-contexts
/**
@@ -170,30 +162,3 @@ export const panelsContext = createContext<HomeAssistant["panels"]>("panels");
export const authContext = createContext<HomeAssistant["auth"]>("auth");
// #endregion deprecated-contexts
// #region related-context
export interface RelatedContextItem {
itemType: ItemType;
itemId: string;
}
/**
* Resolved related entities/devices/areas for the current page context.
* Set by `RelatedContextProvider` when a page fires `hass-related-context`.
* Cleared on navigation.
*/
export const relatedContext = createContext<RelatedIdSets | undefined>(
"related"
);
declare global {
interface HASSDomEvents {
"hass-related-context": RelatedContextItem | undefined;
}
interface HTMLElementEventMap {
"hass-related-context": HASSDomEvent<RelatedContextItem | undefined>;
}
}
// #endregion related-context
+8 -23
View File
@@ -32,17 +32,6 @@ 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"],
@@ -107,19 +96,15 @@ export const deviceComboBoxKeys: FuseWeightedKey[] = [
export const getDevices = (
hass: HomeAssistant,
configEntryLookup: Record<string, ConfigEntry>,
options?: GetDevicesOptions
includeDomains?: string[],
excludeDomains?: string[],
includeDeviceClasses?: string[],
deviceFilter?: HaDevicePickerDeviceFilterFunc,
entityFilter?: HaEntityPickerEntityFilterFunc,
excludeDevices?: string[],
value?: string,
idPrefix = ""
): DevicePickerItem[] => {
const {
includeDomains,
excludeDomains,
includeDeviceClasses,
deviceFilter,
entityFilter,
excludeDevices,
value,
idPrefix = "",
} = options ?? {};
const devices = Object.values(hass.devices);
const entities = Object.values(hass.entities);
+1 -16
View File
@@ -222,12 +222,6 @@ export interface EnergyPreferences {
device_consumption_water: DeviceConsumptionEnergyPreference[];
}
export const EMPTY_PREFERENCES: EnergyPreferences = {
energy_sources: [],
device_consumption: [],
device_consumption_water: [],
};
export interface EnergyInfo {
cost_sensors: Record<string, string>;
solar_forecast_domains: string[];
@@ -808,16 +802,7 @@ export const getEnergyDataCollection = (
if (!collection.prefs) {
// This will raise if not found.
// Detect by checking `e.code === "not_found"
try {
collection.prefs = await getEnergyPreferences(hass);
} catch (err: any) {
if (err.code === "not_found") {
return {
prefs: EMPTY_PREFERENCES,
} as EnergyData;
}
throw err;
}
collection.prefs = await getEnergyPreferences(hass);
}
scheduleHourlyRefresh(collection);
+9 -25
View File
@@ -41,34 +41,18 @@ 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,
options?: GetEntitiesOptions
includeDomains?: string[],
excludeDomains?: string[],
entityFilter?: HaEntityPickerEntityFilterFunc,
includeDeviceClasses?: string[],
includeUnitOfMeasurement?: string[],
includeEntities?: string[],
excludeEntities?: string[],
value?: string,
idPrefix = ""
): EntityComboBoxItem[] => {
const {
includeDomains,
excludeDomains,
entityFilter,
includeDeviceClasses,
includeUnitOfMeasurement,
includeEntities,
excludeEntities,
value,
idPrefix = "",
} = options ?? {};
let items: EntityComboBoxItem[];
let entityIds = Object.keys(hass.states);
+2 -2
View File
@@ -36,11 +36,11 @@ export type ItemType =
| "script_blueprint";
export const findRelated = (
hass: Pick<HomeAssistant, "callWS">,
hass: HomeAssistant,
itemType: ItemType,
itemId: string
): Promise<RelatedResult> =>
hass.callWS<RelatedResult>({
hass.callWS({
type: "search/related",
item_type: itemType,
item_id: itemId,
-18
View File
@@ -87,19 +87,6 @@ const HOME_ASSISTANT_CORE_TITLE = "Home Assistant Core";
const HOME_ASSISTANT_SUPERVISOR_TITLE = "Home Assistant Supervisor";
const HOME_ASSISTANT_OS_TITLE = "Home Assistant Operating System";
// The hassio integration sets these as hard-coded `_attr_title` on the Core,
// Operating System, and Supervisor update entities. They are not translated,
// so a title comparison is the reliable way to identify them without depending
// on the (lazily-fetched) entity sources.
export const isSystemUpdate = (entity: UpdateEntity): boolean => {
const title = entity.attributes.title || "";
return (
title === HOME_ASSISTANT_CORE_TITLE ||
title === HOME_ASSISTANT_OS_TITLE ||
title === HOME_ASSISTANT_SUPERVISOR_TITLE
);
};
export const filterUpdateEntities = (
entities: HassEntities,
language?: string
@@ -146,11 +133,6 @@ export const filterUpdateEntitiesParameterized = (
return updateCanInstall(entity, showSkipped);
});
export const installUpdates = (hass: HomeAssistant, entityIds: string[]) =>
hass.callService("update", "install", {
entity_id: entityIds,
});
export const checkForEntityUpdates = async (
element: HTMLElement,
hass: HomeAssistant
@@ -54,12 +54,14 @@ 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,6 +23,7 @@ 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,6 +36,7 @@ class MoreInfoSun extends LitElement {
)}</span
>
<ha-relative-time
.hass=${this.hass}
.datetime=${item === "ris" ? risingDate : settingDate}
></ha-relative-time>
</div>
@@ -201,6 +201,7 @@ 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>
@@ -212,6 +213,7 @@ class MoreInfoWeather extends LitElement {
)}:
</span>
<ha-relative-time
.hass=${this.hass}
.datetime=${this.stateObj.last_changed}
capitalize
></ha-relative-time>
@@ -223,6 +225,7 @@ class MoreInfoWeather extends LitElement {
)}:
</span>
<ha-relative-time
.hass=${this.hass}
.datetime=${this.stateObj.last_updated}
capitalize
></ha-relative-time>
@@ -30,6 +30,7 @@ export class HuiPersistentNotificationItem extends LitElement {
<span>
<ha-relative-time
id="relative-time"
.hass=${this.hass}
.datetime=${this.notification.created_at}
capitalize
></ha-relative-time>
+64 -27
View File
@@ -1,5 +1,4 @@
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";
@@ -48,9 +47,7 @@ import {
type ActionCommandComboBoxItem,
type NavigationComboBoxItem,
} from "../../data/quick_bar";
import type { RelatedIdSets } from "../../common/search/related-context";
import { sortRelatedFirst } from "../../common/search/related-context";
import { relatedContext } from "../../data/context";
import type { RelatedResult } from "../../data/search";
import {
multiTermSortedSearch,
type FuseWeightedKey,
@@ -73,10 +70,6 @@ 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;
@@ -87,6 +80,8 @@ export class QuickBar extends LitElement {
@state() private _opened = false;
@state() private _relatedResult?: RelatedResult;
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
private get _showEntityId() {
@@ -113,6 +108,8 @@ 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;
}
@@ -435,7 +432,7 @@ export class QuickBar extends LitElement {
this._selectedSection = section as QuickBarSection | undefined;
return this._getItemsMemoized(
this._configEntryLookup,
this._relatedIdSets,
this._relatedResult,
searchString,
this._selectedSection
);
@@ -444,11 +441,12 @@ export class QuickBar extends LitElement {
private _getItemsMemoized = memoizeOne(
(
configEntryLookup: Record<string, ConfigEntry>,
relatedIdSets: RelatedIdSets | undefined,
relatedResult: RelatedResult | undefined,
filter?: string,
section?: QuickBarSection
) => {
const items: (string | PickerComboBoxItem)[] = [];
const relatedIdSets = this._getRelatedIdSets(relatedResult);
if (!section || section === "navigate") {
let navigateItems = this._generateNavigationCommandsMemoized(
@@ -500,7 +498,7 @@ export class QuickBar extends LitElement {
let entityItems = this._getEntitiesMemoized(this.hass);
// Mark related items
if (relatedIdSets?.entities.size) {
if (relatedIdSets.entities.size > 0) {
entityItems = entityItems.map((item) => ({
...item,
isRelated: relatedIdSets.entities.has(
@@ -510,7 +508,7 @@ export class QuickBar extends LitElement {
}
if (filter) {
entityItems = sortRelatedFirst(
entityItems = this._sortRelatedFirst(
this._filterGroup(
"entity",
entityItems,
@@ -539,7 +537,7 @@ export class QuickBar extends LitElement {
);
// Mark related items
if (relatedIdSets?.devices.size) {
if (relatedIdSets.devices.size > 0) {
deviceItems = deviceItems.map((item) => {
const deviceId = item.id.split(SEPARATOR)[1];
return {
@@ -550,7 +548,7 @@ export class QuickBar extends LitElement {
}
if (filter) {
deviceItems = sortRelatedFirst(
deviceItems = this._sortRelatedFirst(
this._filterGroup("device", deviceItems, filter, deviceComboBoxKeys)
);
} else {
@@ -571,7 +569,7 @@ export class QuickBar extends LitElement {
let areaItems = this._getAreasMemoized(this.hass);
// Mark related items
if (relatedIdSets?.areas.size) {
if (relatedIdSets.areas.size > 0) {
areaItems = areaItems.map((item) => {
const areaId = item.id.split(SEPARATOR)[1];
return {
@@ -582,7 +580,7 @@ export class QuickBar extends LitElement {
}
if (filter) {
areaItems = sortRelatedFirst(
areaItems = this._sortRelatedFirst(
this._filterGroup("area", areaItems, filter, areaComboBoxKeys)
);
} else {
@@ -603,13 +601,41 @@ 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, { idPrefix: `entity${SEPARATOR}` })
getEntities(
hass,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
`entity${SEPARATOR}`
)
);
private _getDevicesMemoized = memoizeOne(
(hass: HomeAssistant, configEntryLookup: Record<string, ConfigEntry>) =>
getDevices(hass, configEntryLookup, { idPrefix: `device${SEPARATOR}` })
getDevices(
hass,
configEntryLookup,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
`device${SEPARATOR}`
)
);
private _getAreasMemoized = memoizeOne((hass: HomeAssistant) =>
@@ -619,9 +645,13 @@ export class QuickBar extends LitElement {
hass.devices,
hass.entities,
hass.states,
{
idPrefix: `area${SEPARATOR}`,
}
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
`area${SEPARATOR}`
)
);
@@ -675,13 +705,10 @@ export class QuickBar extends LitElement {
);
}
private _sortBySortingLabel = (
entityA: PickerComboBoxItem,
entityB: PickerComboBoxItem
) =>
private _sortBySortingLabel = (entityA, entityB) =>
caseInsensitiveStringCompare(
entityA.sorting_label!,
entityB.sorting_label!,
(entityA as PickerComboBoxItem).sorting_label!,
(entityB as PickerComboBoxItem).sorting_label!,
this.hass.locale.language
);
@@ -692,6 +719,16 @@ 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,4 +1,5 @@
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";
@@ -9,10 +10,17 @@ 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
View File
@@ -33,6 +33,7 @@ class HassErrorScreen extends LitElement {
`
: html`
<ha-icon-button-arrow-prev
.hass=${this.hass}
@click=${this._handleBack}
></ha-icon-button-arrow-prev>
`}
+1
View File
@@ -35,6 +35,7 @@ class HassLoadingScreen extends LitElement {
`
: html`
<ha-icon-button-arrow-prev
.hass=${this.hass}
@click=${this._handleBack}
></ha-icon-button-arrow-prev>
`}
+2
View File
@@ -43,10 +43,12 @@ class HassSubpage extends LitElement {
? html`
<ha-icon-button-arrow-prev
href=${this.backPath}
.hass=${this.hass}
></ha-icon-button-arrow-prev>
`
: html`
<ha-icon-button-arrow-prev
.hass=${this.hass}
@click=${this._backTapped}
></ha-icon-button-arrow-prev>
`}
@@ -391,6 +391,7 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
<hass-tabs-subpage
.hass=${this.hass}
.localizeFunc=${this.localizeFunc}
.narrow=${this.narrow}
.isWide=${this.isWide}
.backPath=${this.backPath}
.backCallback=${this.backCallback}
+10 -14
View File
@@ -1,4 +1,3 @@
import { consume } from "@lit/context";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import {
@@ -19,7 +18,6 @@ import "../components/ha-icon-button-arrow-prev";
import "../components/ha-menu-button";
import "../components/ha-svg-icon";
import "../components/ha-tab";
import { narrowViewportContext } from "../data/context";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant, Route } from "../types";
@@ -61,9 +59,7 @@ export class HassTabsSubpage extends LitElement {
@property({ attribute: false }) public tabs!: PageNavigation[];
@state()
@consume({ context: narrowViewportContext, subscribe: true })
private _narrow = false;
@property({ type: Boolean, reflect: true }) public narrow = false;
@property({ type: Boolean, reflect: true, attribute: "is-wide" })
public isWide = false;
@@ -120,7 +116,7 @@ export class HassTabsSubpage extends LitElement {
<a href=${page.path} @click=${this._tabClicked}>
<ha-tab
.active=${page.path === activeTab?.path}
.narrow=${this._narrow}
.narrow=${this.narrow}
.name=${page.translationKey
? localizeFunc(page.translationKey)
: page.name}
@@ -139,8 +135,6 @@ export class HassTabsSubpage extends LitElement {
);
public willUpdate(changedProperties: PropertyValues<this>) {
this.toggleAttribute("narrow", this._narrow);
if (changedProperties.has("route")) {
const currentPath = `${this.route.prefix}${this.route.path}`;
this._activeTab = this.tabs.find((tab) =>
@@ -157,37 +151,39 @@ export class HassTabsSubpage extends LitElement {
this.hass.config.components,
this.hass.language,
this.hass.userData,
this._narrow,
this.narrow,
this.localizeFunc || this.hass.localize
);
return html`
<div class="toolbar ${classMap({ narrow: this._narrow })}">
<div class="toolbar ${classMap({ narrow: this.narrow })}">
<slot name="toolbar">
<div class="toolbar-content">
${this.mainPage || (!this.backPath && history.state?.root)
? html`
<ha-menu-button
.hass=${this.hass}
.narrow=${this._narrow}
.narrow=${this.narrow}
></ha-menu-button>
`
: this.backPath
? html`
<ha-icon-button-arrow-prev
.href=${this.backPath}
.hass=${this.hass}
></ha-icon-button-arrow-prev>
`
: html`
<ha-icon-button-arrow-prev
.hass=${this.hass}
@click=${this._backTapped}
></ha-icon-button-arrow-prev>
`}
${this._narrow || !this.showTabs
${this.narrow || !this.showTabs
? html`<div class="main-title">
<slot name="header">${!this.showTabs ? tabs[0] : ""}</slot>
</div>`
: ""}
${this.showTabs && !this._narrow
${this.showTabs && !this.narrow
? html`<div id="tabbar">${tabs}</div>`
: ""}
<div id="toolbar-icon">
@@ -195,7 +191,7 @@ export class HassTabsSubpage extends LitElement {
</div>
</div>
</slot>
${this.showTabs && this._narrow
${this.showTabs && this.narrow
? html`<div id="tabbar" class="bottom-bar">${tabs}</div>`
: ""}
</div>
+1 -11
View File
@@ -1,4 +1,3 @@
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";
@@ -8,7 +7,6 @@ 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";
@@ -38,11 +36,6 @@ 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) => {
@@ -73,6 +66,7 @@ export class HomeAssistantMain extends LitElement {
></ha-sidebar>
${isPanelReady
? html`<partial-panel-resolver
.narrow=${this.narrow}
.hass=${this.hass}
.route=${this.route}
slot="appContent"
@@ -127,10 +121,6 @@ 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;
}
+4 -8
View File
@@ -1,14 +1,12 @@
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, state } from "lit/decorators";
import { customElement, property } 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";
@@ -45,9 +43,7 @@ const COMPONENTS = {
class PartialPanelResolver extends HassRouterPage {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consume({ context: narrowViewportContext, subscribe: true })
private _narrow = false;
@property({ type: Boolean }) public narrow = false;
private _waitForStart = false;
@@ -96,7 +92,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;
}
@@ -104,7 +100,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];
}
+98 -29
View File
@@ -1,6 +1,7 @@
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";
@@ -14,7 +15,6 @@ 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,36 +97,38 @@ class PanelClimate extends LitElement {
protected render() {
return html`
<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}
></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
<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}
.lovelace=${this._lovelace}
.index=${this._viewIndex}
></hui-view
></hui-view-container>
`
: nothing}
</ha-top-app-bar-fixed>
></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
.hass=${this.hass}
.narrow=${this.narrow}
.lovelace=${this._lovelace}
.index=${this._viewIndex}
></hui-view
></hui-view-container>
`
: nothing}
`;
}
@@ -167,11 +169,78 @@ 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%;
@@ -135,6 +135,7 @@ class HaConfigAppDashboard extends LitElement {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${route}
.tabs=${addonTabs}
back-path=${this._fromStore ? "/config/apps/available" : "/config/apps"}
@@ -82,6 +82,8 @@ export class HaConfigAreasDashboard extends LitElement {
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
@state() private _hierarchy?: AreasFloorHierarchy;
@@ -167,6 +169,7 @@ export class HaConfigAreasDashboard extends LitElement {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.isWide=${this.isWide}
.backPath=${this._searchParms.has("historyBack")
? undefined
@@ -20,10 +20,6 @@ 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";
@@ -44,7 +40,7 @@ import {
} from "../../../../data/area_floor_picker";
import { CONDITION_BUILDING_BLOCKS_GROUP } from "../../../../data/condition";
import type { ConfigEntry } from "../../../../data/config_entries";
import { labelsContext, relatedContext } from "../../../../data/context";
import { labelsContext } from "../../../../data/context";
import {
deviceComboBoxKeys,
getDevices,
@@ -133,10 +129,6 @@ export class HaAutomationAddSearch extends LitElement {
| "condition"
| "action";
@state()
@consume({ context: relatedContext, subscribe: true })
private _relatedIdSets?: RelatedIdSets;
@state() private _searchSectionTitle?: string;
@state() private _selectedSearchSection?: SearchSection;
@@ -149,19 +141,11 @@ export class HaAutomationAddSearch extends LitElement {
@query("lit-virtualizer") private _virtualizerElement?: LitVirtualizer;
private _getDevicesMemoized = memoizeOne(
(
hass: HomeAssistant,
configEntryLookup: Record<string, ConfigEntry>,
idPrefix: string
) => getDevices(hass, configEntryLookup, { idPrefix })
);
private _getDevicesMemoized = memoizeOne(getDevices);
private _getLabelsMemoized = memoizeOne(getLabels);
private _getEntitiesMemoized = memoizeOne(
(hass: HomeAssistant, idPrefix: string) => getEntities(hass, { idPrefix })
);
private _getEntitiesMemoized = memoizeOne(getEntities);
private _getAreasAndFloorsMemoized = memoizeOne(getAreasAndFloors);
@@ -210,8 +194,7 @@ export class HaAutomationAddSearch extends LitElement {
this.configEntryLookup,
this.items,
this.newTriggersAndConditions,
this._selectedSearchSection,
this._relatedIdSets
this._selectedSearchSection
);
let emptySearchTranslation: string | undefined;
@@ -504,8 +487,7 @@ export class HaAutomationAddSearch extends LitElement {
configEntryLookup: Record<string, ConfigEntry>,
automationItems: AddAutomationElementListItem[],
newTriggersAndConditions: boolean,
selectedSection?: SearchSection,
relatedIdSets?: RelatedIdSets
selectedSection?: SearchSection
) => {
const resultItems: (
| string
@@ -575,29 +557,24 @@ 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 = sortRelatedFirst(
this._filterGroup(
"entity",
entityItems,
searchTerm,
entityComboBoxKeys
)
entityItems = this._filterGroup(
"entity",
entityItems,
searchTerm,
entityComboBoxKeys
) as EntityComboBoxItem[];
} else if (relatedIdSets?.entities.size) {
entityItems = sortRelatedFirst(entityItems) as EntityComboBoxItem[];
}
if (!selectedSection && entityItems.length) {
@@ -614,29 +591,23 @@ 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 = sortRelatedFirst(
this._filterGroup(
"device",
deviceItems,
searchTerm,
deviceComboBoxKeys
)
deviceItems = this._filterGroup(
"device",
deviceItems,
searchTerm,
deviceComboBoxKeys
);
} else if (relatedIdSets?.devices.size) {
deviceItems = sortRelatedFirst(deviceItems);
}
if (!selectedSection && deviceItems.length) {
@@ -668,31 +639,13 @@ 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 = sortRelatedFirst(
this._filterGroup(
"area",
areasAndFloors,
searchTerm,
areaFloorComboBoxKeys,
false
)
) as FloorComboBoxItem[];
} else if (relatedIdSets?.areas.size) {
areasAndFloors = sortRelatedFirst(
areasAndFloors
areasAndFloors = this._filterGroup(
"area",
areasAndFloors,
searchTerm,
areaFloorComboBoxKeys,
false
) as FloorComboBoxItem[];
}
@@ -1,14 +1,8 @@
import { consume } from "@lit/context";
import type {
CSSResult,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import type { CSSResult, LitElement, 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";
@@ -142,21 +136,6 @@ 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}>
@@ -217,7 +217,7 @@ export class CloudRegister extends LitElement {
try {
await cloudRegister(this.hass, email, password);
this._verificationEmailSent(email, "account_created");
this._verificationEmailSent(email);
} catch (err: any) {
this._password = "";
this._requestInProgress = false;
@@ -238,18 +238,15 @@ 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, "verification_email_sent");
this._verificationEmailSent(username);
} 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
@@ -261,16 +258,13 @@ export class CloudRegister extends LitElement {
await doResend(email);
}
private _verificationEmailSent(
email: string,
messageKey: "account_created" | "verification_email_sent"
) {
private _verificationEmailSent(email: string) {
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.${messageKey}`
"ui.panel.config.cloud.register.account_created"
),
});
}
@@ -11,15 +11,10 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import type { EntitySources } from "../../../data/entity/entity_sources";
import { fetchEntitySourcesWithCache } from "../../../data/entity/entity_sources";
import { extractApiErrorMessage } from "../../../data/hassio/common";
import type {
HassioSupervisorInfo,
@@ -30,17 +25,9 @@ import {
reloadSupervisor,
setSupervisorOption,
} from "../../../data/hassio/supervisor";
import { domainToName } from "../../../data/integration";
import type { UpdateEntity } from "../../../data/update";
import {
checkForEntityUpdates,
filterUpdateEntities,
filterUpdateEntitiesParameterized,
installUpdates,
isSystemUpdate,
latestVersionIsSkipped,
updateIsInstalling,
UpdateEntityFeature,
} from "../../../data/update";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-subpage";
@@ -48,17 +35,6 @@ import type { HomeAssistant } from "../../../types";
import "../dashboard/ha-config-updates";
import { showJoinBetaDialog } from "./updates/show-dialog-join-beta";
interface UpdateGroup {
key: string;
title: string;
entities: UpdateEntity[];
showUpdateAll: boolean;
}
const SYSTEM_KEY = "__system__";
const APPS_KEY = "__apps__";
const INTEGRATIONS_KEY = "__integrations__";
@customElement("ha-config-section-updates")
class HaConfigSectionUpdates extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -71,86 +47,23 @@ class HaConfigSectionUpdates extends LitElement {
@state() private _supervisorInfo?: HassioSupervisorInfo;
@state() private _entitySources?: EntitySources;
@state() private _loadedIntegrationTitles = new Set<string>();
protected firstUpdated(changedProps: PropertyValues<this>) {
super.firstUpdated(changedProps);
if (isComponentLoaded(this.hass.config, "hassio")) {
this._refreshSupervisorInfo();
}
this._loadEntitySources();
}
private async _loadEntitySources() {
try {
this._entitySources = await fetchEntitySourcesWithCache(this.hass);
} catch (_err) {
// Non-fatal: grouping falls back to entity registry platform lookup.
}
}
protected updated(changedProps: PropertyValues<this>) {
super.updated(changedProps);
if (changedProps.has("hass")) {
this._loadIntegrationTitles();
}
}
private _collectUpdateDomains = memoizeOne(
(states: HassEntities, entities: HomeAssistant["entities"]) => {
const domains = new Set<string>();
for (const entity of Object.values(states)) {
if (!entity.entity_id.startsWith("update.")) continue;
const platform = entities[entity.entity_id]?.platform;
if (platform) {
domains.add(platform);
}
}
return domains;
}
);
private async _loadIntegrationTitles() {
const domains = this._collectUpdateDomains(
this.hass.states,
this.hass.entities
);
const toLoad: string[] = [];
for (const domain of domains) {
if (!this._loadedIntegrationTitles.has(domain)) {
toLoad.push(domain);
}
}
if (!toLoad.length) return;
toLoad.forEach((d) => this._loadedIntegrationTitles.add(d));
await this.hass.loadBackendTranslation("title", toLoad);
this.requestUpdate();
}
protected render(): TemplateResult {
const installableUpdates = this._filterInstallableUpdateEntities(
this.hass.states
const canInstallUpdates = this._filterInstallableUpdateEntities(
this.hass.states,
this._showSkipped
);
const notInstallableUpdates = this._filterNotInstallableUpdateEntities(
this.hass.states,
this._showSkipped
);
const skippedUpdates = this._filterSkippedUpdateEntities(
this.hass.states,
this._showSkipped
);
const groups = this._groupUpdates(
installableUpdates,
this._entitySources,
this.hass.localize,
this.hass.entities,
this.hass.locale.language
);
return html`
<hass-subpage
@@ -205,60 +118,19 @@ class HaConfigSectionUpdates extends LitElement {
</ha-dropdown>
</div>
<div class="content">
${groups.map(
(group) => html`
<ha-card outlined>
<div class="card-content">
<div class="card-header">
<div class="title" role="heading" aria-level="2">
${group.title}
</div>
${group.showUpdateAll
? html`
<ha-button
appearance="plain"
size="small"
.group=${group}
.disabled=${group.entities.every((entity) =>
updateIsInstalling(entity)
)}
@click=${this._updateAll}
>
${this.hass.localize(
"ui.panel.config.updates.update_all"
)}
</ha-button>
`
: nothing}
</div>
<ha-config-updates
.hass=${this.hass}
.narrow=${this.narrow}
.updateEntities=${group.entities}
showAll
></ha-config-updates>
</div>
</ha-card>
`
)}
${skippedUpdates.length
${canInstallUpdates.length
? html`
<ha-card outlined>
<div class="card-content">
<div class="card-header">
<div class="title" role="heading" aria-level="2">
${this.hass.localize(
"ui.panel.config.updates.title_skipped",
{
count: skippedUpdates.length,
}
)}
</div>
<div class="title" role="heading" aria-level="2">
${this.hass.localize("ui.panel.config.updates.title", {
count: canInstallUpdates.length,
})}
</div>
<ha-config-updates
.hass=${this.hass}
.narrow=${this.narrow}
.updateEntities=${skippedUpdates}
.updateEntities=${canInstallUpdates}
showAll
></ha-config-updates>
</div>
@@ -269,15 +141,13 @@ class HaConfigSectionUpdates extends LitElement {
? html`
<ha-card outlined>
<div class="card-content">
<div class="card-header">
<div class="title" role="heading" aria-level="2">
${this.hass.localize(
"ui.panel.config.updates.title_not_installable",
{
count: notInstallableUpdates.length,
}
)}
</div>
<div class="title" role="heading" aria-level="2">
${this.hass.localize(
"ui.panel.config.updates.title_not_installable",
{
count: notInstallableUpdates.length,
}
)}
</div>
<ha-config-updates
.hass=${this.hass}
@@ -289,7 +159,7 @@ class HaConfigSectionUpdates extends LitElement {
</ha-card>
`
: nothing}
${groups.length + notInstallableUpdates.length + skippedUpdates.length
${canInstallUpdates.length + notInstallableUpdates.length
? nothing
: html`
<ha-card outlined>
@@ -341,28 +211,9 @@ class HaConfigSectionUpdates extends LitElement {
checkForEntityUpdates(this, this.hass);
}
private async _updateAll(ev: Event) {
const group = (ev.currentTarget as any).group as UpdateGroup;
const entityIds = group.entities
.filter((entity) => !updateIsInstalling(entity))
.map((entity) => entity.entity_id);
if (!entityIds.length) {
return;
}
try {
await installUpdates(this.hass, entityIds);
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize("ui.panel.config.updates.update_all_failed"),
text: extractApiErrorMessage(err),
warning: true,
});
}
}
private _filterInstallableUpdateEntities = memoizeOne(
(entities: HassEntities) =>
filterUpdateEntitiesParameterized(entities, false, false)
(entities: HassEntities, showSkipped: boolean) =>
filterUpdateEntitiesParameterized(entities, showSkipped, false)
);
private _filterNotInstallableUpdateEntities = memoizeOne(
@@ -370,111 +221,6 @@ class HaConfigSectionUpdates extends LitElement {
filterUpdateEntitiesParameterized(entities, showSkipped, true)
);
private _filterSkippedUpdateEntities = memoizeOne(
(entities: HassEntities, showSkipped: boolean): UpdateEntity[] => {
if (!showSkipped) {
return [];
}
return filterUpdateEntities(entities).filter(
(entity) =>
latestVersionIsSkipped(entity) &&
supportsFeature(entity, UpdateEntityFeature.INSTALL)
);
}
);
private _groupUpdates = memoizeOne(
(
entities: UpdateEntity[],
entitySources: EntitySources | undefined,
localize: HomeAssistant["localize"],
entityRegistry: HomeAssistant["entities"],
language: string
): UpdateGroup[] => {
if (!entities.length) {
return [];
}
const systemEntities: UpdateEntity[] = [];
const appEntities: UpdateEntity[] = [];
const byDomain = new Map<string, UpdateEntity[]>();
const otherIntegrationEntities: UpdateEntity[] = [];
for (const entity of entities) {
if (isSystemUpdate(entity)) {
systemEntities.push(entity);
continue;
}
const domain =
entitySources?.[entity.entity_id]?.domain ??
entityRegistry[entity.entity_id]?.platform;
if (domain === "hassio") {
appEntities.push(entity);
continue;
}
if (!domain) {
otherIntegrationEntities.push(entity);
continue;
}
if (!byDomain.has(domain)) {
byDomain.set(domain, []);
}
byDomain.get(domain)!.push(entity);
}
const multiInstanceGroups: UpdateGroup[] = [];
byDomain.forEach((entries, domain) => {
if (entries.length >= 2) {
multiInstanceGroups.push({
key: domain,
title: domainToName(localize, domain),
entities: entries,
showUpdateAll: true,
});
} else {
otherIntegrationEntities.push(...entries);
}
});
multiInstanceGroups.sort((a, b) =>
caseInsensitiveStringCompare(a.title, b.title, language)
);
const groups: UpdateGroup[] = [];
if (systemEntities.length) {
groups.push({
key: SYSTEM_KEY,
title: localize("ui.panel.config.updates.group_system"),
entities: systemEntities,
showUpdateAll: false,
});
}
groups.push(...multiInstanceGroups);
if (otherIntegrationEntities.length) {
groups.push({
key: INTEGRATIONS_KEY,
title: localize("ui.panel.config.updates.group_integrations"),
entities: otherIntegrationEntities,
showUpdateAll: true,
});
}
if (appEntities.length) {
groups.push({
key: APPS_KEY,
title: localize("ui.panel.config.updates.group_apps"),
entities: appEntities,
showUpdateAll: true,
});
}
return groups;
}
);
static styles = css`
.content {
padding: 28px 20px 0;
@@ -501,15 +247,8 @@ class HaConfigSectionUpdates extends LitElement {
padding: 0;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--ha-space-2);
padding: var(--ha-space-4) var(--ha-space-2) 0 var(--ha-space-4);
}
.title {
padding: var(--ha-space-4) var(--ha-space-4) 0;
font-size: var(--ha-font-size-l);
}
@@ -104,6 +104,9 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
return html`
<ha-list-item-button
class=${ifDefined(
entity.attributes.skipped_version ? "skipped" : undefined
)}
.entity_id=${entity.entity_id}
.hasMeta=${!this.narrow}
@click=${this._openMoreInfo}
@@ -163,6 +166,9 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
static get styles(): CSSResultGroup[] {
return [
css`
.skipped {
background: var(--secondary-background-color);
}
ha-list-item-button {
--md-list-item-leading-icon-size: 40px;
}
@@ -36,6 +36,7 @@ class PanelDeveloperTools extends LitElement {
<div class="toolbar">
<ha-icon-button-arrow-prev
slot="navigationIcon"
.hass=${this.hass}
@click=${this._handleBack}
></ha-icon-button-arrow-prev>
<div class="main-title">
@@ -380,7 +380,7 @@ export class HaConfigDevicePage extends LitElement {
if (changedProps.has("deviceId")) {
this._findRelated();
// Broadcast device context for quick bar
fireEvent(this, "hass-related-context", {
fireEvent(this, "hass-quick-bar-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_from}
.label=${this.hass.localize(
"ui.panel.config.energy.battery.dialog.energy_out_of_battery"
)}
.excludeStatistics=${[
...(this._excludeList || []),
this._source.stat_energy_to,
]}
@value-changed=${this._statisticFromChanged}
.helper=${this.hass.localize(
"ui.panel.config.energy.battery.dialog.energy_helper_out",
{ unit: this._energy_units?.join(", ") || "" }
)}
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_into",
{ unit: this._energy_units?.join(", ") || "" }
)}
autofocus
></ha-statistic-picker>
<ha-statistic-picker
.hass=${this.hass}
.helpMissingEntityUrl=${energyStatisticHelpUrl}
.includeUnitClass=${energyUnitClasses}
.value=${this._source.stat_energy_from}
.label=${this.hass.localize(
"ui.panel.config.energy.battery.dialog.energy_out_of_battery"
)}
.excludeStatistics=${[
...(this._excludeList || []),
this._source.stat_energy_to,
]}
@value-changed=${this._statisticFromChanged}
.helper=${this.hass.localize(
"ui.panel.config.energy.battery.dialog.energy_helper_out",
{ unit: this._energy_units?.join(", ") || "" }
)}
></ha-statistic-picker>
<ha-input
@@ -118,6 +118,7 @@ class HaConfigEnergy extends LitElement {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.backPath=${this._searchParms.has("historyBack")
? undefined
: "/config/lovelace/dashboards"}
@@ -774,11 +774,7 @@ export class HaConfigEntities extends LitElement {
.checked=${selected}
.indeterminate=${partial}
></ha-checkbox>
<ha-label
.color=${label.color}
.description=${label.description}
class="text-ellipsis"
>
<ha-label .color=${label.color} .description=${label.description}>
${label.icon
? html`<ha-icon slot="icon" .icon=${label.icon}></ha-icon>`
: nothing}
@@ -1678,14 +1674,10 @@ ${rejected
ha-assist-chip {
--ha-assist-chip-container-shape: 10px;
}
ha-dropdown::part(menu) {
ha-dropdown::part(menu),
ha-dropdown::part(submenu) {
--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;
}
@@ -58,6 +58,8 @@ const DATA_SET_CONFIG: SeriesOption = {
class HaConfigHardwareOverview extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
@state() private _error?: string;
@@ -247,6 +249,7 @@ class HaConfigHardwareOverview extends SubscribeMixin(LitElement) {
<hass-tabs-subpage
back-path="/config/system"
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.tabs=${hardwareTabs(this.hass)}
>
@@ -511,6 +511,7 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.backPath=${this._searchParams.has("historyBack")
? undefined
: "/config"}
@@ -173,6 +173,7 @@ export class BluetoothAdvertisementMonitorPanel extends LitElement {
defaultHidden: false,
template: (ad) =>
html`<ha-relative-time
.hass=${this.hass}
.datetime=${ad.datetime}
capitalize
></ha-relative-time>`,
@@ -375,6 +375,7 @@ class DialogMatterAddDevice extends LitElement {
? html`
<ha-icon-button-arrow-prev
slot="headerNavigationIcon"
.hass=${this.hass}
@click=${this._back}
></ha-icon-button-arrow-prev>
`
@@ -39,6 +39,8 @@ export class HaConfigPerson extends LitElement {
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
@state() private _storageItems?: Person[];
@@ -59,6 +61,7 @@ export class HaConfigPerson extends LitElement {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
back-path="/config"
.tabs=${configSections.persons}
@@ -99,6 +102,7 @@ export class HaConfigPerson extends LitElement {
.entry=${entry}
>
<ha-person-badge
.hass=${this.hass}
.person=${entry}
slot="graphic"
></ha-person-badge>
@@ -135,6 +139,7 @@ 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>
@@ -78,6 +78,7 @@ export class StorageBreakdownChart extends LitElement {
)}
></ha-segmented-bar>`
: html`<ha-sunburst-chart
.hass=${this.hass}
.data=${this._transformToSunburstData(this.storageInfo!)}
.valueFormatter=${this._formatBytes}
></ha-sunburst-chart>`}
+1
View File
@@ -101,6 +101,7 @@ export class HaConfigTags extends SubscribeMixin(LitElement) {
template: (tag) => html`
${tag.last_scanned_datetime
? html`<ha-relative-time
.hass=${this.hass}
.datetime=${tag.last_scanned_datetime}
capitalize
></ha-relative-time>`
@@ -27,6 +27,8 @@ export class HaConfigVoiceAssistantsAssistants extends LitElement {
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
protected render() {
@@ -37,6 +39,7 @@ export class HaConfigVoiceAssistantsAssistants extends LitElement {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
back-path="/config"
.route=${this.route}
.tabs=${voiceAssistantTabs}
+1
View File
@@ -234,6 +234,7 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.narrow=${this.narrow}
.route=${this.route}
.backPath=${this._searchParms.has("historyBack")
? undefined
+1 -10
View File
@@ -14,7 +14,6 @@ import "../lovelace/hui-root";
import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container";
import { DEFAULT_POWER_COLLECTION_KEY } from "./constants";
@customElement("ha-panel-energy")
class PanelEnergy extends LitElement {
@@ -87,15 +86,7 @@ class PanelEnergy extends LitElement {
private async _setLovelace() {
const config: LovelaceConfig = await generateLovelaceDashboardStrategy(
{
strategy: {
type: "energy",
default_collection:
this.route?.path === "/now"
? DEFAULT_POWER_COLLECTION_KEY
: undefined,
},
},
{ strategy: { type: "energy" } },
this.hass
);
@@ -1,16 +1,12 @@
import { ReactiveElement } from "lit";
import { customElement } from "lit/decorators";
import {
EMPTY_PREFERENCES,
getEnergyDataCollection,
} from "../../../data/energy";
import { getEnergyDataCollection } from "../../../data/energy";
import type { EnergyPreferences } from "../../../data/energy";
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
import type { LovelaceConfig } from "../../../data/lovelace/config/types";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { LocalizeKeys } from "../../../common/translations/localize";
import type { HomeAssistant } from "../../../types";
import type { LovelaceStrategyDependency } from "../../lovelace/strategies/types";
import {
DEFAULT_ENERGY_COLLECTION_KEY,
DEFAULT_POWER_COLLECTION_KEY,
@@ -62,20 +58,23 @@ const WIZARD_VIEW = {
cards: [{ type: "custom:energy-setup-wizard-card" }],
};
const EMPTY_PREFERENCES: EnergyPreferences = {
energy_sources: [],
device_consumption: [],
device_consumption_water: [],
};
export interface EnergyDashboardStrategyConfig extends LovelaceStrategyConfig {
type: "energy";
default_collection?: string;
}
@customElement("energy-dashboard-strategy")
export class EnergyDashboardStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
static async generate(
_config: EnergyDashboardStrategyConfig,
hass: HomeAssistant
): Promise<LovelaceConfig> {
const prefs = await fetchEnergyPrefs(hass, _config.default_collection);
const prefs = await fetchEnergyPrefs(hass);
if (
!prefs ||
@@ -148,19 +147,20 @@ export class EnergyDashboardStrategy extends ReactiveElement {
}
async function fetchEnergyPrefs(
hass: HomeAssistant,
defaultCollection?: string
hass: HomeAssistant
): Promise<EnergyPreferences> {
const collection = getEnergyDataCollection(hass, {
key: defaultCollection || DEFAULT_ENERGY_COLLECTION_KEY,
});
return await new Promise<EnergyPreferences>((resolve) => {
const unsub = collection.subscribe((data) => {
unsub();
resolve(data.prefs || EMPTY_PREFERENCES);
});
key: DEFAULT_ENERGY_COLLECTION_KEY,
});
try {
await collection.refresh();
} catch (err: any) {
if (err.code === "not_found") {
return EMPTY_PREFERENCES;
}
throw err;
}
return collection.prefs || EMPTY_PREFERENCES;
}
declare global {
@@ -5,13 +5,10 @@ import { getEnergyDataCollection } from "../../../data/energy";
import type { HomeAssistant } from "../../../types";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
import type { LovelaceStrategyDependency } from "../../lovelace/strategies/types";
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../constants";
@customElement("energy-overview-view-strategy")
export class EnergyOverviewViewStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
static async generate(
_config: LovelaceStrategyConfig,
hass: HomeAssistant
@@ -12,12 +12,9 @@ import {
LARGE_SCREEN_CONDITION,
SMALL_SCREEN_CONDITION,
} from "../../lovelace/strategies/helpers/view-columns-conditions";
import type { LovelaceStrategyDependency } from "../../lovelace/strategies/types";
@customElement("energy-view-strategy")
export class EnergyViewStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
static async generate(
_config: LovelaceStrategyConfig,
hass: HomeAssistant
@@ -6,12 +6,9 @@ import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../constants";
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
import type { LovelaceStrategyDependency } from "../../lovelace/strategies/types";
@customElement("gas-view-strategy")
export class GasViewStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
static async generate(
_config: LovelaceStrategyConfig,
hass: HomeAssistant
@@ -8,12 +8,9 @@ import { DEFAULT_ENERGY_COLLECTION_KEY } from "../constants";
import { shouldShowFloorsAndAreas } from "./show-floors-and-areas";
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
import type { LovelaceStrategyDependency } from "../../lovelace/strategies/types";
@customElement("power-view-strategy")
export class PowerViewStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
static async generate(
_config: LovelaceStrategyConfig,
hass: HomeAssistant
@@ -24,9 +21,7 @@ export class PowerViewStrategy extends ReactiveElement {
const energyCollection = getEnergyDataCollection(hass, {
key: collectionKey,
});
if (!energyCollection.prefs) {
await energyCollection.refresh();
}
await energyCollection.refresh();
const prefs = energyCollection.prefs;
const hasPowerSources = prefs?.energy_sources.some((source) => {
@@ -5,14 +5,11 @@ import type { LovelaceSectionConfig } from "../../../data/lovelace/config/sectio
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../types";
import type { LovelaceStrategyDependency } from "../../lovelace/strategies/types";
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../constants";
import { shouldShowFloorsAndAreas } from "./show-floors-and-areas";
@customElement("water-view-strategy")
export class WaterViewStrategy extends ReactiveElement {
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
static async generate(
_config: LovelaceStrategyConfig,
hass: HomeAssistant
+40 -44
View File
@@ -7,6 +7,7 @@ import { styleMap } from "lit/directives/style-map";
import { atLeastVersion } from "../../common/config/version";
import { navigate } from "../../common/navigate";
import { debounce } from "../../common/util/debounce";
import { deepEqual } from "../../common/util/deep-equal";
import "../../components/ha-button";
import "../../components/ha-svg-icon";
import { updateAreaRegistryEntry } from "../../data/area/area_registry";
@@ -25,10 +26,7 @@ import { showDeviceRegistryDetailDialog } from "../config/devices/device-registr
import { showAddIntegrationDialog } from "../config/integrations/show-add-integration-dialog";
import "../lovelace/hui-root";
import type { ExtraActionItem } from "../lovelace/hui-root";
import {
checkStrategyShouldRegenerate,
generateLovelaceDashboardStrategy,
} from "../lovelace/strategies/get-strategy";
import { expandLovelaceConfigStrategies } from "../lovelace/strategies/get-strategy";
import type { Lovelace } from "../lovelace/types";
import { showEditHomeDialog } from "./dialogs/show-dialog-edit-home";
import { showNewOverviewDialog } from "./dialogs/show-dialog-new-overview";
@@ -95,37 +93,33 @@ class PanelHome extends LitElement {
return;
}
const oldHass = changedProps.get("hass") as this["hass"] | undefined;
if (!oldHass) {
return;
}
// Locale changed: regenerate to refresh translated content
if (oldHass.localize !== this.hass.localize) {
const oldHass = changedProps.get("hass") as this["hass"];
if (oldHass && oldHass.localize !== this.hass.localize) {
this._setLovelace();
return;
}
if (this.hass.config.state !== "RUNNING") {
return;
}
// Home Assistant just started: run the full setup
if (oldHass.config.state !== "RUNNING") {
this._setup();
return;
}
// A registry the strategy depends on changed: regenerate
if (
checkStrategyShouldRegenerate(
"dashboard",
this._strategyConfig.strategy,
oldHass,
this.hass
)
) {
this._debounceRegenerateStrategy();
if (oldHass && this.hass) {
// If the entity registry changed, ask the user if they want to refresh the config
if (
oldHass.entities !== this.hass.entities ||
oldHass.devices !== this.hass.devices ||
oldHass.areas !== this.hass.areas ||
oldHass.floors !== this.hass.floors ||
oldHass.panels !== this.hass.panels
) {
if (this.hass.config.state === "RUNNING") {
this._debounceRegistriesChanged();
return;
}
}
// If ha started, refresh the config
if (
this.hass.config.state === "RUNNING" &&
oldHass.config.state !== "RUNNING"
) {
this._setup();
}
}
}
@@ -150,12 +144,12 @@ class PanelHome extends LitElement {
}
}
private _debounceRegenerateStrategy = debounce(
() => this._regenerateStrategyConfig(),
private _debounceRegistriesChanged = debounce(
() => this._registriesChanged(),
200
);
private _regenerateStrategyConfig() {
private _registriesChanged = async () => {
// If on an area view that no longer exists, redirect to overview
const path = this.route?.path?.split("/")[1];
if (path?.startsWith("areas-")) {
@@ -166,7 +160,7 @@ class PanelHome extends LitElement {
}
}
this._setLovelace();
}
};
private _updateExtraActionItems() {
const path = this.route?.path?.split("/")[1];
@@ -326,8 +320,11 @@ class PanelHome extends LitElement {
});
}
private get _strategyConfig(): LovelaceDashboardStrategyConfig {
return {
private async _setLovelace() {
if (this._loadConfigPromise) {
await this._loadConfigPromise;
}
const strategyConfig: LovelaceDashboardStrategyConfig = {
strategy: {
type: "home",
favorite_entities: this._config.favorite_entities,
@@ -337,17 +334,16 @@ class PanelHome extends LitElement {
shortcuts: this._config.shortcuts,
},
};
}
private async _setLovelace() {
if (this._loadConfigPromise) {
await this._loadConfigPromise;
}
const config = await generateLovelaceDashboardStrategy(
this._strategyConfig,
const config = await expandLovelaceConfigStrategies(
strategyConfig,
this.hass
);
if (deepEqual(config, this._lovelace?.config)) {
return;
}
this._lovelace = {
config: config,
rawConfig: config,
+98 -29
View File
@@ -1,6 +1,7 @@
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";
@@ -14,7 +15,6 @@ import type { Lovelace } from "../lovelace/types";
import "../lovelace/views/hui-view";
import "../lovelace/views/hui-view-container";
import "../lovelace/views/hui-view-background";
import "../../components/ha-top-app-bar-fixed";
const LIGHT_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = {
strategy: {
@@ -97,36 +97,38 @@ class PanelLight extends LitElement {
protected render() {
return html`
<ha-top-app-bar-fixed .narrow=${this.narrow}>
${this._searchParms.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 slot="title">${this.hass.localize("panel.light")}</div>
${this._lovelace
? html`
<hui-view-container .hass=${this.hass}>
<hui-view-background .hass=${this.hass}> </hui-view-background>
<hui-view
<div class="header ${classMap({ narrow: this.narrow })}">
<div class="toolbar">
${this._searchParms.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-top-app-bar-fixed>
></ha-menu-button>
`}
<div class="main-title">${this.hass.localize("panel.light")}</div>
</div>
</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}
`;
}
@@ -167,11 +169,78 @@ class PanelLight 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%;

Some files were not shown because too many files have changed in this diff Show More