Compare commits

..

39 Commits

Author SHA1 Message Date
Bram Kragten
905435db3e Bumped version to 20260107.1 2026-01-09 23:26:47 +01:00
Yosi Levy
ea73fd3f01 Fix for volume scroll in media player (#28891) 2026-01-09 23:26:22 +01:00
Yosi Levy
e519a0203e Arrow fixes in media browser (#28890) 2026-01-09 23:26:21 +01:00
Bram Kragten
d98ee7e0b5 Add support for choose selector to initial form data (#28876)
* Add support for choose selector to initial form data

* Update compute-initial-ha-form-data.ts
2026-01-09 23:26:20 +01:00
Bram Kragten
6fc8c17909 Fix color palette creation (#28867) 2026-01-09 23:26:19 +01:00
dcapslock
201169c3d8 Fix choose selector active_choice when card editor config changes (#28858) 2026-01-09 23:26:18 +01:00
DAccord
303538ac21 Handling empty history (#28852)
Co-authored-by: DAccord <11232265+DAccord@users.noreply.github.com>
2026-01-09 23:26:17 +01:00
Bram Kragten
3c5a6193d0 Bumped version to 20260107.0 2026-01-07 16:32:09 +01:00
Wendelin
5ee4bd63f8 Fix logs provider picker mobile width (#28847) 2026-01-07 16:31:52 +01:00
Wendelin
b193929bd9 Throttle unknown value checks in ha-generic-picker (#28842) 2026-01-07 16:31:51 +01:00
Paul Bottein
3bee5c8cd4 Remove ha-combo-box-textfield (#28841) 2026-01-07 16:31:50 +01:00
Paul Bottein
976c74b8da Prefill the field with current value when editing a custom text item (#28840) 2026-01-07 16:31:49 +01:00
Wendelin
3a4a13db21 Improve device picker performance (#28835) 2026-01-07 16:31:48 +01:00
Paul Bottein
a2f033dd88 Reduce shadow effect for scrollable fade mixin (#28832) 2026-01-07 16:31:46 +01:00
Bram Kragten
a44b94c8df Prevent showing error during loading of statistics picker (#28823) 2026-01-07 16:31:45 +01:00
Bram Kragten
8796830ff9 Bumped version to 20251229.1 2026-01-06 17:18:45 +01:00
Bram Kragten
bdff13d5e1 Use target selector to filter references entities (#28822)
* Use target selector to filter references entities

* Update ha-selector-state.ts
2026-01-06 17:18:20 +01:00
Bram Kragten
4346484afc Use single path for thread icon, add KNX, simplify (#28819)
* Use single path for thread icon, simplify

* Add custom path for KNX
2026-01-06 17:18:19 +01:00
Bram Kragten
533694391e Remove iOS focus handling from dialogs (#28818) 2026-01-06 17:18:18 +01:00
Bram Kragten
3adba7aa1f Fix translation loading of choose selector (#28817) 2026-01-06 17:18:17 +01:00
karwosts
b60552c025 Fix statistic-graph-card cutoff w/ energy date picker (#28810)
* Fix statistics-graph energy-date mode end-time with 5min statistics

* don't modify date/hour for 5minute graph

* suggestedMax use period instead of days

* go back to string types
2026-01-06 17:18:16 +01:00
Paul Bottein
3011d56101 Show close button when zwave firmware update is finished (#28805) 2026-01-06 17:18:15 +01:00
Aidan Timson
c903c0d734 Add option for any state and show translated label for entity state values (#28803)
* Add option for any state

* Use translated labels for value
2026-01-06 17:18:14 +01:00
Aidan Timson
14be390994 Remove duplicate custom items, remove "no matching ..." when allow-custom-value set (#28801)
* Remove duplicate custom items, allow default from picker

* Memoize

* Memoize

* Memoize func

* Don't show no matching item when custom value is allowed

* Remove no items found label now unused

* Cleanup unused translations

* Restore used value

* Remove no items found label now unused

* Remove redundant comment

* Remove searchFn

* Ensure custom value isnt identical

* Fix duplicated value

* Fix duplicated value

* Use additional items for entity state content

* Fix duplicate values

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
2026-01-06 17:18:12 +01:00
Paul Bottein
d48019a48e Remove custom value for unknown icon in icon picker (#28800) 2026-01-06 17:18:11 +01:00
Copilot
7b5cbb76ef Display template targets with neutral badge instead of "Unknown area" error (#28799)
* Initial plan

* Add template target display with neutral badge

- Import mdiCodeBraces icon and isTemplate function
- Check if target ID is a template before checking if it exists
- Display grey {} icon with "Template" text for templated targets
- Add "template" translation key to target_summary
- Prevents misleading red "Unknown area" badge for template targets

Co-authored-by: piitaya <5878303+piitaya@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: piitaya <5878303+piitaya@users.noreply.github.com>
2026-01-06 17:18:10 +01:00
Paul Bottein
c75fab025f Use regular item for bottom padding in combobox (#28798) 2026-01-06 17:18:09 +01:00
karwosts
c007206fa0 Fix statistic names w/ energy_date_selection (#28787) 2026-01-06 17:18:08 +01:00
Norbert Rittel
ab5b5a4276 A few small spelling fixes in user-facing strings (#28786)
- use correct spelling for "Wi-Fi" trademark
- capitalize "PIN" as abbreviation
- fix spelling of "set up" as verb
- fix sentence-casing
2026-01-06 17:18:07 +01:00
Paulus Schoutsen
9eb40f8470 Bluetooth panel to support multi adapter (#28763)
* Support multiple adapters in bluetooth panel

* Move connection allocations up

* Make it tabs

* Add icons

* Revert "Add icons"

This reverts commit e338b6e578.

* Revert "Make it tabs"

This reverts commit d1b19d5c3e.

* Fix scanner matching and no active connection slot support

* Update src/panels/config/integrations/integration-panels/bluetooth/bluetooth-config-dashboard.ts

---------

Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-01-06 17:18:06 +01:00
dependabot[bot]
bc827d9bf1 Bump qs from 6.14.0 to 6.14.1 (#28760)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-06 17:18:05 +01:00
Simon Lamon
24f5d58691 Make cancel a secondary action in blueprint import (#28754) 2026-01-06 17:18:04 +01:00
Simon Lamon
13505a9104 Fix matter translations (#28752) 2026-01-06 17:18:03 +01:00
Simon Lamon
c1d135aa16 Revert lit update (#28751) 2026-01-06 17:18:02 +01:00
Paulus Schoutsen
16d13c3202 Verify bluetooth config entries exist before showing entry (#28745) 2026-01-06 17:18:01 +01:00
Paulus Schoutsen
46b3c34ba1 Hide dashboard controls in kiosk mode (#28742) 2026-01-06 17:18:00 +01:00
Paulus Schoutsen
57a81b9de4 Add config entry picker for Z-Wave JS panel (#28741) 2026-01-06 17:17:58 +01:00
Simon Lamon
69f4f1dbed Provide kioskmode in demo (#28739) 2026-01-06 17:17:57 +01:00
Paulus Schoutsen
355a1aff3f Protocol link updates (#28736)
* Update icons Thread & Insteon

* Remove matter link

* Remove back path from ZHA

* Fix ZHA dashboard config entry
2026-01-06 17:17:56 +01:00
181 changed files with 3050 additions and 5363 deletions

View File

@@ -22,13 +22,11 @@ You are an assistant helping with development of the Home Assistant frontend. Th
```bash
yarn lint # ESLint + Prettier + TypeScript + Lit
yarn format # Auto-fix ESLint + Prettier
yarn lint:types # TypeScript compiler (run WITHOUT file arguments)
yarn lint:types # TypeScript compiler
yarn test # Vitest
script/develop # Development server
```
> **WARNING:** Never run `tsc` or `yarn lint:types` with file arguments (e.g., `yarn lint:types src/file.ts`). When `tsc` receives file arguments, it ignores `tsconfig.json` and emits `.js` files into `src/`, polluting the codebase. Always run `yarn lint:types` without arguments. For individual file type checking, rely on IDE diagnostics. If `.js` files are accidentally generated, clean up with `git clean -fd src/`.
### Component Prefixes
- `ha-` - Home Assistant components

View File

@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Send bundle stats and build information to RelativeCI
uses: relative-ci/agent-action@3c681926017930047fc03acaa35cd6a44efcbfc3 # v3.2.2
uses: relative-ci/agent-action@c45aaa919ef85620af54242a241ac17a8fa35983 # v3.2.1
with:
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
token: ${{ github.token }}

2
.gitignore vendored
View File

@@ -15,7 +15,7 @@ dist/
!.yarn/sdks
!.yarn/versions
.pnp.*
node_modules/
/node_modules/
yarn-error.log
npm-debug.log

2
.nvmrc
View File

@@ -1 +1 @@
24.13.0
24.12.0

View File

@@ -213,9 +213,7 @@ const createRspackConfig = ({
"lit/directives/join$": "lit/directives/join.js",
"lit/directives/repeat$": "lit/directives/repeat.js",
"lit/directives/live$": "lit/directives/live.js",
"lit/directives/keyed$": latestBuild
? "lit/directives/keyed.js"
: path.resolve(__dirname, "../src/common/lit/keyed-es5.ts"),
"lit/directives/keyed$": "lit/directives/keyed.js",
"lit/polyfill-support$": "lit/polyfill-support.js",
"@lit-labs/virtualizer/layouts/grid":
"@lit-labs/virtualizer/layouts/grid.js",

View File

@@ -1,4 +1,4 @@
import type { AreaRegistryEntry } from "../../../src/data/area/area_registry";
import type { AreaRegistryEntry } from "../../../src/data/area_registry";
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
export const mockAreaRegistry = (

View File

@@ -187,11 +187,5 @@ export default tseslint.config(
],
"no-use-before-define": "off",
},
},
{
files: ["src/util/recorder-worklet.js"],
languageOptions: {
globals: globals.audioWorklet,
},
}
);

View File

@@ -10,7 +10,7 @@ import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervis
import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data";
import "../../../../src/components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
import { getEntity } from "../../../../src/fake_data/entity";
import { provideHass } from "../../../../src/fake_data/provide_hass";

View File

@@ -11,7 +11,7 @@ import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry";
import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-selector/ha-selector";
import "../../../../src/components/ha-settings-row";
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
import type { BlueprintInput } from "../../../../src/data/blueprint";
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
import type { FloorRegistryEntry } from "../../../../src/data/floor_registry";

View File

@@ -1,3 +1,4 @@
import "@material/mwc-linear-progress/mwc-linear-progress";
import memoizeOne from "memoize-one";
import { type CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";

View File

@@ -34,18 +34,18 @@
"@codemirror/legacy-modes": "6.5.2",
"@codemirror/search": "6.5.11",
"@codemirror/state": "6.5.3",
"@codemirror/view": "6.39.9",
"@codemirror/view": "6.39.7",
"@date-fns/tz": "1.4.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "7.1.2",
"@formatjs/intl-displaynames": "7.1.2",
"@formatjs/intl-durationformat": "0.9.2",
"@formatjs/intl-getcanonicallocales": "3.1.2",
"@formatjs/intl-listformat": "8.1.2",
"@formatjs/intl-locale": "5.1.2",
"@formatjs/intl-numberformat": "9.1.2",
"@formatjs/intl-pluralrules": "6.1.2",
"@formatjs/intl-relativetimeformat": "12.1.2",
"@formatjs/intl-datetimeformat": "7.1.0",
"@formatjs/intl-displaynames": "7.1.0",
"@formatjs/intl-durationformat": "0.9.0",
"@formatjs/intl-getcanonicallocales": "3.1.0",
"@formatjs/intl-listformat": "8.1.0",
"@formatjs/intl-locale": "5.1.0",
"@formatjs/intl-numberformat": "9.1.0",
"@formatjs/intl-pluralrules": "6.1.0",
"@formatjs/intl-relativetimeformat": "12.1.0",
"@fullcalendar/core": "6.1.20",
"@fullcalendar/daygrid": "6.1.20",
"@fullcalendar/interaction": "6.1.20",
@@ -112,13 +112,13 @@
"hls.js": "1.6.15",
"home-assistant-js-websocket": "9.6.0",
"idb-keyval": "6.2.2",
"intl-messageformat": "11.0.9",
"intl-messageformat": "11.0.7",
"js-yaml": "4.1.1",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
"leaflet.markercluster": "1.5.3",
"lit": "3.3.2",
"lit-html": "3.3.2",
"lit": "3.3.1",
"lit-html": "3.3.1",
"luxon": "3.7.2",
"marked": "17.0.1",
"memoize-one": "6.0.0",
@@ -150,14 +150,14 @@
"@babel/helper-define-polyfill-provider": "0.6.5",
"@babel/plugin-transform-runtime": "7.28.5",
"@babel/preset-env": "7.28.5",
"@bundle-stats/plugin-webpack-filter": "4.21.8",
"@bundle-stats/plugin-webpack-filter": "4.21.7",
"@lokalise/node-api": "15.6.0",
"@octokit/auth-oauth-device": "8.0.3",
"@octokit/plugin-retry": "8.0.3",
"@octokit/rest": "22.0.1",
"@rsdoctor/rspack-plugin": "1.4.0",
"@rspack/core": "1.7.1",
"@rspack/dev-server": "1.1.5",
"@rspack/core": "1.6.8",
"@rspack/dev-server": "1.1.4",
"@types/babel__plugin-transform-runtime": "7.9.5",
"@types/chromecast-caf-receiver": "6.0.25",
"@types/chromecast-caf-sender": "1.0.11",
@@ -176,7 +176,7 @@
"@types/tar": "6.1.13",
"@types/ua-parser-js": "0.7.39",
"@types/webspeechapi": "0.0.29",
"@vitest/coverage-v8": "4.0.17",
"@vitest/coverage-v8": "4.0.16",
"babel-loader": "10.0.0",
"babel-plugin-template-html-minifier": "4.1.0",
"browserslist-useragent-regexp": "4.1.3",
@@ -199,7 +199,7 @@
"gulp-rename": "2.1.0",
"html-minifier-terser": "7.2.0",
"husky": "9.1.7",
"jsdom": "27.4.0",
"jsdom": "27.3.0",
"jszip": "3.10.1",
"lint-staged": "16.2.7",
"lit-analyzer": "2.0.3",
@@ -215,27 +215,27 @@
"terser-webpack-plugin": "5.3.16",
"ts-lit-plugin": "2.0.2",
"typescript": "5.9.3",
"typescript-eslint": "8.52.0",
"vite-tsconfig-paths": "6.0.4",
"vitest": "4.0.17",
"typescript-eslint": "8.50.1",
"vite-tsconfig-paths": "6.0.3",
"vitest": "4.0.16",
"webpack-stats-plugin": "1.1.3",
"webpackbar": "7.0.0",
"workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
},
"resolutions": {
"@material/mwc-button@^0.25.3": "^0.27.0",
"lit": "3.3.2",
"lit-html": "3.3.2",
"lit": "3.3.1",
"lit-html": "3.3.1",
"clean-css": "5.3.3",
"@lit/reactive-element": "2.1.2",
"@fullcalendar/daygrid": "6.1.20",
"globals": "17.0.0",
"globals": "16.5.0",
"tslib": "2.8.1",
"@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
"glob@^10.2.2": "^10.5.0"
},
"packageManager": "yarn@4.12.0",
"volta": {
"node": "24.13.0"
"node": "24.12.0"
}
}

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20251229.0"
version = "20260107.1"
license = "Apache-2.0"
license-files = ["LICENSE*"]
description = "The Home Assistant frontend"

View File

@@ -38,11 +38,13 @@ export class HaAuthFormString extends HaFormString {
}
</style>
<ha-auth-textfield
.type=${!this.isPassword
.type=${
!this.isPassword
? this.stringType
: this.unmaskedPassword
? "text"
: "password"}
: "password"
}
.label=${this.label}
.value=${this.data || ""}
.helper=${this.helper}
@@ -53,17 +55,18 @@ export class HaAuthFormString extends HaFormString {
.name=${this.schema.name}
.autocomplete=${this.schema.autocomplete}
?autofocus=${this.schema.autofocus}
.suffix=${this.isPassword
? // reserve some space for the icon.
html`<div style="width: 24px"></div>`
: this.schema.description?.suffix}
.validationMessage=${this.schema.required
? this.localize?.("ui.panel.page-authorize.form.error_required")
: undefined}
.suffix=${
this.isPassword
? // reserve some space for the icon.
html`<div style="width: 24px"></div>`
: this.schema.description?.suffix
}
.validationMessage=${this.schema.required ? this.localize?.("ui.panel.page-authorize.form.error_required") : undefined}
@input=${this._valueChanged}
@change=${this._valueChanged}
></ha-auth-textfield>
${this.renderIcon()}
></ha-auth-textfield>
${this.renderIcon()}
</ha-auth-textfield>
`;
}
}

View File

@@ -1,4 +1,4 @@
import type { AreaRegistryEntry } from "../../data/area/area_registry";
import type { AreaRegistryEntry } from "../../data/area_registry";
import type { FloorRegistryEntry } from "../../data/floor_registry";
export interface AreasFloorHierarchy {

View File

@@ -93,8 +93,8 @@ export const calcDateRange = (
];
case "now-12m":
return [
calcDate(today, subMonths, hass.locale, hass.config, 12),
calcDate(today, subMonths, hass.locale, hass.config, 0),
calcDate(subMonths(today, 12), startOfMonth, hass.locale, hass.config),
calcDate(subMonths(today, 1), endOfMonth, hass.locale, hass.config),
];
case "now-1h":
return [

View File

@@ -1,4 +1,4 @@
import type { AreaRegistryEntry } from "../../data/area/area_registry";
import type { AreaRegistryEntry } from "../../data/area_registry";
export const computeAreaName = (area: AreaRegistryEntry): string | undefined =>
area.name?.trim();

View File

@@ -1,4 +1,4 @@
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
import type { AreaRegistryEntry } from "../../../data/area_registry";
import type { FloorRegistryEntry } from "../../../data/floor_registry";
import type { HomeAssistant } from "../../../types";

View File

@@ -1,4 +1,4 @@
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
import type { AreaRegistryEntry } from "../../../data/area_registry";
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
import type { FloorRegistryEntry } from "../../../data/floor_registry";
import type { HomeAssistant } from "../../../types";

View File

@@ -1,5 +1,5 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
import type { AreaRegistryEntry } from "../../../data/area_registry";
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
import type {
EntityRegistryDisplayEntry,

View File

@@ -1,53 +0,0 @@
/**
* ES5-compatible implementation of the keyed directive.
* Based on lit-html's keyed directive but written to avoid ES5 minification issues.
*
* This implementation avoids parameter destructuring in the update() method,
* which causes Terser with ecma: 5 to generate invalid references like `_k`.
*
* Used only for ES5 builds (legacy browsers). Modern builds use the original
* lit-html keyed directive.
*
* @see https://github.com/home-assistant/frontend/issues/28732
*/
// eslint-disable-next-line import/extensions
import { directive, Directive } from "lit-html/directive.js";
// eslint-disable-next-line import/extensions
import { setCommittedValue } from "lit-html/directive-helpers.js";
// eslint-disable-next-line lit/no-legacy-imports
import { nothing } from "lit-html";
// eslint-disable-next-line import/extensions
import type { Part } from "lit-html/directive.js";
class KeyedES5 extends Directive {
private _key: unknown = nothing;
render(k: unknown, v: unknown) {
this._key = k;
return v;
}
update(part: unknown, args: [unknown, unknown]) {
const k = args[0];
const v = args[1];
if (k !== this._key) {
// Clear the part before returning a value. The one-arg form of
// setCommittedValue sets the value to a sentinel which forces a
// commit the next render.
setCommittedValue(part as Part);
this._key = k;
}
return v;
}
}
/**
* Associates a renderable value with a unique key. When the key changes, the
* previous DOM is removed and disposed before rendering the next value, even
* if the value - such as a template - is the same.
*
* This is useful for forcing re-renders of stateful components, or working
* with code that expects new data to generate new HTML elements, such as some
* animation techniques.
*/
export const keyed = directive(KeyedES5);

View File

@@ -1,16 +1,6 @@
// From https://github.com/epoberezkin/fast-deep-equal
// MIT License - Copyright (c) 2017 Evgeny Poberezkin
interface DeepEqualOptions {
/** Compare Symbol properties in addition to string keys */
compareSymbols?: boolean;
}
export const deepEqual = (
a: any,
b: any,
options?: DeepEqualOptions
): boolean => {
export const deepEqual = (a: any, b: any): boolean => {
if (a === b) {
return true;
}
@@ -28,7 +18,7 @@ export const deepEqual = (
return false;
}
for (i = length; i-- !== 0; ) {
if (!deepEqual(a[i], b[i], options)) {
if (!deepEqual(a[i], b[i])) {
return false;
}
}
@@ -45,7 +35,7 @@ export const deepEqual = (
}
}
for (i of a.entries()) {
if (!deepEqual(i[1], b.get(i[0]), options)) {
if (!deepEqual(i[1], b.get(i[0]))) {
return false;
}
}
@@ -103,28 +93,11 @@ export const deepEqual = (
for (i = length; i-- !== 0; ) {
const key = keys[i];
if (!deepEqual(a[key], b[key], options)) {
if (!deepEqual(a[key], b[key])) {
return false;
}
}
// Compare Symbol properties if requested
if (options?.compareSymbols) {
const symbolsA = Object.getOwnPropertySymbols(a);
const symbolsB = Object.getOwnPropertySymbols(b);
if (symbolsA.length !== symbolsB.length) {
return false;
}
for (const sym of symbolsA) {
if (!Object.prototype.hasOwnProperty.call(b, sym)) {
return false;
}
if (!deepEqual(a[sym], b[sym], options)) {
return false;
}
}
}
return true;
}

View File

@@ -2,13 +2,9 @@ import { customElement, property, state } from "lit/decorators";
import { LitElement, html, css } from "lit";
import type { EChartsType } from "echarts/core";
import type { SankeySeriesOption } from "echarts/types/dist/echarts";
import type {
CallbackDataParams,
ECElementEvent,
} from "echarts/types/src/util/types";
import type { CallbackDataParams } from "echarts/types/src/util/types";
import memoizeOne from "memoize-one";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import { fireEvent } from "../../common/dom/fire_event";
import SankeyChart from "../../resources/echarts/components/sankey/install";
import type { HomeAssistant } from "../../types";
import type { ECOption } from "../../resources/echarts/echarts";
@@ -25,7 +21,6 @@ export interface Node {
label?: string;
color?: string;
passThrough?: boolean;
entityId?: string;
}
export interface Link {
source: string;
@@ -88,7 +83,6 @@ export class HaSankeyChart extends LitElement {
.options=${options}
height="100%"
.extraComponents=${[SankeyChart]}
@chart-click=${this._handleChartClick}
></ha-chart-base>`;
}
@@ -109,22 +103,6 @@ export class HaSankeyChart extends LitElement {
return null;
};
private _handleChartClick = (ev: CustomEvent<ECElementEvent>) => {
const detail = ev.detail;
// Only handle node clicks (not links)
if (detail.dataType !== "node") {
return;
}
const nodeId = (detail.data as Record<string, any>)?.id;
if (!nodeId) {
return;
}
const node = this.data.nodes.find((n) => n.id === nodeId);
if (node?.entityId) {
fireEvent(this, "node-click", { node });
}
};
private _createData = memoizeOne((data: SankeyChartData, width = 0) => {
const filteredNodes = data.nodes.filter((n) => n.value > 0);
const indexes = [...new Set(filteredNodes.map((n) => n.index))].sort();
@@ -316,7 +294,4 @@ declare global {
interface HTMLElementTagNameMap {
"ha-sankey-chart": HaSankeyChart;
}
interface HASSDomEvents {
"node-click": { node: Node };
}
}

View File

@@ -21,7 +21,6 @@ import { measureTextWidth } from "../../util/text";
import { fireEvent } from "../../common/dom/fire_event";
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
import { filterXSS } from "../../common/util/xss";
const safeParseFloat = (value) => {
const parsed = parseFloat(value);
@@ -185,7 +184,7 @@ export class StateHistoryChartLine extends LitElement {
}
if (param.seriesName) {
return `${param.marker} ${filterXSS(param.seriesName)}: ${value}`;
return `${param.marker} ${param.seriesName}: ${value}`;
}
return `${param.marker} ${value}`;
})

View File

@@ -1364,9 +1364,6 @@ export class HaDataTable extends LitElement {
.mdc-data-table__header-cell > * {
transition: var(--float-start) 0.2s ease;
}
.mdc-data-table__header-cell--numeric > span {
transition: none;
}
.mdc-data-table__header-cell ha-svg-icon {
top: -3px;
position: absolute;

View File

@@ -1,9 +1,8 @@
import { consume } from "@lit/context";
import { css, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { fullEntitiesContext } from "../../data/context";
import type { DeviceAutomation } from "../../data/device/device_automation";
import {
@@ -12,12 +11,11 @@ import {
} from "../../data/device/device_automation";
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
import type { HomeAssistant } from "../../types";
import "../ha-generic-picker";
import "../ha-md-select";
import "../ha-md-select-option";
import type { PickerValueRenderer } from "../ha-picker-field";
const NO_AUTOMATION_KEY = "NO_AUTOMATION";
const UNKNOWN_AUTOMATION_KEY = "UNKNOWN_AUTOMATION";
export abstract class HaDeviceAutomationPicker<
T extends DeviceAutomation,
@@ -30,7 +28,7 @@ export abstract class HaDeviceAutomationPicker<
@property({ type: Object }) public value?: T;
@state() private _automations?: T[];
@state() private _automations: T[] = [];
// Trigger an empty render so we start with a clean DOM.
// paper-listbox does not like changing things around.
@@ -46,6 +44,12 @@ export abstract class HaDeviceAutomationPicker<
);
}
protected get UNKNOWN_AUTOMATION_TEXT() {
return this.hass.localize(
"ui.panel.config.devices.automation.actions.unknown_action"
);
}
private _localizeDeviceAutomation: (
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
@@ -71,7 +75,7 @@ export abstract class HaDeviceAutomationPicker<
}
private get _value() {
if (!this.value || !this._automations) {
if (!this.value) {
return "";
}
@@ -84,7 +88,7 @@ export abstract class HaDeviceAutomationPicker<
);
if (idx === -1) {
return this.value.alias || this.value.type || "unknown";
return UNKNOWN_AUTOMATION_KEY;
}
return `${this._automations[idx].device_id}_${idx}`;
@@ -95,21 +99,37 @@ export abstract class HaDeviceAutomationPicker<
return nothing;
}
const value = this._value;
return html`<ha-generic-picker
.hass=${this.hass}
.label=${this.label}
.value=${value}
.disabled=${!this._automations || this._automations.length === 0}
.getItems=${this._getItems(value, this._automations)}
@value-changed=${this._automationChanged}
.valueRenderer=${this._valueRenderer}
.unknownItemText=${this.hass.localize(
"ui.panel.config.devices.automation.actions.unknown_action"
)}
hide-clear-icon
>
</ha-generic-picker>`;
return html`
<ha-md-select
.label=${this.label}
.value=${value}
@change=${this._automationChanged}
@closed=${stopPropagation}
.disabled=${this._automations.length === 0}
>
${value === NO_AUTOMATION_KEY
? html`<ha-md-select-option .value=${NO_AUTOMATION_KEY}>
${this.NO_AUTOMATION_TEXT}
</ha-md-select-option>`
: nothing}
${value === UNKNOWN_AUTOMATION_KEY
? html`<ha-md-select-option .value=${UNKNOWN_AUTOMATION_KEY}>
${this.UNKNOWN_AUTOMATION_TEXT}
</ha-md-select-option>`
: nothing}
${this._automations.map(
(automation, idx) => html`
<ha-md-select-option .value=${`${automation.device_id}_${idx}`}>
${this._localizeDeviceAutomation(
this.hass,
this._entityReg,
automation
)}
</ha-md-select-option>
`
)}
</ha-md-select>
`;
}
protected updated(changedProps) {
@@ -120,57 +140,6 @@ export abstract class HaDeviceAutomationPicker<
}
}
private _getItems = memoizeOne(
(value: string, automations: T[] | undefined) => {
if (!automations) {
return () => undefined;
}
const automationListItems = automations.map((automation, idx) => {
const primary = this._localizeDeviceAutomation(
this.hass,
this._entityReg,
automation
);
return {
id: `${automation.device_id}_${idx}`,
primary,
};
});
automationListItems.sort((a, b) =>
caseInsensitiveStringCompare(
a.primary,
b.primary,
this.hass.locale.language
)
);
if (value === NO_AUTOMATION_KEY) {
automationListItems.unshift({
id: NO_AUTOMATION_KEY,
primary: this.NO_AUTOMATION_TEXT,
});
}
return () => automationListItems;
}
);
private _valueRenderer: PickerValueRenderer = (value: string) => {
const automation = this._automations?.find(
(a, idx) => value === `${a.device_id}_${idx}`
);
const text = automation
? this._localizeDeviceAutomation(this.hass, this._entityReg, automation)
: value === NO_AUTOMATION_KEY
? this.NO_AUTOMATION_TEXT
: value;
return html`<span slot="headline">${text}</span>`;
};
private async _updateDeviceInfo() {
this._automations = this.deviceId
? (await this._fetchDeviceAutomations(this.hass, this.deviceId)).sort(
@@ -192,14 +161,13 @@ export abstract class HaDeviceAutomationPicker<
this._renderEmpty = false;
}
private _automationChanged(ev: CustomEvent<{ value: string }>) {
ev.stopPropagation();
const value = ev.detail.value;
if (!value || NO_AUTOMATION_KEY === value) {
private _automationChanged(ev) {
const value = ev.target.value;
if (!value || [UNKNOWN_AUTOMATION_KEY, NO_AUTOMATION_KEY].includes(value)) {
return;
}
const [deviceId, idx] = value.split("_");
const automation = this._automations![idx];
const automation = this._automations[idx];
if (automation.device_id !== deviceId) {
return;
}

View File

@@ -7,7 +7,6 @@ import memoizeOne from "memoize-one";
import { ensureArray } from "../../common/array/ensure-array";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { getEntityContext } from "../../common/entity/context/get_entity_context";
import {
STATE_DISPLAY_SPECIAL_CONTENT,
STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS,
@@ -96,9 +95,6 @@ export class HaStateContentPicker extends LitElement {
@property({ type: Boolean, attribute: "allow-name" }) public allowName =
false;
@property({ type: Boolean, attribute: "allow-context" }) public allowContext =
false;
@property() public label?: string;
@property() public value?: string[] | string;
@@ -110,12 +106,7 @@ export class HaStateContentPicker extends LitElement {
private _editIndex?: number;
private _getItems = memoizeOne(
(
entityId?: string,
stateObj?: HassEntity,
allowName?: boolean,
allowContext?: boolean
) => {
(entityId?: string, stateObj?: HassEntity, allowName?: boolean) => {
const domain = entityId ? computeDomain(entityId) : undefined;
const items: PickerComboBoxItem[] = [
{
@@ -158,52 +149,6 @@ export class HaStateContentPicker extends LitElement {
"ui.components.state-content-picker.last_updated"
),
},
...(allowContext && stateObj
? (() => {
const context = getEntityContext(
stateObj,
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
const contextItems: PickerComboBoxItem[] = [];
if (context.device) {
contextItems.push({
id: "device_name",
primary: this.hass.localize(
"ui.components.state-content-picker.device_name"
),
sorting_label: this.hass.localize(
"ui.components.state-content-picker.device_name"
),
});
}
if (context.area) {
contextItems.push({
id: "area_name",
primary: this.hass.localize(
"ui.components.state-content-picker.area_name"
),
sorting_label: this.hass.localize(
"ui.components.state-content-picker.area_name"
),
});
}
if (context.floor) {
contextItems.push({
id: "floor_name",
primary: this.hass.localize(
"ui.components.state-content-picker.floor_name"
),
sorting_label: this.hass.localize(
"ui.components.state-content-picker.floor_name"
),
});
}
return contextItems;
})()
: []),
...(domain
? STATE_DISPLAY_SPECIAL_CONTENT.filter((content) =>
STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS[domain]?.includes(content)
@@ -355,8 +300,7 @@ export class HaStateContentPicker extends LitElement {
const items = this._getItems(
this.entityId,
stateObjForItems,
this.allowName,
this.allowContext
this.allowName
);
return items.find((item) => item.id === value)?.primary;
}
@@ -399,12 +343,7 @@ export class HaStateContentPicker extends LitElement {
const stateObj = this.entityId
? this.hass.states[this.entityId]
: undefined;
const items = this._getItems(
this.entityId,
stateObj,
this.allowName,
this.allowContext
);
const items = this._getItems(this.entityId, stateObj, this.allowName);
const currentValue =
this._editIndex != null ? this._value[this._editIndex] : undefined;
@@ -428,12 +367,7 @@ export class HaStateContentPicker extends LitElement {
const stateObj = this.entityId
? this.hass.states[this.entityId]
: undefined;
const items = this._getItems(
this.entityId,
stateObj,
this.allowName,
this.allowContext
);
const items = this._getItems(this.entityId, stateObj, this.allowName);
// If the search string does not match with the id of any of the items,
// offer to add it as a custom attribute

View File

@@ -143,19 +143,17 @@ export class HaEntityToggle extends LitElement {
// Optimistic update.
this._isOn = turnOn;
try {
await this.hass.callService(serviceDomain, service, {
entity_id: this.stateObj.entity_id,
});
} finally {
setTimeout(async () => {
// If after 2 seconds we have not received a state update
// reset the switch to it's original state.
if (this.stateObj === currentState) {
this._isOn = isOn(this.stateObj);
}
}, 2000);
}
await this.hass.callService(serviceDomain, service, {
entity_id: this.stateObj.entity_id,
});
setTimeout(async () => {
// If after 2 seconds we have not received a state update
// reset the switch to it's original state.
if (this.stateObj === currentState) {
this._isOn = isOn(this.stateObj);
}
}, 2000);
}
static styles = css`

View File

@@ -1,8 +1,8 @@
import { mdiClose } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { listenMediaQuery } from "../common/dom/media_query";
import type { HomeAssistant } from "../types";
import { listenMediaQuery } from "../common/dom/media_query";
import "./ha-bottom-sheet";
import "./ha-dialog-header";
import "./ha-icon-button";
@@ -88,9 +88,6 @@ export class HaAdaptiveDialog extends LitElement {
@property({ type: Boolean, attribute: "block-mode-change" })
public blockModeChange = false;
@property({ type: Boolean, attribute: "without-header" })
public withoutHeader = false;
@state() private _mode: DialogSheetMode = "dialog";
private _unsubMediaQuery?: () => void;
@@ -121,33 +118,27 @@ export class HaAdaptiveDialog extends LitElement {
if (this._mode === "bottom-sheet") {
return html`
<ha-bottom-sheet .open=${this.open} flexcontent>
${!this.withoutHeader
? html`<ha-dialog-header
slot="header"
.subtitlePosition=${this.headerSubtitlePosition}
>
<slot name="headerNavigationIcon" slot="navigationIcon">
<ha-icon-button
data-drawer="close"
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
.path=${mdiClose}
></ha-icon-button>
</slot>
${this.headerTitle !== undefined
? html`<span
slot="title"
class="title"
id="ha-wa-dialog-title"
>
${this.headerTitle}
</span>`
: html`<slot name="headerTitle" slot="title"></slot>`}
${this.headerSubtitle !== undefined
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
<slot name="headerActionItems" slot="actionItems"></slot>
</ha-dialog-header>`
: nothing}
<ha-dialog-header
slot="header"
.subtitlePosition=${this.headerSubtitlePosition}
>
<slot name="headerNavigationIcon" slot="navigationIcon">
<ha-icon-button
data-drawer="close"
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
.path=${mdiClose}
></ha-icon-button>
</slot>
${this.headerTitle !== undefined
? html`<span slot="title" class="title" id="ha-wa-dialog-title">
${this.headerTitle}
</span>`
: html`<slot name="headerTitle" slot="title"></slot>`}
${this.headerSubtitle !== undefined
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
<slot name="headerActionItems" slot="actionItems"></slot>
</ha-dialog-header>
<slot></slot>
<slot name="footer" slot="footer"></slot>
</ha-bottom-sheet>
@@ -165,7 +156,6 @@ export class HaAdaptiveDialog extends LitElement {
.headerSubtitle=${this.headerSubtitle}
.headerSubtitlePosition=${this.headerSubtitlePosition}
flexcontent
.withoutHeader=${this.withoutHeader}
>
<slot name="headerNavigationIcon" slot="headerNavigationIcon">
<ha-icon-button

View File

@@ -6,10 +6,16 @@ import { customElement, property, query } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { computeAreaName } from "../common/entity/compute_area_name";
import { computeDomain } from "../common/entity/compute_domain";
import { computeFloorName } from "../common/entity/compute_floor_name";
import { getAreaContext } from "../common/entity/context/get_area_context";
import { areaComboBoxKeys, getAreas } from "../data/area/area_picker";
import { createAreaRegistryEntry } from "../data/area/area_registry";
import { createAreaRegistryEntry } from "../data/area_registry";
import type {
DeviceEntityDisplayLookup,
DeviceRegistryEntry,
} from "../data/device/device_registry";
import { getDeviceEntityDisplayLookup } from "../data/device/device_registry";
import type { EntityRegistryDisplayEntry } from "../data/entity/entity_registry";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import { showAreaRegistryDetailDialog } from "../panels/config/areas/show-dialog-area-registry-detail";
import type { HomeAssistant, ValueChangedEvent } from "../types";
@@ -24,6 +30,12 @@ import "./ha-svg-icon";
const ADD_NEW_ID = "___ADD_NEW___";
const SEARCH_KEYS = [
{ name: "search_labels.areaName", weight: 10 },
{ name: "search_labels.aliases", weight: 8 },
{ name: "search_labels.floorName", weight: 6 },
{ name: "search_labels.id", weight: 3 },
];
@customElement("ha-area-picker")
export class HaAreaPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -90,8 +102,6 @@ export class HaAreaPicker extends LitElement {
await this._picker?.open();
}
private _getAreasMemoized = memoizeOne(getAreas);
// Recompute value renderer when the areas change
private _computeValueRenderer = memoizeOne(
(_haAreas: HomeAssistant["areas"]): PickerValueRenderer =>
@@ -127,13 +137,183 @@ export class HaAreaPicker extends LitElement {
}
);
private _getAreas = memoizeOne(
(
haAreas: HomeAssistant["areas"],
haDevices: HomeAssistant["devices"],
haEntities: HomeAssistant["entities"],
includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"],
includeDeviceClasses: this["includeDeviceClasses"],
deviceFilter: this["deviceFilter"],
entityFilter: this["entityFilter"],
excludeAreas: this["excludeAreas"]
): PickerComboBoxItem[] => {
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
let inputDevices: DeviceRegistryEntry[] | undefined;
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
const areas = Object.values(haAreas);
const devices = Object.values(haDevices);
const entities = Object.values(haEntities);
if (
includeDomains ||
excludeDomains ||
includeDeviceClasses ||
deviceFilter ||
entityFilter
) {
deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
inputDevices = devices;
inputEntities = entities.filter((entity) => entity.area_id);
if (includeDomains) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) =>
includeDomains.includes(computeDomain(entity.entity_id))
);
});
inputEntities = inputEntities!.filter((entity) =>
includeDomains.includes(computeDomain(entity.entity_id))
);
}
if (excludeDomains) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return true;
}
return entities.every(
(entity) =>
!excludeDomains.includes(computeDomain(entity.entity_id))
);
});
inputEntities = inputEntities!.filter(
(entity) =>
!excludeDomains.includes(computeDomain(entity.entity_id))
);
}
if (includeDeviceClasses) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return (
stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class)
);
});
});
inputEntities = inputEntities!.filter((entity) => {
const stateObj = this.hass.states[entity.entity_id];
return (
stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class)
);
});
}
if (deviceFilter) {
inputDevices = inputDevices!.filter((device) =>
deviceFilter!(device)
);
}
if (entityFilter) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return entityFilter(stateObj);
});
});
inputEntities = inputEntities!.filter((entity) => {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return entityFilter!(stateObj);
});
}
}
let outputAreas = areas;
let areaIds: string[] | undefined;
if (inputDevices) {
areaIds = inputDevices
.filter((device) => device.area_id)
.map((device) => device.area_id!);
}
if (inputEntities) {
areaIds = (areaIds ?? []).concat(
inputEntities
.filter((entity) => entity.area_id)
.map((entity) => entity.area_id!)
);
}
if (areaIds) {
outputAreas = outputAreas.filter((area) =>
areaIds!.includes(area.area_id)
);
}
if (excludeAreas) {
outputAreas = outputAreas.filter(
(area) => !excludeAreas!.includes(area.area_id)
);
}
const items = outputAreas.map<PickerComboBoxItem>((area) => {
const { floor } = getAreaContext(area, this.hass.floors);
const floorName = floor ? computeFloorName(floor) : undefined;
const areaName = computeAreaName(area);
return {
id: area.area_id,
primary: areaName || area.area_id,
secondary: floorName,
icon: area.icon || undefined,
icon_path: area.icon ? undefined : mdiTextureBox,
search_labels: {
areaName: areaName || null,
floorName: floorName || null,
id: area.area_id,
aliases: area.aliases.join(" "),
},
};
});
return items;
}
);
private _getItems = () =>
this._getAreasMemoized(
this._getAreas(
this.hass.areas,
this.hass.floors,
this.hass.devices,
this.hass.entities,
this.hass.states,
this.includeDomains,
this.excludeDomains,
this.includeDeviceClasses,
@@ -214,7 +394,7 @@ export class HaAreaPicker extends LitElement {
.getAdditionalItems=${this._getAdditionalItems}
.valueRenderer=${valueRenderer}
.addButtonLabel=${this.addButtonLabel}
.searchKeys=${areaComboBoxKeys}
.searchKeys=${SEARCH_KEYS}
.unknownItemText=${this.hass.localize(
"ui.components.area-picker.unknown"
)}

View File

@@ -174,14 +174,12 @@ export class HaAutomationRow extends LitElement {
}
::slotted([slot="header"]) {
flex: 1;
min-width: 0;
overflow-wrap: anywhere;
margin: 0 var(--ha-space-3);
}
.icons {
display: flex;
align-items: center;
flex-shrink: 0;
}
:host([sort-selected]) .row {
outline: solid;

View File

@@ -51,10 +51,7 @@ export class HaCard extends LitElement {
font-weight: var(--ha-font-weight-normal);
}
:host
::slotted(
.card-content:not(:nth-child(1 of .card-content, .card-header))
),
:host ::slotted(.card-content:not(:first-child)),
slot:not(:first-child)::slotted(.card-content) {
padding-top: 0;
margin-top: calc(var(--ha-space-2) * -1);

View File

@@ -255,7 +255,6 @@ export class HaCodeEditor extends ReactiveElement {
...this._loadedCodeMirror.tabKeyBindings,
saveKeyBinding,
]),
this._loadedCodeMirror.search({ top: true }),
this._loadedCodeMirror.langCompartment.of(this._mode),
this._loadedCodeMirror.haTheme,
this._loadedCodeMirror.haSyntaxHighlighting,

View File

@@ -1,11 +1,9 @@
import { mdiMinusThick, mdiPlusThick } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-base-time-input";
import type { TimeChangedEvent } from "./ha-base-time-input";
import "./ha-button-toggle-group";
export interface HaDurationData {
days?: number;
@@ -15,8 +13,6 @@ export interface HaDurationData {
milliseconds?: number;
}
const FIELDS = ["milliseconds", "seconds", "minutes", "hours", "days"];
@customElement("ha-duration-input")
class HaDurationInput extends LitElement {
@property({ attribute: false }) public data?: HaDurationData;
@@ -33,80 +29,41 @@ class HaDurationInput extends LitElement {
@property({ attribute: "enable-day", type: Boolean })
public enableDay = false;
@property({ attribute: "allow-negative", type: Boolean })
public allowNegative = false;
@property({ type: Boolean }) public disabled = false;
private _toggleNegative = false;
protected render(): TemplateResult {
return html`
<div class="row">
${this.allowNegative
? html`
<ha-button-toggle-group
size="small"
.buttons=${[
{ label: "+", iconPath: mdiPlusThick, value: "+" },
{ label: "-", iconPath: mdiMinusThick, value: "-" },
]}
.active=${this._negative ? "-" : "+"}
@value-changed=${this._negativeChanged}
></ha-button-toggle-group>
`
: nothing}
<ha-base-time-input
.label=${this.label}
.helper=${this.helper}
.required=${this.required}
.clearable=${!this.required && this.data !== undefined}
.autoValidate=${this.required}
.disabled=${this.disabled}
errorMessage="Required"
enable-second
.enableMillisecond=${this.enableMillisecond}
.enableDay=${this.enableDay}
format="24"
.days=${this._days}
.hours=${this._hours}
.minutes=${this._minutes}
.seconds=${this._seconds}
.milliseconds=${this._milliseconds}
@value-changed=${this._durationChanged}
no-hours-limit
day-label="dd"
hour-label="hh"
min-label="mm"
sec-label="ss"
ms-label="ms"
></ha-base-time-input>
</div>
<ha-base-time-input
.label=${this.label}
.helper=${this.helper}
.required=${this.required}
.clearable=${!this.required && this.data !== undefined}
.autoValidate=${this.required}
.disabled=${this.disabled}
errorMessage="Required"
enable-second
.enableMillisecond=${this.enableMillisecond}
.enableDay=${this.enableDay}
format="24"
.days=${this._days}
.hours=${this._hours}
.minutes=${this._minutes}
.seconds=${this._seconds}
.milliseconds=${this._milliseconds}
@value-changed=${this._durationChanged}
no-hours-limit
day-label="dd"
hour-label="hh"
min-label="mm"
sec-label="ss"
ms-label="ms"
></ha-base-time-input>
`;
}
private get _negative() {
return (
this._toggleNegative ||
(this.data?.days
? this.data.days < 0
: this.data?.hours
? this.data.hours < 0
: this.data?.minutes
? this.data.minutes < 0
: this.data?.seconds
? this.data.seconds < 0
: this.data?.milliseconds
? this.data.milliseconds < 0
: false)
);
}
private get _days() {
return this.data?.days
? this.allowNegative
? Math.abs(Number(this.data.days))
: Number(this.data.days)
? Number(this.data.days)
: this.required || this.data
? 0
: NaN;
@@ -114,9 +71,7 @@ class HaDurationInput extends LitElement {
private get _hours() {
return this.data?.hours
? this.allowNegative
? Math.abs(Number(this.data.hours))
: Number(this.data.hours)
? Number(this.data.hours)
: this.required || this.data
? 0
: NaN;
@@ -124,9 +79,7 @@ class HaDurationInput extends LitElement {
private get _minutes() {
return this.data?.minutes
? this.allowNegative
? Math.abs(Number(this.data.minutes))
: Number(this.data.minutes)
? Number(this.data.minutes)
: this.required || this.data
? 0
: NaN;
@@ -134,9 +87,7 @@ class HaDurationInput extends LitElement {
private get _seconds() {
return this.data?.seconds
? this.allowNegative
? Math.abs(Number(this.data.seconds))
: Number(this.data.seconds)
? Number(this.data.seconds)
: this.required || this.data
? 0
: NaN;
@@ -144,9 +95,7 @@ class HaDurationInput extends LitElement {
private get _milliseconds() {
return this.data?.milliseconds
? this.allowNegative
? Math.abs(Number(this.data.milliseconds))
: Number(this.data.milliseconds)
? Number(this.data.milliseconds)
: this.required || this.data
? 0
: NaN;
@@ -164,14 +113,6 @@ class HaDurationInput extends LitElement {
if ("days" in value) value.days ||= 0;
if ("milliseconds" in value) value.milliseconds ||= 0;
if (this.allowNegative) {
FIELDS.forEach((t) => {
if (value[t]) {
value[t] = Math.abs(value[t]);
}
});
}
if (!this.enableMillisecond && !value.milliseconds) {
// @ts-ignore
delete value.milliseconds;
@@ -194,47 +135,12 @@ class HaDurationInput extends LitElement {
value.days = (value.days ?? 0) + Math.floor(value.hours / 24);
value.hours %= 24;
}
if (this._negative) {
FIELDS.forEach((t) => {
if (value[t]) {
value[t] = -Math.abs(value[t]);
}
});
}
}
fireEvent(this, "value-changed", {
value,
});
}
private _negativeChanged(ev) {
ev.stopPropagation();
const negative = (ev.detail?.value || ev.target.value) === "-";
this._toggleNegative = negative;
const value = this.data;
if (value) {
FIELDS.forEach((t) => {
if (value[t]) {
value[t] = negative ? -Math.abs(value[t]) : Math.abs(value[t]);
}
});
fireEvent(this, "value-changed", {
value,
});
}
}
static styles = css`
.row {
display: flex;
align-items: center;
}
ha-button-toggle-group {
margin: var(--ha-space-2);
}
`;
}
declare global {

View File

@@ -1,198 +0,0 @@
import type { SelectedDetail } from "@material/mwc-list";
import { mdiFilterVariantRemove } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { fireEvent } from "../common/dom/fire_event";
import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant } from "../types";
import "./ha-check-list-item";
import "./ha-expansion-panel";
import "./ha-icon";
import "./ha-icon-button";
import "./ha-label";
import "./ha-list";
import "./ha-list-item";
import "./voice-assistant-brand-icon";
import { voiceAssistants } from "../data/expose";
import "../panels/config/voice-assistants/expose/expose-assistant-icon";
@customElement("ha-filter-voice-assistants")
export class HaFilterVoiceAssistants extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
// the list of selected voiceAssistantIds
@property({ attribute: false }) public value: string[] = [];
@property({ type: Boolean }) public narrow = false;
@property({ type: Boolean, reflect: true }) public expanded = false;
@state() private _voiceAssistantOptions: string[] = [];
@state() private _shouldRender = false;
protected render() {
return html`
<ha-expansion-panel
left-chevron
.expanded=${this.expanded}
@expanded-will-change=${this._expandedWillChange}
@expanded-changed=${this._expandedChanged}
>
<div slot="header" class="header">
${this.hass.localize(
"ui.panel.config.dashboard.voice_assistants.main"
)}
${this.value?.length
? html`<div class="badge">${this.value?.length}</div>
<ha-icon-button
.path=${mdiFilterVariantRemove}
@click=${this._clearFilter}
></ha-icon-button>`
: nothing}
</div>
${this._shouldRender
? html`<ha-list
@selected=${this._assistantsSelected}
class="ha-scrollbar"
multi
>
${repeat(
this._voiceAssistantOptions,
(voiceAssistantId) => voiceAssistantId,
(voiceAssistantId) =>
html`<ha-check-list-item
.value=${voiceAssistantId}
.selected=${(this.value || []).includes(voiceAssistantId)}
hasMeta
graphic="icon"
>
<voice-assistant-brand-icon
slot="graphic"
.voiceAssistantId=${voiceAssistantId}
.hass=${this.hass}
>
</voice-assistant-brand-icon>
${voiceAssistants[voiceAssistantId].name}
</ha-check-list-item>`
)}
</ha-list> `
: nothing}
</ha-expansion-panel>
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._voiceAssistantOptions = Object.keys(voiceAssistants);
}
protected updated(changed) {
if (changed.has("expanded") && this.expanded) {
setTimeout(() => {
if (!this.expanded) return;
this.renderRoot.querySelector("ha-list")!.style.height =
`${this.clientHeight - 49}px`;
}, 300);
}
}
private _expandedWillChange(ev) {
this._shouldRender = ev.detail.expanded;
}
private _expandedChanged(ev) {
this.expanded = ev.detail.expanded;
}
private async _assistantsSelected(
ev: CustomEvent<SelectedDetail<Set<number>>>
) {
if (!ev.detail.index) {
fireEvent(this, "data-table-filter-changed", {
value: [],
items: undefined,
});
this.value = [];
return;
}
const newvalue: string[] = [];
for (const index of ev.detail.index) {
newvalue.push(this._voiceAssistantOptions![index]);
}
this.value = newvalue;
fireEvent(this, "data-table-filter-changed", {
value: this.value,
items: undefined,
});
}
private _clearFilter(ev) {
ev.preventDefault();
this.value = [];
fireEvent(this, "data-table-filter-changed", {
value: undefined,
items: undefined,
});
}
static get styles(): CSSResultGroup {
return [
haStyleScrollbar,
css`
:host {
position: relative;
border-bottom: 1px solid var(--divider-color);
}
:host([expanded]) {
flex: 1;
height: 0;
}
ha-expansion-panel {
--ha-card-border-radius: var(--ha-border-radius-square);
--expansion-panel-content-padding: 0;
}
.header {
display: flex;
align-items: center;
}
.header ha-icon-button {
margin-inline-start: auto;
margin-inline-end: 8px;
}
.badge {
display: inline-block;
margin-left: 8px;
margin-inline-start: 8px;
margin-inline-end: initial;
min-width: 16px;
box-sizing: border-box;
border-radius: var(--ha-border-radius-circle);
font-size: var(--ha-font-size-xs);
font-weight: var(--ha-font-weight-normal);
background-color: var(--primary-color);
line-height: var(--ha-line-height-normal);
text-align: center;
padding: 0px 2px;
color: var(--text-primary-color);
}
.add {
position: absolute;
bottom: 0;
right: 0;
left: 0;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-filter-voice-assistants": HaFilterVoiceAssistants;
}
}

View File

@@ -8,7 +8,7 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
import { computeFloorName } from "../common/entity/compute_floor_name";
import { updateAreaRegistryEntry } from "../data/area/area_registry";
import { updateAreaRegistryEntry } from "../data/area_registry";
import type {
DeviceEntityDisplayLookup,
DeviceRegistryEntry,

View File

@@ -1,4 +1,4 @@
import { mdiPlus } from "@mdi/js";
import { mdiLabel, mdiPlus } from "@mdi/js";
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import type { TemplateResult } from "lit";
import { LitElement, html } from "lit";
@@ -25,9 +25,11 @@ import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-generic-picker";
import type { HaGenericPicker } from "./ha-generic-picker";
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
import type { PickerValueRenderer } from "./ha-picker-field";
import "./ha-svg-icon";
const ADD_NEW_ID = "___ADD_NEW___";
const NO_LABELS = "___NO_LABELS___";
@customElement("ha-label-picker")
export class HaLabelPicker extends SubscribeMixin(LitElement) {
@@ -106,10 +108,52 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
];
}
private _labelMap = memoizeOne(
(
labels: LabelRegistryEntry[] | undefined
): Map<string, LabelRegistryEntry> => {
if (!labels) {
return new Map();
}
return new Map(labels.map((label) => [label.label_id, label]));
}
);
private _computeValueRenderer = memoizeOne(
(labels: LabelRegistryEntry[] | undefined): PickerValueRenderer =>
(value) => {
const label = this._labelMap(labels).get(value);
if (!label) {
return html`
<ha-svg-icon slot="start" .path=${mdiLabel}></ha-svg-icon>
<span slot="headline">${value}</span>
`;
}
return html`
${label.icon
? html`<ha-icon slot="start" .icon=${label.icon}></ha-icon>`
: html`<ha-svg-icon slot="start" .path=${mdiLabel}></ha-svg-icon>`}
<span slot="headline">${label.name}</span>
`;
}
);
private _getLabelsMemoized = memoizeOne(getLabels);
private _getItems = () =>
this._getLabelsMemoized(
private _getItems = () => {
if (!this._labels || this._labels.length === 0) {
return [
{
id: NO_LABELS,
primary: this.hass.localize("ui.components.label-picker.no_labels"),
icon_path: mdiLabel,
},
];
}
return this._getLabelsMemoized(
this.hass.states,
this.hass.areas,
this.hass.devices,
@@ -122,6 +166,7 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
this.entityFilter,
this.excludeLabels
);
};
private _allLabelNames = memoizeOne((labels?: LabelRegistryEntry[]) => {
if (!labels) {
@@ -174,6 +219,8 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
this.placeholder ??
this.hass.localize("ui.components.label-picker.label");
const valueRenderer = this._computeValueRenderer(this._labels);
return html`
<ha-generic-picker
.disabled=${this.disabled}
@@ -190,6 +237,7 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
.value=${this.value}
.getItems=${this._getItems}
.getAdditionalItems=${this._getAdditionalItems}
.valueRenderer=${valueRenderer}
.searchKeys=${labelComboBoxKeys}
@value-changed=${this._valueChanged}
>
@@ -203,6 +251,10 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
const value = ev.detail.value;
if (value === NO_LABELS) {
return;
}
if (!value) {
this._setValue(undefined);
return;

View File

@@ -138,10 +138,10 @@ export class HaMarkdown extends LitElement {
--markdown-table-padding-inline: 0;
--markdown-table-padding-block: 0;
th {
vertical-align: attr(valign, middle);
vertical-align: attr(align, center);
}
td {
vertical-align: attr(valign, middle);
vertical-align: attr(align, left);
}
}
table {

View File

@@ -1,6 +1,6 @@
import type { LitVirtualizer } from "@lit-labs/virtualizer";
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { mdiClose, mdiMagnify, mdiMinusBoxOutline, mdiPlus } from "@mdi/js";
import { mdiMagnify, mdiMinusBoxOutline, mdiPlus } from "@mdi/js";
import Fuse from "fuse.js";
import { css, html, LitElement, nothing } from "lit";
import {
@@ -26,8 +26,6 @@ import "./chips/ha-chip-set";
import "./chips/ha-filter-chip";
import "./ha-combo-box-item";
import "./ha-icon";
import "./ha-icon-button";
import "./ha-svg-icon";
import "./ha-textfield";
import type { HaTextField } from "./ha-textfield";
@@ -149,9 +147,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
@property({ attribute: "selected-section" }) public selectedSection?: string;
@property({ type: Boolean, reflect: true }) public clearable = false;
@query("lit-virtualizer") public virtualizerElement?: LitVirtualizer;
@query("lit-virtualizer") private _virtualizerElement?: LitVirtualizer;
@query("ha-textfield") private _searchFieldElement?: HaTextField;
@@ -164,7 +160,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
}
protected get scrollableElement(): HTMLElement | null {
return this.virtualizerElement as HTMLElement | null;
return this._virtualizerElement as HTMLElement | null;
}
@state() private _sectionTitle?: string;
@@ -211,17 +207,8 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
return html`<ha-textfield
.label=${searchLabel}
@blur=${this._resetSelectedItem}
@input=${this._filterChanged}
.iconTrailing=${this.clearable && !!this._search}
>
<ha-icon-button
@click=${this._clearSearch}
slot="trailingIcon"
.label=${this.hass?.localize("ui.common.clear") || "Clear"}
.path=${mdiClose}
></ha-icon-button>
</ha-textfield>
></ha-textfield>
${this._renderSectionButtons()}
${this.sections?.length
? html`
@@ -257,7 +244,6 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
@unpinned=${this._handleUnpinned}
@scroll=${this._onScrollList}
@focus=${this._focusList}
@blur=${this._resetSelectedItem}
@visibilityChanged=${this._visibilityChanged}
>
</lit-virtualizer>
@@ -290,18 +276,18 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
@eventOptions({ passive: true })
private _visibilityChanged(ev) {
if (
this.virtualizerElement &&
this._virtualizerElement &&
this.sectionTitleFunction &&
this.sections?.length
) {
const firstItem = this.virtualizerElement.items[ev.first];
const secondItem = this.virtualizerElement.items[ev.first + 1];
const firstItem = this._virtualizerElement.items[ev.first];
const secondItem = this._virtualizerElement.items[ev.first + 1];
this._sectionTitle = this.sectionTitleFunction({
firstIndex: ev.first,
lastIndex: ev.last,
firstItem: firstItem as PickerComboBoxItem,
secondItem: secondItem as PickerComboBoxItem,
itemsCount: this.virtualizerElement.items.length,
itemsCount: this._virtualizerElement.items.length,
});
}
}
@@ -417,22 +403,9 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
private _valueSelected = (ev: Event) => {
ev.stopPropagation();
const value = (ev.currentTarget as any).value as string;
const index = Number((ev.currentTarget as any).index);
const newValue = value?.trim();
this._fireSelectedEvents(newValue, index);
};
private _fireSelectedEvents(value: string, index: number) {
fireEvent(this, "value-changed", { value });
fireEvent(this, "index-selected", { index });
}
private _clearSearch = () => {
if (this._searchFieldElement) {
this._searchFieldElement.value = "";
this._searchFieldElement.dispatchEvent(new Event("input"));
}
fireEvent(this, "value-changed", { value: newValue });
};
private _fuseIndex = memoizeOne(
@@ -514,8 +487,8 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
this._items = this._getItems();
// Reset scroll position when filter changes
if (this.virtualizerElement) {
this.virtualizerElement.scrollToIndex(0);
if (this._virtualizerElement) {
this._virtualizerElement.scrollToIndex(0);
}
}
@@ -538,13 +511,13 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
private _selectNextItem = (ev?: KeyboardEvent) => {
ev?.stopPropagation();
ev?.preventDefault();
if (!this.virtualizerElement) {
if (!this._virtualizerElement) {
return;
}
this._searchFieldElement?.focus();
const items = this.virtualizerElement.items as PickerComboBoxItem[];
const items = this._virtualizerElement.items as PickerComboBoxItem[];
const maxItems = items.length - 1;
@@ -578,14 +551,14 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
private _selectPreviousItem = (ev: KeyboardEvent) => {
ev.stopPropagation();
ev.preventDefault();
if (!this.virtualizerElement) {
if (!this._virtualizerElement) {
return;
}
if (this._selectedItemIndex > 0) {
const nextIndex = this._selectedItemIndex - 1;
const items = this.virtualizerElement.items as PickerComboBoxItem[];
const items = this._virtualizerElement.items as PickerComboBoxItem[];
if (!items[nextIndex]) {
return;
@@ -607,13 +580,13 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
private _selectFirstItem = (ev: KeyboardEvent) => {
ev.stopPropagation();
if (!this.virtualizerElement || !this.virtualizerElement.items.length) {
if (!this._virtualizerElement || !this._virtualizerElement.items.length) {
return;
}
const nextIndex = 0;
if (typeof this.virtualizerElement.items[nextIndex] === "string") {
if (typeof this._virtualizerElement.items[nextIndex] === "string") {
this._selectedItemIndex = nextIndex + 1;
} else {
this._selectedItemIndex = nextIndex;
@@ -624,13 +597,13 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
private _selectLastItem = (ev: KeyboardEvent) => {
ev.stopPropagation();
if (!this.virtualizerElement || !this.virtualizerElement.items.length) {
if (!this._virtualizerElement || !this._virtualizerElement.items.length) {
return;
}
const nextIndex = this.virtualizerElement.items.length - 1;
const nextIndex = this._virtualizerElement.items.length - 1;
if (typeof this.virtualizerElement.items[nextIndex] === "string") {
if (typeof this._virtualizerElement.items[nextIndex] === "string") {
this._selectedItemIndex = nextIndex - 1;
} else {
this._selectedItemIndex = nextIndex;
@@ -640,14 +613,14 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
};
private _scrollToSelectedItem = () => {
this.virtualizerElement
this._virtualizerElement
?.querySelector(".selected")
?.classList.remove("selected");
this.virtualizerElement?.scrollToIndex(this._selectedItemIndex, "end");
this._virtualizerElement?.scrollToIndex(this._selectedItemIndex, "end");
requestAnimationFrame(() => {
this.virtualizerElement
this._virtualizerElement
?.querySelector(`#list-item-${this._selectedItemIndex}`)
?.classList.add("selected");
});
@@ -655,20 +628,12 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
private _pickSelectedItem = (ev: KeyboardEvent) => {
ev.stopPropagation();
if (
this.virtualizerElement?.items?.length !== undefined &&
this.virtualizerElement.items.length < 4 && // it still can have a section title and a padding item
this.virtualizerElement.items.filter((item) => typeof item !== "string")
.length === 1
) {
(
this.virtualizerElement?.items as (PickerComboBoxItem | string)[]
).forEach((item, index) => {
if (typeof item !== "string") {
this._fireSelectedEvents(item.id, index);
}
const firstItem = this._virtualizerElement?.items[0] as PickerComboBoxItem;
if (this._virtualizerElement?.items.length === 1) {
fireEvent(this, "value-changed", {
value: firstItem.id,
});
return;
}
if (this._selectedItemIndex === -1) {
@@ -678,16 +643,16 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
// if filter button is focused
ev.preventDefault();
const item = this.virtualizerElement?.items[
const item = this._virtualizerElement?.items[
this._selectedItemIndex
] as PickerComboBoxItem;
if (item) {
this._fireSelectedEvents(item.id, this._selectedItemIndex);
fireEvent(this, "value-changed", { value: item.id });
}
};
private _resetSelectedItem() {
this.virtualizerElement
this._virtualizerElement
?.querySelector(".selected")
?.classList.remove("selected");
this._selectedItemIndex = -1;
@@ -697,11 +662,11 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
typeof item === "string" ? item : item?.id;
private _getInitialSelectedIndex() {
if (!this.virtualizerElement || this._search || !this.value) {
if (!this._virtualizerElement || this._search || !this.value) {
return 0;
}
const index = this.virtualizerElement.items.findIndex(
const index = this._virtualizerElement.items.findIndex(
(item) =>
typeof item !== "string" &&
(item as PickerComboBoxItem).id === this.value
@@ -726,10 +691,6 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
flex: 1;
}
:host([clearable]) {
--text-field-padding: 0 0 0 var(--ha-space-4);
}
ha-textfield {
padding: 0 var(--ha-space-3);
margin-bottom: var(--ha-space-3);
@@ -831,9 +792,8 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
.section-title,
.title {
box-sizing: border-box;
background-color: var(--ha-color-fill-neutral-quiet-resting);
padding: var(--ha-space-1) var(--ha-space-4);
padding: var(--ha-space-1) var(--ha-space-2);
font-weight: var(--ha-font-weight-bold);
color: var(--secondary-text-color);
min-height: var(--ha-space-6);
@@ -862,7 +822,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
opacity: 0;
position: absolute;
top: 1px;
width: calc(100% - var(--ha-space-4));
width: calc(100% - var(--ha-space-8));
}
.section-title.show {
@@ -886,8 +846,4 @@ declare global {
interface HTMLElementTagNameMap {
"ha-picker-combo-box": HaPickerComboBox;
}
interface HASSDomEvents {
"index-selected": { index: number };
}
}

View File

@@ -223,6 +223,7 @@ export class HaRelatedItems extends LitElement {
.src=${brandsUrl({
domain: entry.domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"
@@ -248,6 +249,7 @@ export class HaRelatedItems extends LitElement {
.src=${brandsUrl({
domain: integration,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"

View File

@@ -10,7 +10,7 @@ class HaSectionTitle extends LitElement {
static styles = css`
:host {
background-color: var(--ha-color-fill-neutral-quiet-resting);
padding: var(--ha-space-2) var(--ha-space-3);
padding: var(--ha-space-1) var(--ha-space-2);
font-weight: var(--ha-font-weight-bold);
color: var(--secondary-text-color);
min-height: var(--ha-space-6);

View File

@@ -1,4 +1,3 @@
import memoizeOne from "memoize-one";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { DurationSelector } from "../../data/selector";
@@ -12,10 +11,7 @@ export class HaTimeDuration extends LitElement {
@property({ attribute: false }) public selector!: DurationSelector;
@property({ attribute: false }) public value?:
| HaDurationData
| string
| number;
@property({ attribute: false }) public value?: HaDurationData;
@property() public label?: string;
@@ -25,47 +21,16 @@ export class HaTimeDuration extends LitElement {
@property({ type: Boolean }) public required = true;
private _data = memoizeOne(
(value?: HaDurationData | string | number): HaDurationData | undefined => {
if (typeof value === "number") {
return { seconds: value };
}
if (typeof value === "string") {
const negative = value.trim()[0] === "-";
const parts = value
.split(":")
.map((p) => (negative && p ? -Math.abs(Number(p)) : Number(p)));
if (parts.length === 1) {
return { seconds: parts[0] };
}
if (parts.length === 2) {
return { hours: parts[0], minutes: parts[1] };
}
if (parts.length === 3) {
return {
hours: parts[0],
minutes: parts[1],
seconds: parts[2],
};
}
return undefined;
}
return value;
}
);
protected render() {
return html`
<ha-duration-input
.label=${this.label}
.helper=${this.helper}
.data=${this._data(this.value)}
.data=${this.value}
.disabled=${this.disabled}
.required=${this.required}
.enableDay=${this.selector.duration?.enable_day}
.enableMillisecond=${this.selector.duration?.enable_millisecond}
.allowNegative=${this.selector.duration?.allow_negative}
></ha-duration-input>
`;
}

View File

@@ -87,6 +87,7 @@ export class HaMediaSelector extends LitElement {
this._thumbnailUrl = brandsUrl({
domain: extractDomainFromBrandUrl(thumbnail),
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
});
} else {

View File

@@ -37,7 +37,6 @@ export class HaSelectorUiStateContent extends SubscribeMixin(LitElement) {
.disabled=${this.disabled}
.required=${this.required}
.allowName=${this.selector.ui_state_content?.allow_name || false}
.allowContext=${this.selector.ui_state_content?.allow_context || false}
></ha-entity-state-content-picker>
`;
}

View File

@@ -52,6 +52,8 @@ import "./ha-spinner";
import "./ha-svg-icon";
import "./user/ha-user-badge";
const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body;
const SORT_VALUE_URL_PATHS = {
energy: 1,
map: 2,
@@ -342,6 +344,17 @@ class HaSidebar extends SubscribeMixin(LitElement) {
}
this._calculateCounts();
if (!SUPPORT_SCROLL_IF_NEEDED) {
return;
}
if (oldHass?.panelUrl !== this.hass.panelUrl) {
const selectedEl = this.shadowRoot!.querySelector(".selected");
if (selectedEl) {
// @ts-ignore
selectedEl.scrollIntoViewIfNeeded();
}
}
}
private _calculateCounts = throttle(() => {

View File

@@ -1,7 +1,6 @@
import "@home-assistant/webawesome/dist/components/dialog/dialog";
import type WaDialog from "@home-assistant/webawesome/dist/components/dialog/dialog";
import { mdiClose } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { css, html, LitElement } from "lit";
import {
customElement,
eventOptions,
@@ -50,6 +49,7 @@ export type DialogWidth = "small" | "medium" | "large" | "full";
* @cssprop --ha-dialog-hide-duration - Hide animation duration.
* @cssprop --ha-dialog-surface-background - Dialog background color.
* @cssprop --ha-dialog-border-radius - Border radius of the dialog surface.
* @cssprop --dialog-z-index - Z-index for the dialog.
* @cssprop --dialog-surface-margin-top - Top margin for the dialog surface.
*
* @attr {boolean} open - Controls the dialog open state.
@@ -106,9 +106,6 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
@property({ type: Boolean, reflect: true, attribute: "flexcontent" })
public flexContent = false;
@property({ type: Boolean, attribute: "without-header" })
public withoutHeader = false;
@state()
private _open = false;
@@ -117,8 +114,6 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
@state()
private _bodyScrolled = false;
private _escapePressed = false;
protected get scrollableElement(): HTMLElement | null {
return this.bodyContainer;
}
@@ -144,41 +139,33 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
(this.headerTitle !== undefined ? "ha-wa-dialog-title" : undefined)
)}
aria-describedby=${ifDefined(this.ariaDescribedBy)}
@keydown=${this._handleKeyDown}
@wa-hide=${this._handleHide}
@wa-show=${this._handleShow}
@wa-after-show=${this._handleAfterShow}
@wa-after-hide=${this._handleAfterHide}
>
${!this.withoutHeader
? html` <slot name="header">
<ha-dialog-header
.subtitlePosition=${this.headerSubtitlePosition}
.showBorder=${this._bodyScrolled}
>
<slot name="headerNavigationIcon" slot="navigationIcon">
<ha-icon-button
data-dialog="close"
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
.path=${mdiClose}
></ha-icon-button>
</slot>
${this.headerTitle !== undefined
? html`<span
slot="title"
class="title"
id="ha-wa-dialog-title"
>
${this.headerTitle}
</span>`
: html`<slot name="headerTitle" slot="title"></slot>`}
${this.headerSubtitle !== undefined
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
<slot name="headerActionItems" slot="actionItems"></slot>
</ha-dialog-header>
</slot>`
: nothing}
<slot name="header">
<ha-dialog-header
.subtitlePosition=${this.headerSubtitlePosition}
.showBorder=${this._bodyScrolled}
>
<slot name="headerNavigationIcon" slot="navigationIcon">
<ha-icon-button
data-dialog="close"
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
.path=${mdiClose}
></ha-icon-button>
</slot>
${this.headerTitle !== undefined
? html`<span slot="title" class="title" id="ha-wa-dialog-title">
${this.headerTitle}
</span>`
: html`<slot name="headerTitle" slot="title"></slot>`}
${this.headerSubtitle !== undefined
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
<slot name="headerActionItems" slot="actionItems"></slot>
</ha-dialog-header>
</slot>
<div class="content-wrapper">
<div class="body ha-scrollbar" @scroll=${this._handleBodyScroll}>
<slot></slot>
@@ -221,11 +208,9 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
fireEvent(this, "after-show");
};
private _handleAfterHide = (ev: CustomEvent<{ source: Element }>) => {
if (ev.eventPhase === Event.AT_TARGET) {
this._open = false;
fireEvent(this, "closed");
}
private _handleAfterHide = () => {
this._open = false;
fireEvent(this, "closed");
};
public disconnectedCallback(): void {
@@ -238,23 +223,6 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
this._bodyScrolled = (ev.target as HTMLDivElement).scrollTop > 0;
}
private _handleKeyDown(ev: KeyboardEvent) {
if (ev.key === "Escape") {
this._escapePressed = true;
}
}
private _handleHide(ev: CustomEvent<{ source: Element }>) {
if (
this.preventScrimClose &&
this._escapePressed &&
ev.detail.source === (ev.target as WaDialog).dialog
) {
ev.preventDefault();
}
this._escapePressed = false;
}
static get styles() {
return [
...super.styles,
@@ -303,7 +271,6 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
}
wa-dialog::part(dialog) {
color: var(--primary-text-color);
min-width: var(--width, var(--full-width));
max-width: var(--width, var(--full-width));
max-height: var(

View File

@@ -24,7 +24,6 @@ import { setupLeafletMap } from "../../common/dom/setup-leaflet-map";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { DecoratedMarker } from "../../common/map/decorated_marker";
import { filterXSS } from "../../common/util/xss";
import type { HomeAssistant, ThemeMode } from "../../types";
import { isTouch } from "../../util/is_touch";
import "../ha-icon-button";
@@ -382,7 +381,7 @@ export class HaMap extends ReactiveElement {
this.hass.config
);
}
return `${filterXSS(path.name ?? "")}<br>${formattedTime}`;
return `${path.name}<br>${formattedTime}`;
}
private _drawPaths(): void {
@@ -550,7 +549,7 @@ export class HaMap extends ReactiveElement {
iconHTML = el.outerHTML;
} else {
const el = document.createElement("span");
el.textContent = title;
el.innerHTML = title;
iconHTML = el.outerHTML;
}

View File

@@ -20,7 +20,7 @@ import type {
} from "../../data/media-player";
import { haStyleDialog, haStyleDialogFixedTop } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import "../ha-wa-dialog";
import "../ha-dialog";
import "../ha-dialog-header";
import "../ha-list-item";
import "../ha-icon-button-arrow-prev";
@@ -44,8 +44,6 @@ class DialogMediaPlayerBrowse extends LitElement {
@state() _preferredLayout: MediaPlayerLayoutType = "auto";
@state() private _open = false;
@query("ha-media-player-browse") private _browser!: HaMediaPlayerBrowse;
public showDialog(params: MediaPlayerBrowseDialogParams): void {
@@ -56,11 +54,9 @@ class DialogMediaPlayerBrowse extends LitElement {
media_content_type: undefined,
},
];
this._open = true;
}
public closeDialog() {
this._open = false;
this._params = undefined;
this._navigateIds = undefined;
this._currentItem = undefined;
@@ -75,14 +71,21 @@ class DialogMediaPlayerBrowse extends LitElement {
}
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
flexcontent
<ha-dialog
open
scrimClickAction
escapeKeyAction
hideActions
flexContent
.heading=${!this._currentItem
? this.hass.localize(
"ui.components.media-browser.media-player-browser"
)
: this._currentItem.title}
@closed=${this.closeDialog}
@opened=${this._dialogOpened}
>
<ha-dialog-header show-border slot="header">
<ha-dialog-header show-border slot="heading">
${this._navigateIds.length > (this._params.minimumNavigateLevel ?? 1)
? html`
<ha-icon-button-arrow-prev
@@ -149,7 +152,7 @@ class DialogMediaPlayerBrowse extends LitElement {
<ha-icon-button
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
data-dialog="close"
dialogAction="close"
slot="actionItems"
></ha-icon-button>
</ha-dialog-header>
@@ -169,7 +172,7 @@ class DialogMediaPlayerBrowse extends LitElement {
@media-picked=${this._mediaPicked}
@media-browsed=${this._mediaBrowsed}
></ha-media-player-browse>
</ha-wa-dialog>
</ha-dialog>
`;
}
@@ -221,7 +224,8 @@ class DialogMediaPlayerBrowse extends LitElement {
haStyleDialog,
haStyleDialogFixedTop,
css`
ha-wa-dialog {
ha-dialog {
--dialog-z-index: 9;
--dialog-content-padding: 0;
}
@@ -236,9 +240,9 @@ class DialogMediaPlayerBrowse extends LitElement {
}
@media (min-width: 800px) {
ha-wa-dialog {
--ha-dialog-max-width: 800px;
--ha-dialog-max-height: calc(
ha-dialog {
--mdc-dialog-max-width: 800px;
--mdc-dialog-max-height: calc(
100vh - var(--ha-space-18) - var(--safe-area-inset-y)
);
}

View File

@@ -793,6 +793,7 @@ export class HaMediaPlayerBrowse extends LitElement {
thumbnailUrl = brandsUrl({
domain: extractDomainFromBrandUrl(thumbnailUrl),
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
});
}

View File

@@ -1,15 +1,14 @@
import { type CSSResultGroup, LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { mdiSpeaker, mdiSpeakerPause, mdiSpeakerPlay } from "@mdi/js";
import memoizeOne from "memoize-one";
import { mdiSpeaker } from "@mdi/js";
import type { HomeAssistant } from "../../types";
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
import { computeRTL } from "../../common/util/compute_rtl";
import { computeStateName } from "../../common/entity/compute_state_name";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-switch";
import "../ha-svg-icon";
import type { MediaPlayerEntity } from "../../data/media-player";
@customElement("ha-media-player-toggle")
class HaMediaPlayerToggle extends LitElement {
@@ -21,61 +20,15 @@ class HaMediaPlayerToggle extends LitElement {
@property({ type: Boolean }) public disabled = false;
private _computeDisplayData = memoizeOne(
(
entityId: string,
entities: HomeAssistant["entities"],
devices: HomeAssistant["devices"],
areas: HomeAssistant["areas"],
floors: HomeAssistant["floors"],
isRTL: boolean,
stateObj: HomeAssistant["states"][string]
) => {
const [entityName, deviceName, areaName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
entities,
devices,
areas,
floors
);
const primary = entityName || deviceName || entityId;
const secondary = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(isRTL ? " ◂ " : " ▸ ");
return { primary, secondary };
}
);
protected render() {
const stateObj = this.hass.states[this.entityId];
let icon = mdiSpeaker;
if (stateObj.state === "playing") {
icon = mdiSpeakerPlay;
} else if (stateObj.state === "paused") {
icon = mdiSpeakerPause;
}
const isRTL = computeRTL(this.hass);
const { primary, secondary } = this._computeDisplayData(
this.entityId,
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors,
isRTL,
stateObj
);
return html`<div class="list-item">
<ha-svg-icon .path=${icon}></ha-svg-icon>
<ha-svg-icon .path=${mdiSpeaker}></ha-svg-icon>
<div class="info">
<div class="main-text">${primary}</div>
<div class="secondary-text">${secondary}</div>
<div class="main-text">${computeStateName(stateObj)}</div>
<div class="secondary-text">
${this._formatSecondaryText(stateObj as MediaPlayerEntity)}
</div>
</div>
<ha-switch
.disabled=${this.disabled}
@@ -85,6 +38,16 @@ class HaMediaPlayerToggle extends LitElement {
</div>`;
}
private _formatSecondaryText(stateObj: MediaPlayerEntity): string {
if (stateObj.state !== "playing") {
return this.hass.localize("ui.card.media_player.idle");
}
return [stateObj.attributes.media_title, stateObj.attributes.media_artist]
.filter((segment) => segment)
.join(" · ");
}
static get styles(): CSSResultGroup {
return [
css`

View File

@@ -20,7 +20,7 @@ import { computeDomain } from "../../common/entity/compute_domain";
import { computeEntityName } from "../../common/entity/compute_entity_name";
import { getEntityContext } from "../../common/entity/context/get_entity_context";
import { computeRTL } from "../../common/util/compute_rtl";
import type { AreaRegistryEntry } from "../../data/area/area_registry";
import type { AreaRegistryEntry } from "../../data/area_registry";
import { getConfigEntry } from "../../data/config_entries";
import { labelsContext } from "../../data/context";
import type { DeviceRegistryEntry } from "../../data/device/device_registry";

View File

@@ -1,51 +0,0 @@
import { customElement, property } from "lit/decorators";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html } from "lit";
import { haStyle } from "../resources/styles";
import type { HomeAssistant } from "../types";
import { voiceAssistants } from "../data/expose";
import { brandsUrl } from "../util/brands-url";
@customElement("voice-assistant-brand-icon")
export class VoiceAssistantBrandicon extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public voiceAssistantId!: string;
protected render() {
return html`
<img
class="logo"
alt=${voiceAssistants[this.voiceAssistantId].name}
src=${brandsUrl({
domain: voiceAssistants[this.voiceAssistantId].domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
`;
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
.logo {
position: relative;
height: 24px;
margin-right: 16px;
margin-inline-end: 16px;
margin-inline-start: initial;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"voice-assistant-brand-icon": VoiceAssistantBrandicon;
}
}

View File

@@ -1,204 +0,0 @@
import { mdiTextureBox } from "@mdi/js";
import { computeAreaName } from "../../common/entity/compute_area_name";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeFloorName } from "../../common/entity/compute_floor_name";
import { getAreaContext } from "../../common/entity/context/get_area_context";
import type { HaDevicePickerDeviceFilterFunc } from "../../components/device/ha-device-picker";
import type { PickerComboBoxItem } from "../../components/ha-picker-combo-box";
import type { FuseWeightedKey } from "../../resources/fuseMultiTerm";
import type { HomeAssistant } from "../../types";
import {
getDeviceEntityDisplayLookup,
type DeviceEntityDisplayLookup,
type DeviceRegistryEntry,
} from "../device/device_registry";
import type { HaEntityPickerEntityFilterFunc } from "../entity/entity";
import type { EntityRegistryDisplayEntry } from "../entity/entity_registry";
export const getAreas = (
haAreas: HomeAssistant["areas"],
haFloors: HomeAssistant["floors"],
haDevices: HomeAssistant["devices"],
haEntities: HomeAssistant["entities"],
haStates: HomeAssistant["states"],
includeDomains?: string[],
excludeDomains?: string[],
includeDeviceClasses?: string[],
deviceFilter?: HaDevicePickerDeviceFilterFunc,
entityFilter?: HaEntityPickerEntityFilterFunc,
excludeAreas?: string[],
idPrefix = ""
): PickerComboBoxItem[] => {
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
let inputDevices: DeviceRegistryEntry[] | undefined;
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
const areas = Object.values(haAreas);
const devices = Object.values(haDevices);
const entities = Object.values(haEntities);
if (
includeDomains ||
excludeDomains ||
includeDeviceClasses ||
deviceFilter ||
entityFilter
) {
deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
inputDevices = devices;
inputEntities = entities.filter((entity) => entity.area_id);
if (includeDomains) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) =>
includeDomains.includes(computeDomain(entity.entity_id))
);
});
inputEntities = inputEntities!.filter((entity) =>
includeDomains.includes(computeDomain(entity.entity_id))
);
}
if (excludeDomains) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return true;
}
return entities.every(
(entity) => !excludeDomains.includes(computeDomain(entity.entity_id))
);
});
inputEntities = inputEntities!.filter(
(entity) => !excludeDomains.includes(computeDomain(entity.entity_id))
);
}
if (includeDeviceClasses) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = haStates[entity.entity_id];
if (!stateObj) {
return false;
}
return (
stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class)
);
});
});
inputEntities = inputEntities!.filter((entity) => {
const stateObj = haStates[entity.entity_id];
return (
stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class)
);
});
}
if (deviceFilter) {
inputDevices = inputDevices!.filter((device) => deviceFilter!(device));
}
if (entityFilter) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = haStates[entity.entity_id];
if (!stateObj) {
return false;
}
return entityFilter(stateObj);
});
});
inputEntities = inputEntities!.filter((entity) => {
const stateObj = haStates[entity.entity_id];
if (!stateObj) {
return false;
}
return entityFilter!(stateObj);
});
}
}
let outputAreas = areas;
let areaIds: string[] | undefined;
if (inputDevices) {
areaIds = inputDevices
.filter((device) => device.area_id)
.map((device) => device.area_id!);
}
if (inputEntities) {
areaIds = (areaIds ?? []).concat(
inputEntities
.filter((entity) => entity.area_id)
.map((entity) => entity.area_id!)
);
}
if (areaIds) {
outputAreas = outputAreas.filter((area) => areaIds!.includes(area.area_id));
}
if (excludeAreas) {
outputAreas = outputAreas.filter(
(area) => !excludeAreas!.includes(area.area_id)
);
}
const items = outputAreas.map<PickerComboBoxItem>((area) => {
const { floor } = getAreaContext(area, haFloors);
const floorName = floor ? computeFloorName(floor) : undefined;
const areaName = computeAreaName(area);
return {
id: `${idPrefix}${area.area_id}`,
primary: areaName || area.area_id,
secondary: floorName,
icon: area.icon || undefined,
icon_path: area.icon ? undefined : mdiTextureBox,
search_labels: {
areaId: area.area_id,
aliases: area.aliases.join(" "),
},
};
});
return items;
};
export const areaComboBoxKeys: FuseWeightedKey[] = [
{
name: "primary",
weight: 10,
},
{
name: "search_labels.aliases",
weight: 8,
},
{
name: "secondary",
weight: 6,
},
{
name: "search_labels.domain",
weight: 4,
},
{
name: "search_labels.areaId",
weight: 2,
},
];

View File

@@ -6,7 +6,7 @@ import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-dev
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
import type { HomeAssistant } from "../types";
import type { AreaRegistryEntry } from "./area/area_registry";
import type { AreaRegistryEntry } from "./area_registry";
import {
getDeviceEntityDisplayLookup,
type DeviceEntityDisplayLookup,

View File

@@ -1,12 +1,12 @@
import type { HomeAssistant } from "../../types";
import type { DeviceRegistryEntry } from "../device/device_registry";
import type { HomeAssistant } from "../types";
import type { DeviceRegistryEntry } from "./device/device_registry";
import type {
EntityRegistryDisplayEntry,
EntityRegistryEntry,
} from "../entity/entity_registry";
import type { RegistryEntry } from "../registry";
} from "./entity/entity_registry";
import type { RegistryEntry } from "./registry";
export { subscribeAreaRegistry } from "../ws-area_registry";
export { subscribeAreaRegistry } from "./ws-area_registry";
export interface AreaRegistryEntry extends RegistryEntry {
aliases: string[];

View File

@@ -449,9 +449,16 @@ const getEnergyData = async (
const allStatIDs = [...energyStatIds, ...waterStatIds, ...powerStatIds];
const dayDifference = differenceInDays(end || new Date(), start);
const period = getSuggestedPeriod(start, end);
const finePeriod = getSuggestedPeriod(start, end, true);
const period =
isFirstDayOfMonth(start) &&
(!end || isLastDayOfMonth(end)) &&
dayDifference > 35
? "month"
: dayDifference > 2
? "day"
: "hour";
const finePeriod =
dayDifference > 64 ? "day" : dayDifference > 8 ? "hour" : "5minute";
const statsMetadata: Record<string, StatisticsMetaData> = {};
const statsMetadataArray = allStatIDs.length
@@ -582,7 +589,7 @@ const getEnergyData = async (
consumptionStatIDs,
co2SignalEntity,
end,
period
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
);
if (compare) {
_fossilEnergyConsumptionCompare = getFossilEnergyConsumption(
@@ -591,7 +598,7 @@ const getEnergyData = async (
consumptionStatIDs,
co2SignalEntity,
endCompare,
period
dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour"
);
}
}
@@ -1420,22 +1427,3 @@ export const formatPowerShort = (
units[unitIndex]
);
};
export function getSuggestedPeriod(
start: Date,
end?: Date,
fine = false
): "5minute" | "hour" | "day" | "month" {
const dayDifference = differenceInDays(end || new Date(), start);
if (fine) {
return dayDifference > 64 ? "day" : dayDifference > 8 ? "hour" : "5minute";
}
return isFirstDayOfMonth(start) &&
(!end || isLastDayOfMonth(end)) &&
dayDifference > 35
? "month"
: dayDifference > 2
? "day"
: "hour";
}

View File

@@ -69,7 +69,6 @@ export const DOMAIN_ATTRIBUTES_UNITS = {
current_humidity: "%",
min_humidity: "%",
max_humidity: "%",
target_humidity_step: "%",
},
light: {
color_temp: "mired",

View File

@@ -1,6 +1,4 @@
import type { HomeAssistant } from "../types";
import type { EntityRegistryEntry } from "./entity/entity_registry";
import { entityRegistryByEntityId } from "./entity/entity_registry";
export const voiceAssistants = {
conversation: { domain: "assist_pipeline", name: "Assist" },
@@ -54,13 +52,3 @@ export const listExposedEntities = (hass: HomeAssistant) =>
hass.callWS<{ exposed_entities: Record<string, ExposeEntitySettings> }>({
type: "homeassistant/expose_entity/list",
});
export const getEntityVoiceAssistantsIds = (
entityRegistry: EntityRegistryEntry[],
entityId: string
) => {
const entity = entityRegistryByEntityId(entityRegistry)[entityId];
return Object.keys(voiceAssistants).filter(
(vaKey) => entity?.options?.[vaKey]?.should_expose
);
};

View File

@@ -1,5 +1,5 @@
import type { HomeAssistant } from "../types";
import type { AreaRegistryEntry } from "./area/area_registry";
import type { AreaRegistryEntry } from "./area_registry";
import type { RegistryEntry } from "./registry";
export { subscribeAreaRegistry } from "./ws-area_registry";

View File

@@ -16,7 +16,6 @@ export type HumidifierEntity = HassEntityBase & {
mode?: string;
action?: HumidifierAction;
available_modes?: string[];
target_humidity_step?: number;
};
};

View File

@@ -52,9 +52,6 @@ export interface BaseActionConfig {
export interface ConfirmationRestrictionConfig {
text?: string;
title?: string;
confirm_text?: string;
dismiss_text?: string;
exemptions?: RestrictionConfig[];
}

View File

@@ -49,7 +49,6 @@ export interface LovelaceBaseViewConfig {
title?: string;
path?: string;
icon?: string;
show_icon_and_title?: boolean;
theme?: string;
panel?: boolean;
background?: string | LovelaceViewBackgroundConfig;

View File

@@ -11,8 +11,6 @@ import {
mdiViewDashboard,
} from "@mdi/js";
import type { HomeAssistant, PanelInfo } from "../types";
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
import type { LocalizeKeys } from "../common/translations/localize";
/** Panel to show when no panel is picked. */
export const DEFAULT_PANEL = "lovelace";
@@ -74,40 +72,6 @@ export const getPanelTitleFromUrlPath = (
return getPanelTitle(hass, panel);
};
/**
* Get subpage title for config panel routes.
* Returns the specific subpage title (e.g., "Automations") if found,
* or undefined to fall back to the panel title (e.g., "Settings").
*
* @param hass HomeAssistant instance
* @param path Full route path (e.g., "/config/automation/dashboard")
* @param configSections Config sections metadata for resolving subpage titles
* @returns Localized subpage title, or undefined if not found
*/
export const getConfigSubpageTitle = (
hass: HomeAssistant,
path: string,
configSections: Record<string, PageNavigation[]>
): string | undefined => {
// Search through all config section groups for a matching path
for (const sectionGroup of Object.values(configSections)) {
const pageNav = sectionGroup.find((nav) => path.startsWith(nav.path));
if (pageNav) {
if (pageNav.translationKey) {
const localized = hass.localize(pageNav.translationKey as LocalizeKeys);
if (localized) {
return localized;
}
}
if (pageNav.name) {
return pageNav.name;
}
}
}
return undefined;
};
export const getPanelIcon = (panel: PanelInfo): string | undefined => {
if (!panel.icon) {
switch (panel.component_name) {

View File

@@ -1,327 +0,0 @@
import {
mdiKeyboard,
mdiNavigationVariant,
mdiPuzzle,
mdiReload,
mdiServerNetwork,
mdiStorePlus,
} from "@mdi/js";
import { canShowPage } from "../common/config/can_show_page";
import { componentsWithService } from "../common/config/components_with_service";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
import { configSections } from "../panels/config/ha-panel-config";
import type { HomeAssistant } from "../types";
import type { HassioAddonInfo } from "./hassio/addon";
import { domainToName } from "./integration";
import { getPanelIcon, getPanelNameTranslationKey } from "./panel";
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
export interface NavigationComboBoxItem extends PickerComboBoxItem {
path: string;
image?: string;
iconColor?: string;
}
export interface BaseNavigationCommand {
path: string;
primary: string;
icon_path?: string;
iconPath?: string;
iconColor?: string;
image?: string;
}
export interface ActionCommandComboBoxItem extends PickerComboBoxItem {
action: string;
domain?: string;
}
export interface NavigationInfo extends PageNavigation {
primary: string;
}
const generateNavigationPanelCommands = (
localize: HomeAssistant["localize"],
panels: HomeAssistant["panels"],
addons?: HassioAddonInfo[]
): BaseNavigationCommand[] =>
Object.entries(panels)
.filter(
([panelKey]) => panelKey !== "_my_redirect" && panelKey !== "hassio"
)
.map(([_panelKey, panel]) => {
const translationKey = getPanelNameTranslationKey(panel);
const icon = getPanelIcon(panel) || "mdi:view-dashboard";
const primary = localize(translationKey) || panel.title || panel.url_path;
let image: string | undefined;
if (addons) {
const addon = addons.find(({ slug }) => slug === panel.url_path);
if (addon) {
image = addon.icon
? `/api/hassio/addons/${addon.slug}/icon`
: undefined;
}
}
return {
primary,
icon,
image,
path: `/${panel.url_path}`,
};
});
const getNavigationInfoFromConfig = (
localize: HomeAssistant["localize"],
page: PageNavigation
): NavigationInfo | undefined => {
const path = page.path.substring(1);
let name = path.substring(path.indexOf("/") + 1);
name = name.indexOf("/") > -1 ? name.substring(0, name.indexOf("/")) : name;
const caption =
(name && localize(`ui.dialogs.quick-bar.commands.navigation.${name}`)) ||
// @ts-expect-error
(page.translationKey && localize(page.translationKey));
if (caption) {
return { ...page, primary: caption };
}
return undefined;
};
const generateNavigationConfigSectionCommands = (
hass: HomeAssistant
): BaseNavigationCommand[] => {
if (!hass.user?.is_admin) {
return [];
}
const items: NavigationInfo[] = [];
Object.values(configSections).forEach((sectionPages) => {
sectionPages.forEach((page) => {
if (!canShowPage(hass, page)) {
return;
}
const info = getNavigationInfoFromConfig(hass.localize, page);
if (!info) {
return;
}
// Add to list, but only if we do not already have an entry for the same path and component
if (items.some((e) => e.path === info.path)) {
return;
}
items.push(info);
});
});
return items;
};
const finalizeNavigationCommands = (
localize: HomeAssistant["localize"],
items: BaseNavigationCommand[]
): NavigationComboBoxItem[] =>
items.map((item, index) => {
const secondary = localize(
"ui.dialogs.quick-bar.commands.types.navigation"
);
return {
id: `navigation_${index}_${item.path}`,
icon_path: item.iconPath || mdiNavigationVariant,
secondary,
sorting_label: `${item.primary}_${secondary}`,
...item,
};
});
export const generateNavigationCommands = (
hass: HomeAssistant,
addons?: HassioAddonInfo[]
): NavigationComboBoxItem[] => {
const panelItems = generateNavigationPanelCommands(
hass.localize,
hass.panels,
addons
);
const sectionItems = generateNavigationConfigSectionCommands(hass);
const supervisorItems: BaseNavigationCommand[] = [];
if (hass.user?.is_admin && isComponentLoaded(hass, "hassio")) {
supervisorItems.push({
path: "/hassio/store",
icon_path: mdiStorePlus,
primary: hass.localize(
"ui.dialogs.quick-bar.commands.navigation.addon_store"
),
});
supervisorItems.push({
path: "/hassio/dashboard",
icon_path: mdiPuzzle,
primary: hass.localize(
"ui.dialogs.quick-bar.commands.navigation.addon_dashboard"
),
});
if (addons) {
for (const addon of addons.filter((a) => a.version)) {
supervisorItems.push({
path: `/hassio/addon/${addon.slug}`,
image: addon.icon
? `/api/hassio/addons/${addon.slug}/icon`
: undefined,
primary: hass.localize(
"ui.dialogs.quick-bar.commands.navigation.addon_info",
{ addon: addon.name }
),
});
}
}
}
const additionalItems = [
{
path: "",
primary: hass.localize(
"ui.dialogs.quick-bar.commands.navigation.shortcuts"
),
icon_path: mdiKeyboard,
},
];
return finalizeNavigationCommands(hass.localize, [
...panelItems,
...sectionItems,
...supervisorItems,
...additionalItems,
]);
};
const generateReloadCommands = (
hass: HomeAssistant
): ActionCommandComboBoxItem[] => {
// Get all domains that have a direct "reload" service
const reloadableDomains = componentsWithService(hass, "reload");
const commands = reloadableDomains.map((domain) => ({
primary:
hass.localize(`ui.dialogs.quick-bar.commands.reload.${domain}`) ||
hass.localize("ui.dialogs.quick-bar.commands.reload.reload", {
domain: domainToName(hass.localize, domain),
}),
domain,
action: "reload",
icon_path: mdiReload,
secondary: hass.localize(`ui.dialogs.quick-bar.commands.types.reload`),
}));
// Add "frontend.reload_themes"
commands.push({
primary: hass.localize("ui.dialogs.quick-bar.commands.reload.themes"),
domain: "frontend",
action: "reload_themes",
icon_path: mdiReload,
secondary: hass.localize("ui.dialogs.quick-bar.commands.types.reload"),
});
// Add "homeassistant.reload_core_config"
commands.push({
primary: hass.localize("ui.dialogs.quick-bar.commands.reload.core"),
domain: "homeassistant",
action: "reload_core_config",
icon_path: mdiReload,
secondary: hass.localize("ui.dialogs.quick-bar.commands.types.reload"),
});
// Add "homeassistant.reload_all"
commands.push({
primary: hass.localize("ui.dialogs.quick-bar.commands.reload.all"),
domain: "homeassistant",
action: "reload_all",
icon_path: mdiReload,
secondary: hass.localize("ui.dialogs.quick-bar.commands.types.reload"),
});
return commands.map((command, index) => ({
...command,
id: `command_${index}_${command.primary}`,
sorting_label: `${command.primary}_${command.secondary}_${command.domain}`,
}));
};
const generateServerControlCommands = (
hass: HomeAssistant
): ActionCommandComboBoxItem[] => {
const serverActions = ["restart", "stop"] as const;
return serverActions.map((action, index) => {
const primary = hass.localize(
"ui.dialogs.quick-bar.commands.server_control.perform_action",
{
action: hass.localize(
`ui.dialogs.quick-bar.commands.server_control.${action}`
),
}
);
const secondary = hass.localize(
"ui.dialogs.quick-bar.commands.types.server_control"
);
return {
id: `server_control_${index}_${action}`,
primary,
domain: "homeassistant",
icon_path: mdiServerNetwork,
secondary,
sorting_label: `${primary}_${secondary}_${action}`,
action,
};
});
};
export const generateActionCommands = (
hass: HomeAssistant
): ActionCommandComboBoxItem[] => [
...generateReloadCommands(hass),
...generateServerControlCommands(hass),
];
export const commandComboBoxKeys: FuseWeightedKey[] = [
{
name: "primary",
weight: 10,
},
{
name: "domain",
weight: 8,
},
{
name: "secondary",
weight: 6,
},
];
export const navigateComboBoxKeys: FuseWeightedKey[] = [
{
name: "primary",
weight: 10,
},
{
name: "path",
weight: 8,
},
{
name: "secondary",
weight: 6,
},
];

View File

@@ -221,7 +221,6 @@ export interface DurationSelector {
duration: {
enable_day?: boolean;
enable_millisecond?: boolean;
allow_negative?: boolean;
} | null;
}
@@ -377,7 +376,7 @@ interface SelectBoxOptionImage {
}
export interface SelectOption {
value: string;
value: any;
label: string;
description?: string;
image?: string | SelectBoxOptionImage;
@@ -501,7 +500,6 @@ export interface UiStateContentSelector {
ui_state_content: {
entity_id?: string;
allow_name?: boolean;
allow_context?: boolean;
} | null;
}

View File

@@ -3,8 +3,8 @@ import { computeDomain } from "../common/entity/compute_domain";
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
import type { HomeAssistant } from "../types";
import type { AreaRegistryEntry } from "./area/area_registry";
import type { FloorComboBoxItem } from "./area_floor_picker";
import type { AreaRegistryEntry } from "./area_registry";
import type { DevicePickerItem } from "./device/device_picker";
import type { DeviceRegistryEntry } from "./device/device_registry";
import type { HaEntityPickerEntityFilterFunc } from "./entity/entity";

View File

@@ -1,18 +0,0 @@
import type { HomeAssistant, ThemeSettings } from "../types";
import { saveFrontendUserData, subscribeFrontendUserData } from "./frontend";
declare global {
interface FrontendUserData {
theme: ThemeSettings;
}
}
export const subscribeThemePreferences = (
hass: HomeAssistant,
callback: (data: { value: ThemeSettings | null }) => void
) => subscribeFrontendUserData(hass.connection, "theme", callback);
export const saveThemePreferences = (
hass: HomeAssistant,
data: ThemeSettings
) => saveFrontendUserData(hass.connection, "theme", data);

View File

@@ -44,27 +44,14 @@ export const updateUsesProgress = (entity: UpdateEntity): boolean =>
supportsFeature(entity, UpdateEntityFeature.PROGRESS) &&
entity.attributes.update_percentage !== null;
export const updateAvailable = (
entity: UpdateEntity,
showSkipped = false
): boolean =>
entity.state === BINARY_STATE_ON ||
(showSkipped && Boolean(entity.attributes.skipped_version));
export const updateCanInstall = (
entity: UpdateEntity,
showSkipped = false
): boolean =>
updateAvailable(entity, showSkipped) &&
(entity.state === BINARY_STATE_ON ||
(showSkipped && Boolean(entity.attributes.skipped_version))) &&
supportsFeature(entity, UpdateEntityFeature.INSTALL);
export const updateCanNotInstall = (
entity: UpdateEntity,
showSkipped = false
): boolean =>
updateAvailable(entity, showSkipped) &&
!supportsFeature(entity, UpdateEntityFeature.INSTALL);
export const latestVersionIsSkipped = (entity: UpdateEntity): boolean =>
!!(
entity.attributes.latest_version &&
@@ -121,17 +108,13 @@ export const filterUpdateEntities = (
);
});
export const filterUpdateEntitiesParameterized = (
export const filterUpdateEntitiesWithInstall = (
entities: HassEntities,
showSkipped = false,
showNotInstallable = false
showSkipped = false
) =>
filterUpdateEntities(entities).filter((entity) => {
if (showNotInstallable) {
return updateCanNotInstall(entity, showSkipped);
}
return updateCanInstall(entity, showSkipped);
});
filterUpdateEntities(entities).filter((entity) =>
updateCanInstall(entity, showSkipped)
);
export const checkForEntityUpdates = async (
element: HTMLElement,

View File

@@ -2,7 +2,7 @@ import type { Connection } from "home-assistant-js-websocket";
import { createCollection } from "home-assistant-js-websocket";
import type { Store } from "home-assistant-js-websocket/dist/store";
import { debounce } from "../common/util/debounce";
import type { AreaRegistryEntry } from "./area/area_registry";
import type { AreaRegistryEntry } from "./area_registry";
const fetchAreaRegistry = (conn: Connection) =>
conn.sendMessagePromise<AreaRegistryEntry[]>({

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,14 @@
import { fireEvent } from "../../common/dom/fire_event";
export type QuickBarSection =
| "entity"
| "device"
| "area"
| "navigate"
| "command";
export const enum QuickBarMode {
Command = "command",
Device = "device",
Entity = "entity",
}
export interface QuickBarParams {
entityFilter?: string;
mode?: QuickBarSection;
mode?: QuickBarMode;
hint?: string;
}

View File

@@ -1,10 +1,12 @@
import { css, html, LitElement } from "lit";
import { mdiAppleKeyboardCommand } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../common/dom/fire_event";
import type { LocalizeKeys } from "../../common/translations/localize";
import "../../components/ha-alert";
import { createCloseHeading } from "../../components/ha-dialog";
import "../../components/ha-svg-icon";
import "../../components/ha-wa-dialog";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { isMac } from "../../util/is_mac";
@@ -37,10 +39,6 @@ const _SHORTCUTS: Section[] = [
{
textTranslationKey: "ui.dialogs.shortcuts.searching.on_any_page",
},
{
shortcut: [CTRL_CMD, "K"],
descriptionTranslationKey: "ui.dialogs.shortcuts.searching.search",
},
{
shortcut: ["C"],
descriptionTranslationKey:
@@ -156,10 +154,6 @@ const _SHORTCUTS: Section[] = [
shortcut: ["M"],
descriptionTranslationKey: "ui.dialogs.shortcuts.other.my_link",
},
{
shortcut: ["Shift", "/"],
descriptionTranslationKey: "ui.dialogs.shortcuts.other.show_shortcuts",
},
],
},
];
@@ -168,22 +162,17 @@ const _SHORTCUTS: Section[] = [
class DialogShortcuts extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _open = false;
@state() private _opened = false;
public async showDialog(): Promise<void> {
this._open = true;
this._opened = true;
}
private _dialogClosed() {
this._open = false;
public async closeDialog(): Promise<void> {
this._opened = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
public async closeDialog() {
this._open = false;
return true;
}
private _renderShortcut(
shortcutKeys: ShortcutString[],
descriptionKey: LocalizeKeys
@@ -195,7 +184,9 @@ class DialogShortcuts extends LitElement {
html`<span
>${shortcutKey === CTRL_CMD
? isMac
? "⌘"
? html`<ha-svg-icon
.path=${mdiAppleKeyboardCommand}
></ha-svg-icon>`
: this.hass.localize("ui.panel.config.automation.editor.ctrl")
: typeof shortcutKey === "string"
? shortcutKey
@@ -210,11 +201,20 @@ class DialogShortcuts extends LitElement {
}
protected render() {
if (!this._opened) {
return nothing;
}
return html`
<ha-wa-dialog
.open=${this._open}
@closed=${this._dialogClosed}
.headerTitle=${this.hass.localize("ui.dialogs.shortcuts.title")}
<ha-dialog
open
hideActions
@closed=${this.closeDialog}
defaultAction="ignore"
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.dialogs.shortcuts.title")
)}
>
<div class="content">
${_SHORTCUTS.map(
@@ -237,7 +237,7 @@ class DialogShortcuts extends LitElement {
)}
</div>
<ha-alert slot="footer">
<ha-alert>
${this.hass.localize("ui.dialogs.shortcuts.enable_shortcuts_hint", {
user_profile: html`<a href="/profile/general#shortcuts"
>${this.hass.localize(
@@ -246,12 +246,25 @@ class DialogShortcuts extends LitElement {
>`,
})}
</ha-alert>
</ha-wa-dialog>
</ha-dialog>
`;
}
static styles = [
haStyleDialog,
css`
ha-dialog {
--dialog-z-index: 15;
}
h3:first-of-type {
margin-top: 0;
}
.content {
margin-bottom: 24px;
}
.shortcut {
display: flex;
flex-direction: row;
@@ -273,10 +286,6 @@ class DialogShortcuts extends LitElement {
ha-svg-icon {
width: 12px;
}
ha-alert a {
color: var(--primary-color);
}
`,
];
}

View File

@@ -28,7 +28,6 @@ window.loadES5Adapter = () => {
};
let panelEl: HTMLElement | undefined;
let initialized = false;
function setProperties(properties) {
if (!panelEl) {
@@ -129,23 +128,13 @@ function initialize(
});
}
function handleReady() {
if (initialized) return;
initialized = true;
window.parent.customPanel!.registerIframe(initialize, setProperties);
}
document.addEventListener(
"DOMContentLoaded",
() => window.parent.customPanel!.registerIframe(initialize, setProperties),
{ once: true }
);
// Initial load
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", handleReady, { once: true });
} else {
handleReady();
}
window.addEventListener("pageshow", handleReady);
window.addEventListener("pagehide", () => {
initialized = false;
window.addEventListener("unload", () => {
// allow disconnected callback to fire
while (document.body.lastChild) {
document.body.removeChild(document.body.lastChild);

View File

@@ -152,7 +152,7 @@ export const provideHass = (
for (const ent of ensureArray(newEntities)) {
hass().entities[ent.entityId] = {
entity_id: ent.entityId,
name: ent.attributes.friendly_name || null,
name: ent.name,
icon: ent.icon,
platform: "demo",
labels: [],

View File

@@ -21,8 +21,8 @@ import "../../../components/ha-wa-dialog";
import type {
AreaRegistryEntry,
AreaRegistryEntryMutableParams,
} from "../../../data/area/area_registry";
import { deleteAreaRegistryEntry } from "../../../data/area/area_registry";
} from "../../../data/area_registry";
import { deleteAreaRegistryEntry } from "../../../data/area_registry";
import {
SENSOR_DEVICE_CLASS_HUMIDITY,
SENSOR_DEVICE_CLASS_TEMPERATURE,

View File

@@ -14,16 +14,16 @@ import "../../../components/ha-button";
import "../../../components/ha-dialog-footer";
import "../../../components/ha-floor-icon";
import "../../../components/ha-icon";
import "../../../components/ha-wa-dialog";
import "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import "../../../components/ha-sortable";
import "../../../components/ha-svg-icon";
import "../../../components/ha-wa-dialog";
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
import type { AreaRegistryEntry } from "../../../data/area_registry";
import {
reorderAreaRegistryEntries,
updateAreaRegistryEntry,
} from "../../../data/area/area_registry";
} from "../../../data/area_registry";
import { reorderFloorRegistryEntries } from "../../../data/floor_registry";
import { haStyle, haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";

View File

@@ -18,7 +18,7 @@ import "../../../components/ha-settings-row";
import "../../../components/ha-svg-icon";
import "../../../components/ha-textfield";
import "../../../components/ha-wa-dialog";
import { updateAreaRegistryEntry } from "../../../data/area/area_registry";
import { updateAreaRegistryEntry } from "../../../data/area_registry";
import type {
FloorRegistryEntry,
FloorRegistryEntryMutableParams,

View File

@@ -23,11 +23,11 @@ import "../../../components/ha-icon-next";
import "../../../components/ha-list";
import "../../../components/ha-list-item";
import "../../../components/ha-tooltip";
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
import type { AreaRegistryEntry } from "../../../data/area_registry";
import {
deleteAreaRegistryEntry,
updateAreaRegistryEntry,
} from "../../../data/area/area_registry";
} from "../../../data/area_registry";
import type { AutomationEntity } from "../../../data/automation";
import { fullEntitiesContext } from "../../../data/context";
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";

View File

@@ -31,12 +31,12 @@ import "../../../components/ha-list-item";
import "../../../components/ha-sortable";
import type { HaSortableOptions } from "../../../components/ha-sortable";
import "../../../components/ha-svg-icon";
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
import type { AreaRegistryEntry } from "../../../data/area_registry";
import {
createAreaRegistryEntry,
reorderAreaRegistryEntries,
updateAreaRegistryEntry,
} from "../../../data/area/area_registry";
} from "../../../data/area_registry";
import type { FloorRegistryEntry } from "../../../data/floor_registry";
import {
createFloorRegistryEntry,

View File

@@ -2,7 +2,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
import type {
AreaRegistryEntry,
AreaRegistryEntryMutableParams,
} from "../../../data/area/area_registry";
} from "../../../data/area_registry";
export interface AreaRegistryDetailDialogParams {
entry?: AreaRegistryEntry;

View File

@@ -58,7 +58,6 @@ import { fullEntitiesContext } from "../../../../data/context";
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
import type {
Action,
DeviceAction,
NonConditionAction,
RepeatAction,
ServiceAction,
@@ -234,13 +233,6 @@ export default class HaAutomationActionRow extends LitElement {
private _renderRow() {
const type = getAutomationActionType(this.action);
const target =
type === "service" && "target" in this.action
? (this.action as ServiceAction).target
: type === "device_id" && (this.action as DeviceAction).device_id
? { device_id: (this.action as DeviceAction).device_id }
: undefined;
return html`
${type === "service" && "action" in this.action && this.action.action
? html`
@@ -262,7 +254,9 @@ export default class HaAutomationActionRow extends LitElement {
${capitalizeFirstLetter(
describeAction(this.hass, this._entityReg, this.action)
)}
${target ? this._renderTargets(target) : nothing}
${type === "service" && "target" in this.action
? this._renderTargets((this.action as ServiceAction).target)
: nothing}
</h3>
<slot name="icons" slot="icons"></slot>
@@ -863,7 +857,6 @@ export default class HaAutomationActionRow extends LitElement {
}
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
ev.stopPropagation();
const action = ev.detail?.item?.value;
if (!action) {

View File

@@ -57,11 +57,11 @@ import {
ACTION_COLLECTIONS,
ACTION_ICONS,
} from "../../../data/action";
import type { FloorComboBoxItem } from "../../../data/area_floor_picker";
import {
getAreaDeviceLookup,
getAreaEntityLookup,
} from "../../../data/area/area_registry";
import type { FloorComboBoxItem } from "../../../data/area_floor_picker";
} from "../../../data/area_registry";
import {
DYNAMIC_PREFIX,
getValueFromDynamic,
@@ -232,8 +232,6 @@ class DialogAddAutomationElement
private _configEntryLookup: Record<string, ConfigEntry> = {};
private _closing = false;
// #endregion variables
// #region lifecycle
@@ -349,8 +347,6 @@ class DialogAddAutomationElement
}
}
this._closing = true;
// if dialog is closed, but root level isn't active, clean up history state
if (mainWindow.history.state?.dialogData) {
this._open = false;
@@ -364,7 +360,6 @@ class DialogAddAutomationElement
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
this._open = true;
this._closing = false;
this._params = undefined;
this._selectedCollectionIndex = undefined;
this._selectedGroup = undefined;
@@ -436,24 +431,6 @@ class DialogAddAutomationElement
// #region render
private _getEmptyNote(automationElementType: string) {
if (
automationElementType !== "trigger" &&
automationElementType !== "condition"
) {
return undefined;
}
return this.hass.localize(
`ui.panel.config.automation.editor.${automationElementType}s.no_items_for_target_note`,
{
labs_link: html`<a href="/config/labs" @click=${this._close}
>${this.hass.localize("ui.panel.config.labs.caption")}</a
>`,
}
);
}
protected render() {
if (!this._params) {
return nothing;
@@ -719,7 +696,6 @@ class DialogAddAutomationElement
.emptyLabel=${this.hass.localize(
`ui.panel.config.automation.editor.${automationElementType}s.no_items_for_target`
)}
.emptyNote=${this._getEmptyNote(automationElementType)}
.tooltipDescription=${this._tab === "targets"}
.target=${(this._tab === "targets" &&
this._selectedTarget &&
@@ -1715,9 +1691,9 @@ class DialogAddAutomationElement
// #region interaction
private _close = () => {
private _close() {
this._open = false;
};
}
private _back() {
mainWindow.history.back();
@@ -1899,10 +1875,7 @@ class DialogAddAutomationElement
}
private _handleClosed() {
// if closing isn't already in progress, close the dialog
if (!this._closing) {
this.closeDialog();
}
this.closeDialog();
}
// #region interaction
@@ -2089,7 +2062,6 @@ class DialogAddAutomationElement
.content.column {
flex-direction: column;
gap: var(--ha-space-3);
}
ha-md-list {

View File

@@ -28,10 +28,6 @@ import "../../../../components/ha-md-list-item";
import "../../../../components/ha-section-title";
import "../../../../components/ha-state-icon";
import "../../../../components/ha-svg-icon";
import {
getAreaDeviceLookup,
getAreaEntityLookup,
} from "../../../../data/area/area_registry";
import {
getAreasNestedInFloors,
type AreaFloorValue,
@@ -39,6 +35,10 @@ import {
type FloorNestedComboBoxItem,
type UnassignedAreasFloorComboBoxItem,
} from "../../../../data/area_floor_picker";
import {
getAreaDeviceLookup,
getAreaEntityLookup,
} from "../../../../data/area_registry";
import {
getConfigEntries,
type ConfigEntry,
@@ -1504,7 +1504,14 @@ export default class HaAutomationAddFromTarget extends LitElement {
box-shadow: inset var(--ha-shadow-offset-x-lg)
calc(var(--ha-shadow-offset-y-lg) * -1) var(--ha-shadow-blur-lg)
var(--ha-shadow-spread-lg) var(--ha-color-shadow-light);
z-index: 2;
}
@media (prefers-color-scheme: dark) {
.targets-show-more {
box-shadow: inset var(--ha-shadow-offset-x-lg)
calc(var(--ha-shadow-offset-y-lg) * -1) var(--ha-shadow-blur-lg)
var(--ha-shadow-spread-lg) var(--ha-color-shadow-dark);
}
}
@media all and (max-width: 870px), all and (max-height: 500px) {

View File

@@ -1,5 +1,5 @@
import { mdiInformationOutline, mdiPlus } from "@mdi/js";
import { LitElement, css, html, nothing, type TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import {
customElement,
eventOptions,
@@ -39,8 +39,6 @@ export class HaAutomationAddItems extends LitElement {
@property({ attribute: "empty-label" }) public emptyLabel!: string;
@property({ attribute: false }) public emptyNote?: string | TemplateResult;
@property({ attribute: false }) public target?: Target;
@property({ attribute: false }) public getLabel!: (
@@ -81,9 +79,6 @@ export class HaAutomationAddItems extends LitElement {
? html`${this.emptyLabel}
${this.target
? html`<div>${this._renderTarget(this.target)}</div>`
: nothing}
${this.emptyNote
? html`<div class="empty-note">${this.emptyNote}</div>`
: nothing}`
: repeat(
this.items,
@@ -204,7 +199,6 @@ export class HaAutomationAddItems extends LitElement {
static styles = css`
:host {
display: flex;
flex-grow: 1;
}
:host([scrollable]) .items {
overflow: auto;
@@ -219,24 +213,13 @@ export class HaAutomationAddItems extends LitElement {
background-color: var(--ha-color-surface-default);
align-items: center;
color: var(--ha-color-text-secondary);
padding: var(--ha-space-4);
padding: 0;
margin: 0 var(--ha-space-4)
max(var(--safe-area-inset-bottom), var(--ha-space-3));
line-height: var(--ha-line-height-expanded);
justify-content: center;
}
.empty-note {
color: var(--ha-color-text-secondary);
margin-top: var(--ha-space-2);
text-align: center;
}
.empty-note a {
color: currentColor;
text-decoration: underline;
}
.items.error {
background-color: var(--ha-color-fill-danger-quiet-resting);
color: var(--ha-color-on-danger-normal);
@@ -302,8 +285,6 @@ export class HaAutomationAddItems extends LitElement {
border-radius: var(--ha-border-radius-md);
background: var(--ha-color-fill-neutral-normal-resting);
padding: 0 var(--ha-space-2) 0 var(--ha-space-1);
border: var(--ha-border-width-sm) solid
var(--ha-color-border-neutral-quiet);
color: var(--ha-color-on-neutral-normal);
overflow: hidden;
}

View File

@@ -1,4 +1,5 @@
import { mdiClose, mdiPlus } from "@mdi/js";
import { dump } from "js-yaml";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -18,8 +19,13 @@ import "../../../../components/ha-textarea";
import "../../../../components/ha-textfield";
import "../../category/ha-category-picker";
import { computeStateDomain } from "../../../../common/entity/compute_state_domain";
import { supportsMarkdownHelper } from "../../../../common/translations/markdown_support";
import { subscribeOne } from "../../../../common/util/subscribe-one";
import type { GenDataTaskResult } from "../../../../data/ai_task";
import { fetchCategoryRegistry } from "../../../../data/category_registry";
import { subscribeEntityRegistry } from "../../../../data/entity/entity_registry";
import { subscribeLabelRegistry } from "../../../../data/label/label_registry";
import type { HassDialog } from "../../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
@@ -27,11 +33,6 @@ import type {
EntityRegistryUpdate,
SaveDialogParams,
} from "./show-dialog-automation-save";
import {
generateMetadataSuggestionTask,
processMetadataSuggestion,
type MetadataSuggestionResult,
} from "../../common/suggest-metadata-ai";
@customElement("ha-dialog-automation-save")
class DialogAutomationSave extends LitElement implements HassDialog {
@@ -156,7 +157,7 @@ class DialogAutomationSave extends LitElement implements HassDialog {
`
: nothing}
${this._visibleOptionals.includes("description")
? html`<ha-textarea
? html` <ha-textarea
.label=${this.hass.localize(
"ui.panel.config.automation.editor.description.label"
)}
@@ -167,7 +168,6 @@ class DialogAutomationSave extends LitElement implements HassDialog {
autogrow
.value=${this._newDescription}
.helper=${supportsMarkdownHelper(this.hass.localize)}
helperPersistent
@input=${this._valueChanged}
></ha-textarea>`
: nothing}
@@ -334,49 +334,180 @@ class DialogAutomationSave extends LitElement implements HassDialog {
this.closeDialog();
}
private _generateTask = async (): Promise<SuggestWithAIGenerateTask> =>
generateMetadataSuggestionTask(this.hass, {
domain: this._params.domain,
config: this._params.config,
includeDescription: true,
});
private _getSuggestData() {
return Promise.all([
subscribeOne(this.hass.connection, subscribeLabelRegistry).then((labs) =>
Object.fromEntries(labs.map((lab) => [lab.label_id, lab.name]))
),
subscribeOne(this.hass.connection, subscribeEntityRegistry).then((ents) =>
Object.fromEntries(ents.map((ent) => [ent.entity_id, ent]))
),
fetchCategoryRegistry(this.hass.connection, "automation").then((cats) =>
Object.fromEntries(cats.map((cat) => [cat.category_id, cat.name]))
),
]);
}
private _generateTask = async (): Promise<SuggestWithAIGenerateTask> => {
const [labels, entities, categories] = await this._getSuggestData();
const inspirations: string[] = [];
const domain = this._params.domain;
for (const entity of Object.values(this.hass.states)) {
const entityEntry = entities[entity.entity_id];
if (
computeStateDomain(entity) !== domain ||
entity.attributes.restored ||
!entity.attributes.friendly_name ||
!entityEntry
) {
continue;
}
let inspiration = `- ${entity.attributes.friendly_name}`;
const category = categories[entityEntry.categories.automation];
if (category) {
inspiration += ` (category: ${category})`;
}
if (entityEntry.labels.length) {
inspiration += ` (labels: ${entityEntry.labels
.map((label) => labels[label])
.join(", ")})`;
}
inspirations.push(inspiration);
}
const term = this._params.domain === "script" ? "script" : "automation";
return {
type: "data",
task: {
task_name: `frontend__${term}__save`,
instructions: `Suggest in language "${this.hass.language}" a name, description, category and labels for the following Home Assistant ${term}.
The name should be relevant to the ${term}'s purpose.
${
inspirations.length
? `The name should be in same style and sentence capitalization as existing ${term}s.
Suggest a category and labels if relevant to the ${term}'s purpose.
Only suggest category and labels that are already used by existing ${term}s.`
: `The name should be short, descriptive, sentence case, and written in the language ${this.hass.language}.`
}
If the ${term} contains 5+ steps, include a short description.
For inspiration, here are existing ${term}s:
${inspirations.join("\n")}
The ${term} configuration is as follows:
${dump(this._params.config)}
`,
structure: {
name: {
description: "The name of the automation",
required: true,
selector: {
text: {},
},
},
description: {
description: "A short description of the automation",
required: false,
selector: {
text: {},
},
},
labels: {
description: "Labels for the automation",
required: false,
selector: {
text: {
multiple: true,
},
},
},
category: {
description: "The category of the automation",
required: false,
selector: {
select: {
options: Object.entries(categories).map(([id, name]) => ({
value: id,
label: name,
})),
},
},
},
},
},
};
};
private async _handleSuggestion(
event: CustomEvent<GenDataTaskResult<MetadataSuggestionResult>>
event: CustomEvent<
GenDataTaskResult<{
name: string;
description?: string;
category?: string;
labels?: string[];
}>
>
) {
const result = event.detail;
const processed = await processMetadataSuggestion(
this.hass,
this._params.domain,
result
);
const [labels, _entities, categories] = await this._getSuggestData();
this._newName = processed.name;
if (processed.description) {
this._newDescription = processed.description;
this._newName = result.data.name;
if (result.data.description) {
this._newDescription = result.data.description;
if (!this._visibleOptionals.includes("description")) {
this._visibleOptionals = [...this._visibleOptionals, "description"];
}
}
if (processed.categoryId) {
this._entryUpdates = {
...this._entryUpdates,
category: processed.categoryId,
};
if (!this._visibleOptionals.includes("category")) {
this._visibleOptionals = [...this._visibleOptionals, "category"];
if (result.data.category) {
// We get back category name, convert it to ID
const categoryId = Object.entries(categories).find(
([, name]) => name === result.data.category
)?.[0];
if (categoryId) {
this._entryUpdates = {
...this._entryUpdates,
category: categoryId,
};
if (!this._visibleOptionals.includes("category")) {
this._visibleOptionals = [...this._visibleOptionals, "category"];
}
}
}
if (processed.labelIds?.length) {
this._entryUpdates = {
...this._entryUpdates,
labels: processed.labelIds,
};
if (!this._visibleOptionals.includes("labels")) {
this._visibleOptionals = [...this._visibleOptionals, "labels"];
if (result.data.labels?.length) {
// We get back label names, convert them to IDs
const newLabels: Record<string, undefined | string> = Object.fromEntries(
result.data.labels.map((name) => [name, undefined])
);
let toFind = result.data.labels.length;
for (const [labelId, labelName] of Object.entries(labels)) {
if (labelName in newLabels && newLabels[labelName] === undefined) {
newLabels[labelName] = labelId;
toFind--;
if (toFind === 0) {
break;
}
}
}
const foundLabels = Object.values(newLabels).filter(
(labelId) => labelId !== undefined
);
if (foundLabels.length) {
this._entryUpdates = {
...this._entryUpdates,
labels: foundLabels,
};
if (!this._visibleOptionals.includes("labels")) {
this._visibleOptionals = [...this._visibleOptionals, "labels"];
}
}
}
}
@@ -439,7 +570,7 @@ class DialogAutomationSave extends LitElement implements HassDialog {
ha-category-picker,
ha-labels-picker,
ha-area-picker,
ha-chip-set:has(> ha-assist-chip) {
ha-chip-set {
margin-top: 16px;
}
ha-alert {

View File

@@ -76,7 +76,6 @@ import "./types/ha-automation-condition-template";
import "./types/ha-automation-condition-time";
import "./types/ha-automation-condition-trigger";
import "./types/ha-automation-condition-zone";
import type { DeviceCondition } from "../../../../data/device/device_automation";
export interface ConditionElement extends LitElement {
condition: Condition;
@@ -185,14 +184,6 @@ export default class HaAutomationConditionRow extends LitElement {
}
private _renderRow() {
const target =
"target" in (this.conditionDescriptions[this.condition.condition] || {})
? (this.condition as PlatformCondition).target
: "device_id" in this.condition &&
(this.condition as DeviceCondition).device_id
? { device_id: [(this.condition as DeviceCondition).device_id] }
: undefined;
return html`
<ha-condition-icon
slot="leading-icon"
@@ -203,7 +194,10 @@ export default class HaAutomationConditionRow extends LitElement {
${capitalizeFirstLetter(
describeCondition(this.condition, this.hass, this._entityReg)
)}
${target ? this._renderTargets(target) : nothing}
${"target" in
(this.conditionDescriptions[this.condition.condition] || {})
? this._renderTargets((this.condition as PlatformCondition).target)
: nothing}
</h3>
<slot name="icons" slot="icons"></slot>
@@ -839,7 +833,6 @@ export default class HaAutomationConditionRow extends LitElement {
}
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
ev.stopPropagation();
const action = ev.detail?.item?.value;
if (!action) {

View File

@@ -57,7 +57,6 @@ import "../../../components/ha-filter-devices";
import "../../../components/ha-filter-entities";
import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-labels";
import "../../../components/ha-filter-voice-assistants";
import "../../../components/ha-icon-button";
import "../../../components/ha-md-divider";
import "../../../components/ha-md-menu";
@@ -67,7 +66,7 @@ import type { HaMdMenuItem } from "../../../components/ha-md-menu-item";
import "../../../components/ha-sub-menu";
import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
import { createAreaRegistryEntry } from "../../../data/area/area_registry";
import { createAreaRegistryEntry } from "../../../data/area_registry";
import type { AutomationEntity } from "../../../data/automation";
import {
deleteAutomation,
@@ -116,8 +115,6 @@ import { showCategoryRegistryDetailDialog } from "../category/show-dialog-catego
import { configSections } from "../ha-panel-config";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import { showNewAutomationDialog } from "./show-dialog-new-automation";
import { getEntityVoiceAssistantsIds } from "../../../data/expose";
import "../voice-assistants/expose/expose-assistant-icon";
type AutomationItem = AutomationEntity & {
name: string;
@@ -379,31 +376,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
></ha-icon-button>
`,
},
voice_assistants: {
title: localize(
"ui.panel.config.voice_assistants.expose.headers.assistants"
),
type: "flex",
defaultHidden: true,
minWidth: "160px",
maxWidth: "160px",
template: (automation) => {
const exposedToVoiceAssistantIds = getEntityVoiceAssistantsIds(
this._entityReg,
automation.entity_id
);
return html` ${exposedToVoiceAssistantIds.length !== 0
? exposedToVoiceAssistantIds.map(
(vaId) =>
html` <voice-assistants-expose-assistant-icon
.assistant=${vaId}
.hass=${this.hass}
>
</voice-assistants-expose-assistant-icon>`
)
: "—"}`;
},
},
};
return columns;
}
@@ -661,15 +633,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-categories>
<ha-filter-voice-assistants
.hass=${this.hass}
.value=${this._filters["ha-filter-voice-assistants"]?.value}
@data-table-filter-changed=${this._filterChanged}
slot="filter-pane"
.expanded=${this._expandedFilter === "ha-filter-voice-assistants"}
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-voice-assistants>
<ha-filter-blueprints
.hass=${this.hass}
.type=${"automation"}
@@ -1040,7 +1003,8 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
? // @ts-ignore
items.intersection(categoryItems)
: new Set([...items].filter((x) => categoryItems!.has(x)));
} else if (
}
if (
key === "ha-filter-labels" &&
Array.isArray(filter.value) &&
filter.value.length
@@ -1062,29 +1026,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
? // @ts-ignore
items.intersection(labelItems)
: new Set([...items].filter((x) => labelItems!.has(x)));
} else if (
key === "ha-filter-voice-assistants" &&
Array.isArray(filter.value) &&
filter.value.length
) {
const assistItems = new Set<string>();
this.automations
.filter((automation) =>
getEntityVoiceAssistantsIds(
this._entityReg,
automation.entity_id
).some((va) => (filter.value as string[]).includes(va))
)
.forEach((automation) => assistItems.add(automation.entity_id));
if (!items) {
items = assistItems;
continue;
}
items =
"intersection" in items
? // @ts-ignore
items.intersection(assistItems)
: new Set([...items].filter((x) => assistItems!.has(x)));
}
}
this._filteredAutomations = items ? [...items] : undefined;

View File

@@ -350,7 +350,6 @@ export default class HaAutomationOptionRow extends LitElement {
}
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
ev.stopPropagation();
const action = ev.detail?.item?.value;
if (!action) {

View File

@@ -223,8 +223,6 @@ export class HaAutomationRowTargets extends LitElement {
background: var(--ha-color-fill-neutral-normal-resting);
padding: 0 var(--ha-space-2) 0 var(--ha-space-1);
color: var(--ha-color-on-neutral-normal);
border: var(--ha-border-width-sm) solid
var(--ha-color-border-neutral-quiet);
overflow: hidden;
height: 32px;
}

View File

@@ -56,7 +56,6 @@ import { isTrigger, subscribeTrigger } from "../../../../data/automation";
import { describeTrigger } from "../../../../data/automation_i18n";
import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context";
import type { DeviceTrigger } from "../../../../data/device/device_automation";
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
import type { TriggerDescriptions } from "../../../../data/trigger";
import { isTriggerList } from "../../../../data/trigger";
@@ -197,15 +196,6 @@ export default class HaAutomationTriggerRow extends LitElement {
const yamlMode = this._yamlMode || !supported;
const target =
type === "platform" &&
"target" in
this.triggerDescriptions[(this.trigger as PlatformTrigger).trigger]
? (this.trigger as PlatformTrigger).target
: type === "device" && (this.trigger as DeviceTrigger).device_id
? { device_id: (this.trigger as DeviceTrigger).device_id }
: undefined;
return html`
${type === "list"
? html`<ha-svg-icon
@@ -220,7 +210,11 @@ export default class HaAutomationTriggerRow extends LitElement {
></ha-trigger-icon>`}
<h3 slot="header">
${describeTrigger(this.trigger, this.hass, this._entityReg)}
${target ? this._renderTargets(target) : nothing}
${type === "platform" &&
"target" in
this.triggerDescriptions[(this.trigger as PlatformTrigger).trigger]
? this._renderTargets((this.trigger as PlatformTrigger).target)
: nothing}
</h3>
<slot name="icons" slot="icons"></slot>
@@ -815,7 +809,6 @@ export default class HaAutomationTriggerRow extends LitElement {
}
private _handleDropdownSelect(ev: CustomEvent<{ item: HaDropdownItem }>) {
ev.stopPropagation();
const action = ev.detail?.item?.value;
if (!action) {

View File

@@ -152,6 +152,7 @@ class HaBackupConfigAgents extends LitElement {
.src=${brandsUrl({
domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"

View File

@@ -66,6 +66,7 @@ class HaBackupAgentsPicker extends LitElement {
.src=${brandsUrl({
domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"

View File

@@ -226,6 +226,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
.src=${brandsUrl({
domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
height="24"

View File

@@ -207,6 +207,7 @@ class HaConfigBackupDetails extends LitElement {
.src=${brandsUrl({
domain,
type: "icon",
useFallback: true,
darkOptimized:
this.hass.themes?.darkMode,
})}

View File

@@ -252,6 +252,7 @@ class HaConfigBackupSettings extends LitElement {
.src=${brandsUrl({
domain: "cloud",
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
crossorigin="anonymous"

View File

@@ -206,8 +206,8 @@ class HaBlueprintOverview extends LitElement {
sortable: true,
valueColumn: "usageCount",
type: "numeric",
minWidth: "90px",
maxWidth: "90px",
minWidth: "100px",
maxWidth: "120px",
template: (blueprint) => {
const count = blueprint.usageCount ?? 0;
return html`

View File

@@ -4,8 +4,10 @@ import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert";
import "../../../components/ha-button";
import "../../../components/ha-wa-dialog";
import "../../../components/ha-dialog-footer";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-icon-picker";
import "../../../components/ha-settings-row";
import "../../../components/ha-textfield";
import { updateEntityRegistryEntry } from "../../../data/entity/entity_registry";
import { haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
@@ -26,21 +28,14 @@ class DialogAssignCategory extends LitElement {
@state() private _submitting?: boolean;
@state() private _open = false;
public showDialog(params: AssignCategoryDialogParams): void {
this._params = params;
this._scope = params.scope;
this._category = params.entityReg.categories[params.scope];
this._error = undefined;
this._open = true;
}
public closeDialog(): void {
this._open = false;
}
private _dialogClosed(): void {
this._error = "";
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
@@ -52,46 +47,47 @@ class DialogAssignCategory extends LitElement {
}
const entry = this._params.entityReg.categories[this._params.scope];
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${entry
? this.hass.localize("ui.panel.config.category.assign.edit")
: this.hass.localize("ui.panel.config.category.assign.assign")}
@closed=${this._dialogClosed}
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
entry
? this.hass.localize("ui.panel.config.category.assign.edit")
: this.hass.localize("ui.panel.config.category.assign.assign")
)}
>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<div class="form">
<ha-category-picker
.hass=${this.hass}
.scope=${this._scope}
.label=${this.hass.localize(
"ui.components.category-picker.category"
)}
.value=${this._category}
@value-changed=${this._categoryChanged}
autofocus
></ha-category-picker>
<div>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<div class="form">
<ha-category-picker
.hass=${this.hass}
.scope=${this._scope}
.label=${this.hass.localize(
"ui.components.category-picker.category"
)}
.value=${this._category}
@value-changed=${this._categoryChanged}
></ha-category-picker>
</div>
</div>
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${!!this._submitting}
>
${this.hass.localize("ui.common.save")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
<ha-button
appearance="plain"
slot="primaryAction"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${!!this._submitting}
>
${this.hass.localize("ui.common.save")}
</ha-button>
</ha-dialog>
`;
}

View File

@@ -3,9 +3,9 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-alert";
import "../../../components/ha-wa-dialog";
import "../../../components/ha-dialog-footer";
import { createCloseHeading } from "../../../components/ha-dialog";
import "../../../components/ha-icon-picker";
import "../../../components/ha-settings-row";
import "../../../components/ha-button";
import "../../../components/ha-textfield";
import type {
@@ -30,14 +30,11 @@ class DialogCategoryDetail extends LitElement {
@state() private _submitting?: boolean;
@state() private _open = false;
public async showDialog(
params: CategoryRegistryDetailDialogParams
): Promise<void> {
this._params = params;
this._error = undefined;
this._open = true;
if (this._params.entry) {
this._name = this._params.entry.name || "";
this._icon = this._params.entry.icon || null;
@@ -49,10 +46,6 @@ class DialogCategoryDetail extends LitElement {
}
public closeDialog(): void {
this._open = false;
}
private _dialogClosed(): void {
this._error = "";
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
@@ -65,55 +58,61 @@ class DialogCategoryDetail extends LitElement {
const entry = this._params.entry;
const nameInvalid = !this._isNameValid();
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${entry
? this.hass.localize("ui.panel.config.category.editor.edit")
: this.hass.localize("ui.panel.config.category.editor.create")}
@closed=${this._dialogClosed}
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
entry
? this.hass.localize("ui.panel.config.category.editor.edit")
: this.hass.localize("ui.panel.config.category.editor.create")
)}
>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<div class="form">
<ha-textfield
.value=${this._name}
@input=${this._nameChanged}
.label=${this.hass.localize("ui.panel.config.category.editor.name")}
.validationMessage=${this.hass.localize(
"ui.panel.config.category.editor.required_error_msg"
)}
required
autofocus
></ha-textfield>
<div>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<div class="form">
<ha-textfield
.value=${this._name}
@input=${this._nameChanged}
.label=${this.hass.localize(
"ui.panel.config.category.editor.name"
)}
.validationMessage=${this.hass.localize(
"ui.panel.config.category.editor.required_error_msg"
)}
required
dialogInitialFocus
></ha-textfield>
<ha-icon-picker
.hass=${this.hass}
.value=${this._icon}
@value-changed=${this._iconChanged}
.label=${this.hass.localize("ui.panel.config.category.editor.icon")}
></ha-icon-picker>
<ha-icon-picker
.hass=${this.hass}
.value=${this._icon}
@value-changed=${this._iconChanged}
.label=${this.hass.localize(
"ui.panel.config.category.editor.icon"
)}
></ha-icon-picker>
</div>
</div>
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${nameInvalid || !!this._submitting}
>
${entry
? this.hass.localize("ui.common.save")
: this.hass.localize("ui.common.add")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
<ha-button
appearance="plain"
slot="secondaryAction"
@click=${this.closeDialog}
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${nameInvalid || !!this._submitting}
>
${entry
? this.hass.localize("ui.common.save")
: this.hass.localize("ui.common.add")}
</ha-button>
</ha-dialog>
`;
}

View File

@@ -20,6 +20,7 @@ import type { HomeAssistant, ValueChangedEvent } from "../../../types";
import { showCategoryRegistryDetailDialog } from "./show-dialog-category-registry-detail";
const ADD_NEW_ID = "___ADD_NEW___";
const NO_CATEGORIES_ID = "___NO_CATEGORIES___";
@customElement("ha-category-picker")
export class HaCategoryPicker extends SubscribeMixin(LitElement) {
@@ -100,11 +101,17 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
);
private _getCategories = memoizeOne(
(
categories: CategoryRegistryEntry[] | undefined
): PickerComboBoxItem[] | undefined => {
if (!categories) {
return undefined;
(categories: CategoryRegistryEntry[] | undefined): PickerComboBoxItem[] => {
if (!categories || categories.length === 0) {
return [
{
id: NO_CATEGORIES_ID,
primary: this.hass.localize(
"ui.components.category-picker.no_categories"
),
icon_path: mdiTag,
},
];
}
const items = categories.map<PickerComboBoxItem>((category) => ({
@@ -203,6 +210,10 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
const value = ev.detail.value;
if (value === NO_CATEGORIES_ID) {
return;
}
if (!value) {
this._setValue(undefined);
return;

View File

@@ -1,263 +0,0 @@
import { dump } from "js-yaml";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { subscribeOne } from "../../../common/util/subscribe-one";
import type { AITaskStructure, GenDataTaskResult } from "../../../data/ai_task";
import { fetchCategoryRegistry } from "../../../data/category_registry";
import {
subscribeEntityRegistry,
type EntityRegistryEntry,
} from "../../../data/entity/entity_registry";
import { subscribeLabelRegistry } from "../../../data/label/label_registry";
import type { HomeAssistant } from "../../../types";
import type { SuggestWithAIGenerateTask } from "../../../components/ha-suggest-with-ai-button";
export interface MetadataSuggestionResult {
name: string;
description?: string;
category?: string;
labels?: string[];
}
export interface ProcessedMetadataSuggestionResult {
name: string;
description?: string;
categoryId?: string;
labelIds?: string[];
}
export interface MetadataSuggestionConfig {
/** The domain to suggest metadata for (automation, script) */
domain: "automation" | "script";
/** The configuration to suggest metadata for */
config: any;
/** Whether to include description field in the suggestion */
includeDescription?: boolean;
/** Whether to include icon field in the suggestion (scripts only) */
includeIcon?: boolean;
}
type Categories = Record<string, string>;
type Entities = Record<string, EntityRegistryEntry>;
type Labels = Record<string, string>;
const fetchCategories = (
connection: HomeAssistant["connection"],
domain: MetadataSuggestionConfig["domain"]
): Promise<Categories> =>
fetchCategoryRegistry(connection, domain).then((cats) =>
Object.fromEntries(cats.map((cat) => [cat.category_id, cat.name]))
);
const fetchEntities = (
connection: HomeAssistant["connection"]
): Promise<Entities> =>
subscribeOne(connection, subscribeEntityRegistry).then((ents) =>
Object.fromEntries(ents.map((ent) => [ent.entity_id, ent]))
);
const fetchLabels = (
connection: HomeAssistant["connection"]
): Promise<Labels> =>
subscribeOne(connection, subscribeLabelRegistry).then((labs) =>
Object.fromEntries(labs.map((lab) => [lab.label_id, lab.name]))
);
function buildMetadataInspirations(
states: HomeAssistant["states"],
entities: Record<string, EntityRegistryEntry>,
categories: Categories,
labels: Labels,
domain: MetadataSuggestionConfig["domain"]
): string[] {
const inspirations: string[] = [];
for (const entity of Object.values(states)) {
const entityEntry = entities[entity.entity_id];
if (
!entityEntry ||
computeStateDomain(entity) !== domain ||
entity.attributes.restored ||
!entity.attributes.friendly_name
) {
continue;
}
let inspiration = `- ${entity.attributes.friendly_name}`;
// Get the category for this domain
const category = categories[entityEntry.categories[domain]];
if (category) {
inspiration += ` (category: ${category})`;
}
if (entityEntry.labels.length) {
inspiration += ` (labels: ${entityEntry.labels
.map((label) => labels[label])
.join(", ")})`;
}
inspirations.push(inspiration);
}
return inspirations;
}
export async function generateMetadataSuggestionTask(
connection: HomeAssistant["connection"],
states: HomeAssistant["states"],
language: HomeAssistant["language"],
suggestionConfig: MetadataSuggestionConfig
): Promise<SuggestWithAIGenerateTask> {
const { domain, config, includeDescription } = suggestionConfig;
let categories: Categories = {};
let entities: Entities = {};
let labels: Labels = {};
try {
[categories, entities, labels] = await Promise.all([
fetchCategories(connection, domain),
fetchEntities(connection),
fetchLabels(connection),
]);
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error getting suggest metadata:", error);
}
const inspirations = buildMetadataInspirations(
states,
entities,
categories,
labels,
domain
);
const structure: AITaskStructure = {
name: {
description: `The name of the ${domain}`,
required: true,
selector: {
text: {},
},
},
...(includeDescription && {
description: {
description: `A short description of the ${domain}`,
required: false,
selector: {
text: {},
},
},
}),
labels: {
description: `Labels for the ${domain}`,
required: false,
selector: {
text: {
multiple: true,
},
},
},
category: {
description: `The category of the ${domain}`,
required: false,
selector: {
select: {
options: Object.entries(categories).map(([id, name]) => ({
value: id,
label: name,
})),
},
},
},
};
return {
type: "data",
task: {
task_name: `frontend__${domain}__save`,
instructions: `Suggest in language "${language}" a name${includeDescription ? ", description" : ""}, category and labels for the following Home Assistant ${domain}.
The name should be relevant to the ${domain}'s purpose.
${
inspirations.length
? `The name should be in same style and sentence capitalization as existing ${domain}s.
Suggest a category and labels if relevant to the ${domain}'s purpose.
Only suggest category and labels that are already used by existing ${domain}s.`
: `The name should be short, descriptive, sentence case, and written in the language ${language}.`
}${
includeDescription
? `
If the ${domain} contains 5+ steps, include a short description.`
: ""
}
For inspiration, here are existing ${domain}s:
${inspirations.join("\n")}
The ${domain} configuration is as follows:
${dump(config)}
`,
structure,
},
};
}
export async function processMetadataSuggestion(
connection: HomeAssistant["connection"],
domain: MetadataSuggestionConfig["domain"],
result: GenDataTaskResult<MetadataSuggestionResult>
): Promise<ProcessedMetadataSuggestionResult> {
let categories: Categories = {};
let labels: Labels = {};
try {
[categories, labels] = await Promise.all([
fetchCategories(connection, domain),
fetchLabels(connection),
]);
} catch (error) {
// eslint-disable-next-line no-console
console.error("Error getting suggest metadata:", error);
}
const processed: ProcessedMetadataSuggestionResult = {
name: result.data.name,
description: result.data.description ?? undefined,
};
// Convert category name to ID
if (result.data.category) {
const categoryId = Object.entries(categories).find(
([, name]) => name === result.data.category
)?.[0];
if (categoryId) {
processed.categoryId = categoryId;
}
}
// Convert label names to IDs
if (result.data.labels?.length) {
const newLabels: Record<string, undefined | string> = Object.fromEntries(
result.data.labels.map((name) => [name, undefined])
);
let toFind = result.data.labels.length;
for (const [labelId, labelName] of Object.entries(labels)) {
if (labelName in newLabels && newLabels[labelName] === undefined) {
newLabels[labelName] = labelId;
toFind--;
if (toFind === 0) {
break;
}
}
}
const foundLabels = Object.values(newLabels).filter(
(labelId) => labelId !== undefined
);
if (foundLabels.length) {
processed.labelIds = foundLabels;
}
}
return processed;
}

View File

@@ -1,16 +1,19 @@
import {
mdiDotsVertical,
mdiLocationEnter,
mdiLocationExit,
mdiRefresh,
} from "@mdi/js";
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical, mdiRefresh } from "@mdi/js";
import type { HassEntities } from "home-assistant-js-websocket";
import type { TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
import "../../../components/ha-alert";
import "../../../components/ha-bar";
import "../../../components/ha-button-menu";
import "../../../components/ha-card";
import "../../../components/ha-check-list-item";
import "../../../components/ha-list-item";
import "../../../components/ha-metric";
import { extractApiErrorMessage } from "../../../data/hassio/common";
import type {
HassioSupervisorInfo,
@@ -23,16 +26,13 @@ import {
} from "../../../data/hassio/supervisor";
import {
checkForEntityUpdates,
filterUpdateEntitiesParameterized,
filterUpdateEntitiesWithInstall,
} from "../../../data/update";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-subpage";
import type { HomeAssistant } from "../../../types";
import "../dashboard/ha-config-updates";
import { showJoinBetaDialog } from "./updates/show-dialog-join-beta";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "@home-assistant/webawesome/dist/components/divider/divider";
@customElement("ha-config-section-updates")
class HaConfigSectionUpdates extends LitElement {
@@ -53,11 +53,7 @@ class HaConfigSectionUpdates extends LitElement {
}
protected render(): TemplateResult {
const canInstallUpdates = this._filterInstallableUpdateEntities(
this.hass.states,
this._showSkipped
);
const notInstallableUpdates = this._filterNotInstallableUpdateEntities(
const canInstallUpdates = this._filterUpdateEntitiesWithInstall(
this.hass.states,
this._showSkipped
);
@@ -77,86 +73,57 @@ class HaConfigSectionUpdates extends LitElement {
.path=${mdiRefresh}
@click=${this._checkUpdates}
></ha-icon-button>
<ha-dropdown @wa-select=${this._handleOverflowAction}>
<ha-button-menu multi>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-dropdown-item
type="checkbox"
.checked=${this._showSkipped}
value="show_skipped"
<ha-check-list-item
left
@request-selected=${this._toggleSkipped}
.selected=${this._showSkipped}
>
${this.hass.localize("ui.panel.config.updates.show_skipped")}
</ha-dropdown-item>
</ha-check-list-item>
${this._supervisorInfo
? html`
<wa-divider></wa-divider>
<ha-dropdown-item
value="toggle_beta"
<li divider role="separator"></li>
<ha-list-item
@request-selected=${this._toggleBeta}
.disabled=${this._supervisorInfo.channel === "dev"}
>
<ha-svg-icon
.path=${this._supervisorInfo.channel === "stable"
? mdiLocationEnter
: mdiLocationExit}
slot="icon"
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.updates.${this._supervisorInfo.channel === "stable" ? "join" : "leave"}_beta`
)}
${this._supervisorInfo.channel === "stable"
? this.hass.localize("ui.panel.config.updates.join_beta")
: this.hass.localize(
"ui.panel.config.updates.leave_beta"
)}
</ha-dropdown-item>
</ha-list-item>
`
: nothing}
</ha-dropdown>
: ""}
</ha-button-menu>
</div>
<div class="content">
${canInstallUpdates.length
? html`
<ha-card outlined>
<div class="card-content">
<ha-card outlined>
<div class="card-content">
${canInstallUpdates.length
? html`
<ha-config-updates
.hass=${this.hass}
.narrow=${this.narrow}
.updateEntities=${canInstallUpdates}
.isInstallable=${true}
showAll
></ha-config-updates>
</div>
</ha-card>
`
: nothing}
${notInstallableUpdates.length
? html`
<ha-card outlined>
<div class="card-content">
<ha-config-updates
.hass=${this.hass}
.narrow=${this.narrow}
.updateEntities=${notInstallableUpdates}
.isInstallable=${false}
showAll
></ha-config-updates>
</div>
</ha-card>
`
: nothing}
${canInstallUpdates.length + notInstallableUpdates.length
? nothing
: html`
<ha-card outlined>
<div class="no-updates">
${this.hass.localize("ui.panel.config.updates.no_updates")}
</div>
</ha-card>
`}
`
: html`
<div class="no-updates">
${this.hass.localize(
"ui.panel.config.updates.no_updates"
)}
</div>
`}
</div>
</ha-card>
</div>
</hass-subpage>
`;
@@ -166,19 +133,27 @@ class HaConfigSectionUpdates extends LitElement {
this._supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
}
private _handleOverflowAction(
ev: CustomEvent<{ item: { value: string } }>
): void {
if (ev.detail.item.value === "toggle_beta") {
if (this._supervisorInfo!.channel === "stable") {
showJoinBetaDialog(this, {
join: () => this._setChannel("beta"),
});
} else {
this._setChannel("stable");
}
} else if (ev.detail.item.value === "show_skipped") {
this._showSkipped = !this._showSkipped;
private _toggleSkipped(ev: CustomEvent<RequestSelectedDetail>): void {
if (ev.detail.source !== "property") {
return;
}
this._showSkipped = !this._showSkipped;
}
private async _toggleBeta(
ev: CustomEvent<RequestSelectedDetail>
): Promise<void> {
if (!shouldHandleRequestSelectedEvent(ev)) {
return;
}
if (this._supervisorInfo!.channel === "stable") {
showJoinBetaDialog(this, {
join: async () => this._setChannel("beta"),
});
} else {
this._setChannel("stable");
}
}
@@ -202,14 +177,9 @@ class HaConfigSectionUpdates extends LitElement {
checkForEntityUpdates(this, this.hass);
}
private _filterInstallableUpdateEntities = memoizeOne(
private _filterUpdateEntitiesWithInstall = memoizeOne(
(entities: HassEntities, showSkipped: boolean) =>
filterUpdateEntitiesParameterized(entities, showSkipped, false)
);
private _filterNotInstallableUpdateEntities = memoizeOne(
(entities: HassEntities, showSkipped: boolean) =>
filterUpdateEntitiesParameterized(entities, showSkipped, true)
filterUpdateEntitiesWithInstall(entities, showSkipped)
);
static styles = css`

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