Compare commits

..

42 Commits

Author SHA1 Message Date
Bram Kragten
78d41dfd55 Merge branch 'rc' 2026-01-09 23:28:00 +01:00
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
ac88f3ed0b Merge branch 'rc' 2026-01-07 16:32:25 +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
Franck Nijhof
3a3036c635 20251229.0 (#28727) 2025-12-29 14:34:15 +01:00
95 changed files with 980 additions and 2811 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

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

@@ -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

@@ -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",
@@ -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,8 +215,8 @@
"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",
"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",
@@ -224,12 +224,12 @@
},
"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"

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

@@ -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,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

@@ -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

@@ -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

@@ -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

@@ -793,7 +793,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
.section-title,
.title {
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

@@ -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

@@ -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,5 +1,4 @@
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 } from "lit";
import {
@@ -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.
@@ -114,8 +114,6 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
@state()
private _bodyScrolled = false;
private _escapePressed = false;
protected get scrollableElement(): HTMLElement | null {
return this.bodyContainer;
}
@@ -141,8 +139,6 @@ 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}
@@ -212,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 {
@@ -229,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,
@@ -294,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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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;

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

@@ -1,3 +1,4 @@
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";
@@ -153,10 +154,6 @@ const _SHORTCUTS: Section[] = [
shortcut: ["M"],
descriptionTranslationKey: "ui.dialogs.shortcuts.other.my_link",
},
{
shortcut: ["Shift", "/"],
descriptionTranslationKey: "ui.dialogs.shortcuts.other.show_shortcuts",
},
],
},
];
@@ -187,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

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

@@ -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>

View File

@@ -2062,7 +2062,6 @@ class DialogAddAutomationElement
.content.column {
flex-direction: column;
gap: var(--ha-space-3);
}
ha-md-list {

View File

@@ -285,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

@@ -157,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"
)}
@@ -168,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}
@@ -571,7 +570,7 @@ ${dump(this._params.config)}
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 any).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>

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";
@@ -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.automation.picker.headers.voice_assistants"
),
type: "icon",
defaultHidden: true,
minWidth: "100px",
maxWidth: "100px",
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

@@ -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>

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

@@ -101,13 +101,7 @@ 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 [
{

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`

View File

@@ -31,7 +31,7 @@ import {
import type { UpdateEntity } from "../../../data/update";
import {
checkForEntityUpdates,
filterUpdateEntitiesParameterized,
filterUpdateEntitiesWithInstall,
} from "../../../data/update";
import {
QuickBarMode,
@@ -161,27 +161,24 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
total: 0,
};
private _pages = memoizeOne(
(cloudStatus, isCloudLoaded, hasExternalSettings) => [
isCloudLoaded
? [
{
component: "cloud",
path: "/config/cloud",
name: "Home Assistant Cloud",
info: cloudStatus,
iconPath: mdiCloudLock,
iconColor: "#3B808E",
translationKey: "cloud",
},
...configSections.dashboard,
]
: configSections.dashboard,
hasExternalSettings ? configSections.dashboard_external_settings : [],
configSections.dashboard_2,
configSections.dashboard_3,
]
);
private _pages = memoizeOne((cloudStatus, isCloudLoaded) => [
isCloudLoaded
? [
{
component: "cloud",
path: "/config/cloud",
name: "Home Assistant Cloud",
info: cloudStatus,
iconPath: mdiCloudLock,
iconColor: "#3B808E",
translationKey: "cloud",
},
...configSections.dashboard,
]
: configSections.dashboard,
configSections.dashboard_2,
configSections.dashboard_3,
]);
public hassSubscribe(): UnsubscribeFunc[] {
return [
@@ -206,7 +203,7 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
protected render(): TemplateResult {
const { updates: canInstallUpdates, total: totalUpdates } =
this._filterUpdateEntitiesParameterized(
this._filterUpdateEntitiesWithInstall(
this.hass.states,
this.hass.entities
);
@@ -291,7 +288,6 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
.narrow=${this.narrow}
.total=${totalUpdates}
.updateEntities=${canInstallUpdates}
.isInstallable=${true}
></ha-config-updates>
${totalUpdates > canInstallUpdates.length
? html`
@@ -314,8 +310,7 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
: ""}
${this._pages(
this.cloudStatus,
isComponentLoaded(this.hass, "cloud"),
this.hass.auth.external?.config.hasSettingsScreen
isComponentLoaded(this.hass, "cloud")
).map((categoryPages) =>
categoryPages.length === 0
? nothing
@@ -349,16 +344,14 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
showShortcutsDialog(this);
}
private _filterUpdateEntitiesParameterized = memoizeOne(
private _filterUpdateEntitiesWithInstall = memoizeOne(
(
entities: HomeAssistant["states"],
entityRegistry: HomeAssistant["entities"]
): { updates: UpdateEntity[]; total: number } => {
const updates = filterUpdateEntitiesParameterized(
entities,
false,
false
).filter((entity) => !entityRegistry[entity.entity_id]?.hidden);
const updates = filterUpdateEntitiesWithInstall(entities).filter(
(entity) => !entityRegistry[entity.entity_id]?.hidden
);
return {
updates: updates.slice(0, updates.length === 3 ? updates.length : 2),

View File

@@ -32,8 +32,6 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
@property({ type: Number }) public total?: number;
@property({ attribute: false }) public isInstallable = true;
@state() private _devices?: DeviceRegistryEntry[];
@state() private _entities?: EntityRegistryEntry[];
@@ -91,16 +89,9 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
return html`
<div class="title" role="heading" aria-level="2">
${this.isInstallable
? this.hass.localize("ui.panel.config.updates.title", {
count: this.total || this.updateEntities.length,
})
: this.hass.localize(
"ui.panel.config.updates.title_not_installable",
{
count: this.total || this.updateEntities.length,
}
)}
${this.hass.localize("ui.panel.config.updates.title", {
count: this.total || this.updateEntities.length,
})}
</div>
<ha-md-list>
${updates.map((entity) => {

View File

@@ -5,9 +5,8 @@ import { fireEvent } from "../../../../common/dom/fire_event";
import { computeDeviceNameDisplay } from "../../../../common/entity/compute_device_name";
import "../../../../components/ha-alert";
import "../../../../components/ha-area-picker";
import "../../../../components/ha-wa-dialog";
import "../../../../components/ha-dialog-footer";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog";
import "../../../../components/ha-labels-picker";
import type { HaSwitch } from "../../../../components/ha-switch";
import "../../../../components/ha-textfield";
@@ -20,8 +19,6 @@ import type { DeviceRegistryDetailDialogParams } from "./show-dialog-device-regi
class DialogDeviceRegistryDetail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _open = false;
@state() private _nameByUser!: string;
@state() private _error?: string;
@@ -45,15 +42,10 @@ class DialogDeviceRegistryDetail extends LitElement {
this._areaId = this._params.device.area_id || "";
this._labels = this._params.device.labels || [];
this._disabledBy = this._params.device.disabled_by;
this._open = true;
await this.updateComplete;
}
public closeDialog(): void {
this._open = false;
}
private _dialogClosed(): void {
this._error = "";
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
@@ -65,12 +57,10 @@ class DialogDeviceRegistryDetail extends LitElement {
}
const device = this._params.device;
return html`
<ha-wa-dialog
.hass=${this.hass}
.open=${this._open}
header-title=${computeDeviceNameDisplay(device, this.hass)}
prevent-scrim-close
@closed=${this._dialogClosed}
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${computeDeviceNameDisplay(device, this.hass)}
>
<div>
${this._error
@@ -78,7 +68,6 @@ class DialogDeviceRegistryDetail extends LitElement {
: ""}
<div class="form">
<ha-textfield
autofocus
.value=${this._nameByUser}
@input=${this._nameChanged}
.label=${this.hass.localize(
@@ -86,6 +75,7 @@ class DialogDeviceRegistryDetail extends LitElement {
)}
.placeholder=${device.name || ""}
.disabled=${this._submitting}
dialogInitialFocus
></ha-textfield>
<ha-area-picker
.hass=${this.hass}
@@ -141,25 +131,22 @@ class DialogDeviceRegistryDetail extends LitElement {
</div>
</div>
</div>
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
@click=${this.closeDialog}
.disabled=${this._submitting}
appearance="plain"
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${this._submitting}
>
${this.hass.localize("ui.dialogs.device-registry-detail.update")}
</ha-button>
</ha-dialog-footer>
</ha-wa-dialog>
<ha-button
slot="secondaryAction"
@click=${this.closeDialog}
.disabled=${this._submitting}
appearance="plain"
>
${this.hass.localize("ui.common.cancel")}
</ha-button>
<ha-button
slot="primaryAction"
@click=${this._updateEntry}
.disabled=${this._submitting}
>
${this.hass.localize("ui.dialogs.device-registry-detail.update")}
</ha-button>
</ha-dialog>
`;
}

View File

@@ -64,7 +64,6 @@ import "../../../components/ha-filter-floor-areas";
import "../../../components/ha-filter-integrations";
import "../../../components/ha-filter-labels";
import "../../../components/ha-filter-states";
import "../../../components/ha-filter-voice-assistants";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-md-divider";
@@ -116,8 +115,6 @@ import { isHelperDomain } from "../helpers/const";
import "../integrations/ha-integration-overflow-menu";
import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import { getEntityVoiceAssistantsIds } from "../../../data/expose";
import "../voice-assistants/expose/expose-assistant-icon";
export interface StateEntity extends Omit<
EntityRegistryEntry,
@@ -496,31 +493,6 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
template: (entry) =>
entry.label_entries.map((lbl) => lbl.name).join(" "),
},
voice_assistants: {
title: localize(
"ui.panel.config.entities.picker.headers.voice_assistants"
),
type: "icon",
defaultHidden: true,
minWidth: "100px",
maxWidth: "100px",
template: (entry) => {
const exposedToVoiceAssistantIds = getEntityVoiceAssistantsIds(
this._entities,
entry.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>`
)
: "—"}`;
},
},
})
);
@@ -665,16 +637,6 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
filteredEntities = filteredEntities.filter((entity) =>
entity.labels.some((lbl) => (filter as string[]).includes(lbl))
);
} else if (
key === "ha-filter-voice-assistants" &&
Array.isArray(filter) &&
filter.length
) {
filteredEntities = filteredEntities.filter((entity) =>
getEntityVoiceAssistantsIds(this._entities, entity.entity_id).some(
(va) => (filter as string[]).includes(va)
)
);
}
});
@@ -1114,15 +1076,6 @@ ${
.narrow=${this.narrow}
@expanded-changed=${this._filterExpanded}
></ha-filter-labels>
<ha-filter-voice-assistants
.hass=${this.hass}
.value=${this._filters["ha-filter-voice-assistants"]}
@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>
${
includeAddDeviceFab
? html`<ha-fab
@@ -1175,7 +1128,6 @@ ${
const subEntry = this._searchParms.get("sub_entry");
const device = this._searchParms.get("device");
const label = this._searchParms.get("label");
const voiceAssistant = this._searchParms.get("voice_assistant");
if (!domain && !configEntry && !label && !device) {
return;
@@ -1188,7 +1140,6 @@ ${
"ha-filter-integrations": domain ? [domain] : [],
"ha-filter-devices": device ? [device] : [],
"ha-filter-labels": label ? [label] : [],
"ha-filter-voice-assistants": voiceAssistant ? [voiceAssistant] : [],
config_entry: configEntry ? [configEntry] : [],
sub_entry: subEntry ? [subEntry] : [],
};

View File

@@ -105,24 +105,7 @@ export const configSections: Record<string, PageNavigation[]> = {
iconColor: "#3263C3",
},
],
dashboard_external_settings: [
{
path: "#external-app-configuration",
translationKey: "companion",
iconPath: mdiCellphoneCog,
iconColor: "#8E24AA",
},
],
dashboard_2: [
{
path: "/config/matter",
name: "Matter",
iconPath:
"M7.228375 6.41685c0.98855 0.80195 2.16365 1.3412 3.416275 1.56765V1.30093l1.3612 -0.7854275 1.360125 0.7854275V7.9845c1.252875 -0.226675 2.4283 -0.765875 3.41735 -1.56765l2.471225 1.4293c-4.019075 3.976275 -10.490025 3.976275 -14.5091 0l2.482925 -1.4293Zm3.00335 17.067575c1.43325 -5.47035 -1.8052 -11.074775 -7.2604 -12.564675v2.859675c1.189125 0.455 2.244125 1.202875 3.0672 2.174275L0.25 19.2955v1.5719l1.3611925 0.781175L7.39865 18.3068c0.430175 1.19825 0.550625 2.48575 0.35015 3.743l2.482925 1.434625ZM21.034 10.91975c-5.452225 1.4932 -8.6871 7.09635 -7.254025 12.564675l2.47655 -1.43035c-0.200025 -1.257275 -0.079575 -2.544675 0.35015 -3.743025l5.7832 3.337525L23.75 20.86315V19.2955L17.961475 15.9537c0.8233 -0.97115 1.878225 -1.718975 3.0672 -2.174275l0.005325 -2.859675Z",
iconColor: "#2458B3",
component: "matter",
translationKey: "matter",
},
{
path: "/config/zha",
name: "Zigbee",
@@ -190,6 +173,12 @@ export const configSections: Record<string, PageNavigation[]> = {
iconColor: "#5A87FA",
component: ["person", "users"],
},
{
path: "#external-app-configuration",
translationKey: "companion",
iconPath: mdiCellphoneCog,
iconColor: "#8E24AA",
},
{
path: "/config/system",
translationKey: "system",

View File

@@ -260,6 +260,8 @@ export class DialogHelperDetail extends LitElement {
open
@closed=${this.closeDialog}
class=${classMap({ "button-left": !this._domain })}
scrimClickAction
escapeKeyAction
.hideActions=${!this._domain}
.heading=${createCloseHeading(
this.hass,

View File

@@ -105,7 +105,7 @@ class HaTimerForm extends LitElement {
<ha-checkbox
.configValue=${"restore"}
.checked=${this._restore}
@change=${this._toggleRestore}
@click=${this._toggleRestore}
.disabled=${this.disabled}
>
</ha-checkbox>
@@ -135,8 +135,11 @@ class HaTimerForm extends LitElement {
});
}
private _toggleRestore(ev) {
this._restore = ev.target.checked;
private _toggleRestore() {
if (this.disabled) {
return;
}
this._restore = !this._restore;
fireEvent(this, "value-changed", {
value: { ...this._item, restore: this._restore },
});

View File

@@ -51,7 +51,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";
import "../../../components/ha-icon-overflow-menu";
import "../../../components/ha-md-divider";
@@ -123,8 +122,6 @@ import "../integrations/ha-integration-overflow-menu";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import { isHelperDomain, type HelperDomain } from "./const";
import { showHelperDetailDialog } from "./show-dialog-helper-detail";
import { getEntityVoiceAssistantsIds } from "../../../data/expose";
import "../voice-assistants/expose/expose-assistant-icon";
interface HelperItem {
id: string;
@@ -208,7 +205,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
})
private _activeHiddenColumns?: string[];
@state() private _helperEntities: HassEntity[] = [];
@state() private _stateItems: HassEntity[] = [];
@state() private _disabledEntityEntries?: EntityRegistryEntry[];
@@ -226,8 +223,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
@state() private _diagnosticHandlers?: Record<string, boolean>;
@state() private _searchParms = new URLSearchParams(window.location.search);
@storage({
storage: "sessionStorage",
key: "helpers-table-filters",
@@ -250,7 +245,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];
@state() private _filteredHelperEntityIds?: string[] | null;
@state() private _filteredStateItems?: string[] | null;
private _sizeController = new ResizeController(this, {
callback: (entries) => entries[0]?.contentRect.width,
@@ -485,32 +480,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
</ha-icon-overflow-menu>
`,
},
voice_assistants: {
title: localize(
"ui.panel.config.helpers.picker.headers.voice_assistants"
),
type: "icon",
defaultHidden: true,
minWidth: "100px",
maxWidth: "100px",
template: (helper) => {
const exposedToVoiceAssistantIds = getEntityVoiceAssistantsIds(
this._entityReg,
helper.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>
`
)
: "—"}`;
},
},
})
);
@@ -641,7 +610,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
protected render(): TemplateResult {
if (
!this.hass ||
this._helperEntities === undefined ||
this._stateItems === undefined ||
this._entityEntries === undefined ||
this._configEntries === undefined
) {
@@ -716,14 +685,14 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
(!this._sizeController.value && this.hass.dockedSidebar === "docked");
const helpers = this._getItems(
this.hass.localize,
this._helperEntities,
this._stateItems,
this._disabledEntityEntries || [],
this._entityEntries,
this._configEntries,
this._entityReg,
this._categories,
this._labels,
this._filteredHelperEntityIds
this._filteredStateItems
);
return html`
<hass-tabs-subpage-data-table
@@ -810,15 +779,6 @@ export class HaConfigHelpers 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"]}
@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>
${!this.narrow
? html`<ha-md-button-menu slot="selection-bar">
@@ -981,7 +941,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
filter.length
) {
const labelItems = new Set<string>();
this._helperEntities
this._stateItems
.filter((stateItem) =>
entityRegistryByEntityId(this._entityReg)[
stateItem.entity_id
@@ -1000,13 +960,14 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
? // @ts-ignore
items.intersection(labelItems)
: new Set([...items].filter((x) => labelItems!.has(x)));
} else if (
}
if (
key === "ha-filter-categories" &&
Array.isArray(filter) &&
filter.length
) {
const categoryItems = new Set<string>();
this._helperEntities
this._stateItems
.filter(
(stateItem) =>
filter[0] ===
@@ -1026,85 +987,10 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
? // @ts-ignore
items.intersection(categoryItems)
: new Set([...items].filter((x) => categoryItems!.has(x)));
} else if (
key === "ha-filter-voice-assistants" &&
Array.isArray(filter) &&
filter.length
) {
const assistItems = new Set<string>();
this._helperEntities
.filter((stateItem) =>
getEntityVoiceAssistantsIds(
this._entityReg,
stateItem.entity_id
).some((va) => (filter as string[]).includes(va))
)
.forEach((stateItem) => assistItems.add(stateItem.entity_id));
(this._disabledEntityEntries || [])
.filter((entry) =>
getEntityVoiceAssistantsIds(this._entityReg, entry.entity_id).some(
(va) => (filter as string[]).includes(va)
)
)
.forEach((entry) => assistItems.add(entry.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._filteredHelperEntityIds = items ? [...items] : undefined;
}
public connectedCallback() {
super.connectedCallback();
window.addEventListener("location-changed", this._locationChanged);
window.addEventListener("popstate", this._popState);
}
disconnectedCallback(): void {
super.disconnectedCallback();
window.removeEventListener("location-changed", this._locationChanged);
window.removeEventListener("popstate", this._popState);
}
private _locationChanged = () => {
if (window.location.search.substring(1) !== this._searchParms.toString()) {
this._searchParms = new URLSearchParams(window.location.search);
this._setFiltersFromUrl();
}
};
private _popState = () => {
if (window.location.search.substring(1) !== this._searchParms.toString()) {
this._searchParms = new URLSearchParams(window.location.search);
this._setFiltersFromUrl();
}
};
private _setFiltersFromUrl() {
const device = this._searchParms.get("device");
const label = this._searchParms.get("label");
const category = this._searchParms.get("category");
const voiceAssistant = this._searchParms.get("voice_assistant");
if (!category && !label && !device) {
return;
}
this._filter = history.state?.filter || "";
this._filters = {
"ha-filter-devices": device ? [device] : [],
"ha-filter-labels": label ? [label] : [],
"ha-filter-categories": category ? [category] : [],
"ha-filter-voice-assistants": voiceAssistant ? [voiceAssistant] : [],
};
this._filteredStateItems = items ? [...items] : undefined;
}
private _clearFilter() {
@@ -1207,7 +1093,7 @@ ${rejected
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._setFiltersFromUrl();
this._fetchEntitySources();
if (isComponentLoaded(this.hass, "diagnostics")) {
@@ -1320,10 +1206,6 @@ ${rejected
protected willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
this._setFiltersFromUrl();
}
if (!this._entityEntries || !this._configEntries) {
return;
}
@@ -1343,7 +1225,7 @@ ${rejected
}
let changed =
!this._helperEntities ||
!this._stateItems ||
changedProps.has("_entityEntries") ||
changedProps.has("_configEntries") ||
changedProps.has("_entitySource");
@@ -1358,17 +1240,17 @@ ${rejected
const entityIds = Object.keys(this._entitySource);
const newHelpers = Object.values(this.hass!.states).filter(
const newStates = Object.values(this.hass!.states).filter(
(entity) =>
entityIds.includes(entity.entity_id) ||
isHelperDomain(computeStateDomain(entity))
);
if (
this._helperEntities.length !== newHelpers.length ||
!this._helperEntities.every((val, idx) => newHelpers[idx] === val)
this._stateItems.length !== newStates.length ||
!this._stateItems.every((val, idx) => newStates[idx] === val)
) {
this._helperEntities = newHelpers;
this._stateItems = newStates;
}
}

View File

@@ -327,6 +327,7 @@ class AddIntegrationDialog extends LitElement {
return html`<ha-dialog
open
@closed=${this.closeDialog}
scrimClickAction
hideActions
.heading=${createCloseHeading(
this.hass,

View File

@@ -23,6 +23,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { isDevVersion } from "../../../common/config/version";
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
import { copyToClipboard } from "../../../common/util/copy-clipboard";
@@ -212,7 +213,10 @@ class HaConfigEntryRow extends LitElement {
? html`<ha-button slot="end" @click=${this._handleEnable}>
${this.hass.localize("ui.common.enable")}
</ha-button>`
: configPanel && !stateText
: configPanel &&
(item.domain !== "matter" ||
isDevVersion(this.hass.config.version)) &&
!stateText
? html`<a
slot="end"
href=${`/${configPanel}?config_entry=${item.entry_id}`}

View File

@@ -1,19 +1,11 @@
import { mdiAlertCircle, mdiCheckCircle, mdiPlus } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-expansion-panel";
import "../../../../../components/ha-fab";
import "../../../../../components/ha-svg-icon";
import type { ConfigEntry } from "../../../../../data/config_entries";
import { getConfigEntries } from "../../../../../data/config_entries";
import type { HomeAssistant } from "../../../../../types";
import "../../../../../components/ha-button";
import {
acceptSharedMatterDevice,
canCommissionMatterExternal,
@@ -26,6 +18,7 @@ import {
import { showPromptDialog } from "../../../../../dialogs/generic/show-dialog-box";
import "../../../../../layouts/hass-subpage";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
@customElement("matter-config-dashboard")
export class MatterConfigDashboard extends LitElement {
@@ -33,8 +26,6 @@ export class MatterConfigDashboard extends LitElement {
@property({ type: Boolean }) public narrow = false;
@state() private _configEntry?: ConfigEntry;
@state() private _error?: string;
private _unsub?: UnsubscribeFunc;
@@ -44,33 +35,10 @@ export class MatterConfigDashboard extends LitElement {
this._stopRedirect();
}
protected firstUpdated(changedProperties: PropertyValues) {
super.firstUpdated(changedProperties);
if (this.hass) {
this._fetchConfigEntry();
}
}
private _matterDeviceCount = memoizeOne(
(devices: HomeAssistant["devices"]): number =>
Object.values(devices).filter((device) =>
device.identifiers.some((identifier) => identifier[0] === "matter")
).length
);
protected render(): TemplateResult | typeof nothing {
if (!this._configEntry) {
return nothing;
}
const isOnline = this._configEntry.state === "loaded";
protected render(): TemplateResult {
return html`
<hass-subpage
.narrow=${this.narrow}
.hass=${this.hass}
header="Matter"
has-fab
>
${isComponentLoaded(this.hass, "thread")
<hass-subpage .narrow=${this.narrow} .hass=${this.hass} header="Matter">
${isComponentLoaded(this.hass, "otbr")
? html`
<ha-button
appearance="plain"
@@ -83,114 +51,53 @@ export class MatterConfigDashboard extends LitElement {
)}</ha-button
>
`
: nothing}
<div class="container">
<ha-card class="network-status">
: ""}
<div class="content">
<ha-card header="Matter">
<ha-alert alert-type="warning"
>${this.hass.localize(
"ui.panel.config.matter.panel.experimental_note"
)}</ha-alert
>
<div class="card-content">
<div class="heading">
<div class="icon">
<ha-svg-icon
.path=${isOnline ? mdiCheckCircle : mdiAlertCircle}
class=${isOnline ? "online" : "offline"}
></ha-svg-icon>
</div>
<div class="details">
Matter
${this.hass.localize(
"ui.panel.config.matter.panel.status_title"
)}:
${this.hass.localize(
`ui.panel.config.matter.panel.status_${isOnline ? "online" : "offline"}`
)}<br />
<small>
${this.hass.localize(
"ui.panel.config.matter.panel.devices",
{ count: this._matterDeviceCount(this.hass.devices) }
)}
</small>
</div>
</div>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
${this.hass.localize("ui.panel.config.matter.panel.add_devices")}
</div>
<div class="card-actions">
<ha-button
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
appearance="plain"
size="small"
${canCommissionMatterExternal(this.hass)
? html`<ha-button
appearance="plain"
@click=${this._startMobileCommissioning}
>${this.hass.localize(
"ui.panel.config.matter.panel.mobile_app_commisioning"
)}</ha-button
>`
: ""}
<ha-button appearance="plain" @click=${this._commission}
>${this.hass.localize(
"ui.panel.config.matter.panel.commission_device"
)}</ha-button
>
${this.hass.localize("ui.panel.config.devices.caption")}
</ha-button>
<ha-button
appearance="plain"
size="small"
href=${`/config/entities/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
<ha-button appearance="plain" @click=${this._acceptSharedDevice}
>${this.hass.localize(
"ui.panel.config.matter.panel.add_shared_device"
)}</ha-button
>
<ha-button appearance="plain" @click=${this._setWifi}
>${this.hass.localize(
"ui.panel.config.matter.panel.set_wifi_credentials"
)}</ha-button
>
<ha-button appearance="plain" @click=${this._setThread}
>${this.hass.localize(
"ui.panel.config.matter.panel.set_thread_credentials"
)}</ha-button
>
${this.hass.localize("ui.panel.config.entities.caption")}
</ha-button>
</div>
</ha-card>
<ha-expansion-panel
outlined
.header=${this.hass.localize(
"ui.panel.config.matter.panel.developer_tools_title"
)}
.secondary=${this.hass.localize(
"ui.panel.config.matter.panel.developer_tools_description"
)}
>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
<div class="dev-tools-content">
<p>
${this.hass.localize(
"ui.panel.config.matter.panel.developer_tools_info"
)}
</p>
<div class="dev-tools-actions">
${canCommissionMatterExternal(this.hass)
? html`<ha-button
appearance="plain"
@click=${this._startMobileCommissioning}
>${this.hass.localize(
"ui.panel.config.matter.panel.mobile_app_commisioning"
)}</ha-button
>`
: nothing}
<ha-button appearance="plain" @click=${this._commission}
>${this.hass.localize(
"ui.panel.config.matter.panel.commission_device"
)}</ha-button
>
<ha-button appearance="plain" @click=${this._acceptSharedDevice}
>${this.hass.localize(
"ui.panel.config.matter.panel.add_shared_device"
)}</ha-button
>
<ha-button appearance="plain" @click=${this._setWifi}
>${this.hass.localize(
"ui.panel.config.matter.panel.set_wifi_credentials"
)}</ha-button
>
<ha-button appearance="plain" @click=${this._setThread}
>${this.hass.localize(
"ui.panel.config.matter.panel.set_thread_credentials"
)}</ha-button
>
</div>
</div>
</ha-expansion-panel>
</div>
<a href="/config/matter/add" slot="fab">
<ha-fab
.label=${this.hass.localize(
"ui.panel.config.matter.panel.add_device"
)}
extended
>
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</ha-fab>
</a>
</hass-subpage>
`;
}
@@ -329,101 +236,27 @@ export class MatterConfigDashboard extends LitElement {
}
}
private async _fetchConfigEntry(): Promise<void> {
const configEntries = await getConfigEntries(this.hass, {
domain: "matter",
});
if (configEntries.length) {
this._configEntry = configEntries[0];
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
ha-card {
margin: auto;
margin-top: var(--ha-space-4);
max-width: 500px;
}
ha-card .card-actions {
display: flex;
justify-content: flex-end;
}
.network-status div.heading {
display: flex;
align-items: center;
}
.network-status div.heading .icon {
margin-inline-end: var(--ha-space-4);
}
.network-status div.heading ha-svg-icon {
--mdc-icon-size: 48px;
}
.network-status div.heading .details {
font-size: var(--ha-font-size-xl);
}
.network-status small {
font-size: var(--ha-font-size-m);
}
.network-status .online {
color: var(--state-on-color, var(--success-color));
}
.network-status .offline {
color: var(--error-color, var(--error-color));
}
.container {
padding: var(--ha-space-2) var(--ha-space-4) var(--ha-space-4);
}
ha-expansion-panel {
margin: auto;
margin-top: var(--ha-space-4);
max-width: 500px;
background: var(--card-background-color);
border-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
--expansion-panel-summary-padding: var(--ha-space-2) var(--ha-space-4);
--expansion-panel-content-padding: 0 var(--ha-space-4);
}
.dev-tools-content {
padding: 0 0 var(--ha-space-4);
}
.dev-tools-content p {
margin: 0 0 var(--ha-space-4);
color: var(--primary-text-color);
}
.dev-tools-actions {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--ha-space-2);
}
a[slot="toolbar-icon"] {
text-decoration: none;
}
a[slot="fab"] {
text-decoration: none;
}
`,
];
}
static styles = [
haStyle,
css`
ha-alert[alert-type="warning"] {
position: relative;
top: -16px;
}
.content {
padding: 24px 0 32px;
max-width: 600px;
margin: 0 auto;
direction: ltr;
}
ha-card:first-child {
margin-bottom: 16px;
}
a[slot="toolbar-icon"] {
text-decoration: none;
}
`,
];
}
declare global {

View File

@@ -45,9 +45,9 @@ class DialogThreadDataset extends LitElement implements HassDialog {
<div>
Network name: ${dataset.network_name}<br />
Channel: ${dataset.channel}<br />
Dataset ID: ${dataset.dataset_id}<br />
PAN ID: ${dataset.pan_id}<br />
Extended PAN ID: ${dataset.extended_pan_id}<br />
Dataset id: ${dataset.dataset_id}<br />
Pan id: ${dataset.pan_id}<br />
Extended Pan id: ${dataset.extended_pan_id}<br />
${hasOTBR
? html`OTBR URL: ${otbrInfo.url}<br />

View File

@@ -51,7 +51,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-icon-overflow-menu";
import "../../../components/ha-md-divider";
@@ -108,8 +107,6 @@ import { showAssignCategoryDialog } from "../category/show-dialog-assign-categor
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
import { configSections } from "../ha-panel-config";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import { getEntityVoiceAssistantsIds } from "../../../data/expose";
import "../voice-assistants/expose/expose-assistant-icon";
type SceneItem = SceneEntity & {
name: string;
@@ -413,31 +410,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
</ha-icon-overflow-menu>
`,
},
voice_assistants: {
title: localize(
"ui.panel.config.scene.picker.headers.voice_assistants"
),
type: "icon",
defaultHidden: true,
minWidth: "100px",
maxWidth: "100px",
template: (scene) => {
const exposedToVoiceAssistantIds = getEntityVoiceAssistantsIds(
this._entityReg,
scene.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;
@@ -680,15 +652,6 @@ class HaSceneDashboard 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>
${!this.narrow
? html`<ha-md-button-menu slot="selection-bar">
@@ -924,7 +887,8 @@ class HaSceneDashboard 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
@@ -946,28 +910,6 @@ class HaSceneDashboard 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.scenes
.filter((scene) =>
getEntityVoiceAssistantsIds(this._entityReg, scene.entity_id).some(
(va) => (filter.value as string[]).includes(va)
)
)
.forEach((scene) => assistItems.add(scene.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._filteredScenes = items ? [...items] : undefined;

View File

@@ -53,7 +53,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-icon-overflow-menu";
import "../../../components/ha-md-divider";
@@ -112,8 +111,6 @@ import { showAssignCategoryDialog } from "../category/show-dialog-assign-categor
import { showCategoryRegistryDetailDialog } from "../category/show-dialog-category-registry-detail";
import { configSections } from "../ha-panel-config";
import { showLabelDetailDialog } from "../labels/show-dialog-label-detail";
import { getEntityVoiceAssistantsIds } from "../../../data/expose";
import "../voice-assistants/expose/expose-assistant-icon";
type ScriptItem = ScriptEntity & {
name: string;
@@ -401,32 +398,8 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
</ha-icon-overflow-menu>
`,
},
voice_assistants: {
title: localize(
"ui.panel.config.script.picker.headers.voice_assistants"
),
type: "icon",
defaultHidden: true,
minWidth: "100px",
maxWidth: "100px",
template: (script) => {
const exposedToVoiceAssistantIds = getEntityVoiceAssistantsIds(
this._entityReg,
script.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;
}
);
@@ -662,15 +635,6 @@ class HaScriptPicker 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=${"script"}
@@ -929,7 +893,8 @@ class HaScriptPicker 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
@@ -951,28 +916,6 @@ class HaScriptPicker 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.scripts
.filter((script) =>
getEntityVoiceAssistantsIds(this._entityReg, script.entity_id).some(
(va) => (filter.value as string[]).includes(va)
)
)
.forEach((script) => assistItems.add(script.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._filteredScripts = items ? [...items] : undefined;

View File

@@ -23,6 +23,7 @@ export class VoiceAssistantExposeAssistantIcon extends LitElement {
render() {
if (!this.assistant || !voiceAssistants[this.assistant]) return nothing;
return html`
<div class="container" id="container">
<img

View File

@@ -21,8 +21,6 @@ class EventSubscribeCard extends LitElement {
@state() private _subscribed?: () => void;
@state() private _eventFilter = "";
@state() private _events: {
id: number;
event: HassEvent;
@@ -32,8 +30,6 @@ class EventSubscribeCard extends LitElement {
private _eventCount = 0;
@state() _ignoredEventsCount = 0;
public disconnectedCallback() {
super.disconnectedCallback();
if (this._subscribed) {
@@ -74,16 +70,6 @@ class EventSubscribeCard extends LitElement {
.value=${this._eventType}
@input=${this._valueChanged}
></ha-textfield>
<ha-textfield
.label=${this.hass!.localize(
"ui.panel.developer-tools.tabs.events.filter_events"
)}
.value=${this._eventFilter}
.disabled=${this._subscribed !== undefined}
helperPersistent
.helper=${`${this.hass!.localize("ui.panel.developer-tools.tabs.events.filter_helper")}${this._ignoredEventsCount ? ` ${this.hass!.localize("ui.panel.developer-tools.tabs.events.filter_ignored", { count: this._ignoredEventsCount })}` : ""}`}
@input=${this._filterChanged}
></ha-textfield>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
@@ -149,46 +135,6 @@ class EventSubscribeCard extends LitElement {
this._error = undefined;
}
private _filterChanged(ev): void {
this._eventFilter = ev.target.value;
}
private _testEventFilter(event: HassEvent): boolean {
if (!this._eventFilter) {
return true;
}
const searchStr = this._eventFilter;
function visit(node) {
// Handle primitives directly
if (node === null || typeof node !== "object") {
return String(node).includes(searchStr);
}
// Handle arrays and plain objects
for (const key in node) {
if (!Object.prototype.hasOwnProperty.call(node, key)) continue;
// Check key
if (key.includes(searchStr)) return true;
const value = node[key];
// Check primitive value
if (value === null || typeof value !== "object") {
if (String(value).includes(searchStr)) return true;
} else if (visit(value)) {
// Recurse into nested object/array
return true;
}
}
return false;
}
return visit(event);
}
private async _startOrStopListening(): Promise<void> {
if (this._subscribed) {
this._subscribed();
@@ -198,10 +144,6 @@ class EventSubscribeCard extends LitElement {
try {
this._subscribed =
await this.hass!.connection.subscribeEvents<HassEvent>((event) => {
if (!this._testEventFilter(event)) {
this._ignoredEventsCount++;
return;
}
const tail =
this._events.length > 30
? this._events.slice(0, 29)
@@ -226,7 +168,6 @@ class EventSubscribeCard extends LitElement {
private _clearEvents(): void {
this._events = [];
this._eventCount = 0;
this._ignoredEventsCount = 0;
this._error = undefined;
}

View File

@@ -201,7 +201,6 @@ class HaPanelDevStatistics extends KeyboardShortcutMixin(LitElement) {
label: this.hass.localize(
"ui.panel.developer-tools.tabs.statistics.fix_issue.fix"
),
type: "icon",
template: (statistic) =>
html`${statistic.issues
? html`<ha-button

View File

@@ -283,18 +283,13 @@ class PanelEnergy extends LitElement {
["grid", "solar", "battery"].includes(source.type)
);
const hasPowerSource = this._prefs.energy_sources.some(
(source) =>
(source.type === "solar" && source.stat_rate) ||
(source.type === "battery" && source.stat_rate) ||
(source.type === "grid" && source.power?.length)
);
const hasDevicePower = this._prefs.device_consumption.some(
(device) => device.stat_rate
);
const hasPower = hasPowerSource || hasDevicePower;
const hasPower =
this._prefs.energy_sources.some(
(source) =>
(source.type === "solar" && source.stat_rate) ||
(source.type === "battery" && source.stat_rate) ||
(source.type === "grid" && source.power?.length)
) || this._prefs.device_consumption.some((device) => device.stat_rate);
const hasWater =
this._prefs.energy_sources.some((source) => source.type === "water") ||
@@ -319,10 +314,7 @@ class PanelEnergy extends LitElement {
if (hasPower) {
views.push(POWER_VIEW);
}
if (
hasPowerSource ||
[hasEnergy, hasGas, hasWater].filter(Boolean).length > 1
) {
if (views.length > 1) {
views.unshift(OVERVIEW_VIEW);
}
return {

View File

@@ -31,7 +31,6 @@ import { formatTime } from "../../../../../common/datetime/format_time";
import type { ECOption } from "../../../../../resources/echarts/echarts";
import { filterXSS } from "../../../../../common/util/xss";
import type { StatisticPeriod } from "../../../../../data/recorder";
import { getSuggestedPeriod } from "../../../../../data/energy";
export function getSuggestedMax(period: StatisticPeriod, end: Date): number {
let suggestedMax = new Date(end);
@@ -57,6 +56,10 @@ export function getSuggestedMax(period: StatisticPeriod, end: Date): number {
return suggestedMax.getTime();
}
export function getSuggestedPeriod(dayDifference: number): StatisticPeriod {
return dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour";
}
function createYAxisLabelFormatter(locale: FrontendLocaleData) {
let previousValue: number | undefined;
@@ -92,7 +95,7 @@ export function getCommonOptions(
type: "time",
min: start,
max: getSuggestedMax(
getSuggestedPeriod(start, end, detailedDailyData),
detailedDailyData ? "5minute" : getSuggestedPeriod(dayDifference),
end
),
},

View File

@@ -186,7 +186,7 @@ export class HuiEnergyDevicesGraphCard
params.value[0] as number,
this.hass.locale,
params.value < 0.1 ? { maximumFractionDigits: 3 } : undefined
)} kWh ${params.percent ? `(${params.percent} %)` : ""}`;
)} kWh`;
return `${title}${params.marker} ${params.seriesName}: <div style="direction:ltr; display: inline;">${value}</div>`;
}

View File

@@ -1,4 +1,4 @@
import { endOfToday, isToday, startOfToday } from "date-fns";
import { differenceInDays, endOfToday, isToday, startOfToday } from "date-fns";
import type { HassConfig, UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
@@ -18,7 +18,6 @@ import type {
import {
getEnergyDataCollection,
getEnergySolarForecasts,
getSuggestedPeriod,
} from "../../../../data/energy";
import type { Statistics, StatisticsMetaData } from "../../../../data/recorder";
import { getStatisticLabel } from "../../../../data/recorder";
@@ -355,7 +354,7 @@ export class HuiEnergySolarGraphCard
) {
const data: LineSeriesOption[] = [];
const period = getSuggestedPeriod(start, end);
const dayDifference = differenceInDays(end || new Date(), start);
// Process solar forecast data.
solarSources.forEach((source) => {
@@ -371,10 +370,10 @@ export class HuiEnergySolarGraphCard
if (dateObj < start || (end && dateObj > end)) {
return;
}
if (period === "month") {
if (dayDifference > 35) {
dateObj.setDate(1);
}
if (period === "month" || period === "day") {
if (dayDifference > 2) {
dateObj.setHours(0, 0, 0, 0);
} else {
dateObj.setMinutes(0, 0, 0);

View File

@@ -67,19 +67,6 @@ export const SUM_DEVICE_CLASSES = [
"water",
];
// Additional sources for sensor device classes from entity attributes
// Maps device_class -> array of { domain, attribute } to include in aggregation
export const SENSOR_ATTRIBUTE_SOURCES: Record<
string,
{ domain: string; attribute: string }[]
> = {
temperature: [{ domain: "climate", attribute: "current_temperature" }],
humidity: [
{ domain: "climate", attribute: "current_humidity" },
{ domain: "humidifier", attribute: "current_humidity" },
],
};
export interface AreaCardFeatureContext extends LovelaceCardFeatureContext {
exclude_entities?: string[];
}
@@ -264,24 +251,6 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
}
);
private _domainEntityIds = memoizeOne(
(
entities: HomeAssistant["entities"],
areaId: string,
domains: string[],
excludeEntities?: string[]
): string[] => {
const filter = generateEntityFilter(this.hass, {
area: areaId,
entity_category: "none",
domain: domains,
});
return Object.keys(entities).filter(
(id) => filter(id) && !excludeEntities?.includes(id)
);
}
);
private _computeActiveAlertStates(): HassEntity[] {
const areaId = this._config?.area;
const area = areaId ? this.hass.areas[areaId] : undefined;
@@ -390,91 +359,58 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
: this.hass.formatEntityState(stateObj);
}
const sensorEntityIds = groupedEntities.get(sensorClass) || [];
const values: number[] = [];
let uom: string | undefined;
const entityIds = groupedEntities.get(sensorClass);
// Track devices that have sensor entities contributing values
// to avoid duplicate readings from climate/humidifier attributes
const devicesWithSensorValues = new Set<string>();
for (const entityId of sensorEntityIds) {
const stateObj = this.hass.states[entityId];
if (
stateObj &&
!isUnavailableState(stateObj.state) &&
isNumericState(stateObj) &&
!isNaN(Number(stateObj.state))
) {
if (!uom) {
uom = stateObj.attributes.unit_of_measurement;
}
if (stateObj.attributes.unit_of_measurement === uom) {
values.push(Number(stateObj.state));
// Track the device this sensor belongs to
const entityEntry = this.hass.entities[entityId];
if (entityEntry?.device_id) {
devicesWithSensorValues.add(entityEntry.device_id);
}
}
}
if (!entityIds) {
return undefined;
}
// Collect values from additional attribute sources
const attrSources = SENSOR_ATTRIBUTE_SOURCES[sensorClass];
if (attrSources) {
const domains = [...new Set(attrSources.map((s) => s.domain))];
const attrEntityIds = this._domainEntityIds(
this.hass.entities,
area.area_id,
domains,
excludeEntities
);
// Ensure all entities have state
const entities = entityIds
.map((entityId) => this.hass.states[entityId])
.filter(Boolean);
for (const entityId of attrEntityIds) {
const stateObj = this.hass.states[entityId];
if (!stateObj) continue;
// Skip if this entity's device already has a sensor contributing values
const entityEntry = this.hass.entities[entityId];
if (
entityEntry?.device_id &&
devicesWithSensorValues.has(entityEntry.device_id)
) {
continue;
}
const domain = entityId.split(".")[0];
const source = attrSources.find((s) => s.domain === domain);
if (!source) continue;
const attrValue = stateObj.attributes[source.attribute];
if (attrValue == null || isNaN(Number(attrValue))) continue;
if (!uom) {
// Determine unit from attribute
uom = this._getAttributeUnit(sensorClass, domain);
}
values.push(Number(attrValue));
}
if (entities.length === 0) {
return undefined;
}
if (values.length === 0) {
// If only one entity, return its formatted state
if (entities.length === 1) {
const stateObj = entities[0];
return isUnavailableState(stateObj.state)
? ""
: this.hass.formatEntityState(stateObj);
}
// Use the first entity's unit_of_measurement for formatting
const uom = entities.find(
(entity) => entity.attributes.unit_of_measurement
)?.attributes.unit_of_measurement;
// Ensure all entities have the same unit_of_measurement
const validEntities = entities.filter(
(entity) =>
entity.attributes.unit_of_measurement === uom &&
isNumericState(entity) &&
!isNaN(Number(entity.state))
);
if (validEntities.length === 0) {
return undefined;
}
const value = SUM_DEVICE_CLASSES.includes(sensorClass)
? values.reduce((acc, v) => acc + v, 0)
: this._computeMedianValue(values);
? this._computeSumState(validEntities)
: this._computeMedianState(validEntities);
const formattedValue = formatNumber(value, this.hass.locale, {
const formattedAverage = formatNumber(value, this.hass!.locale, {
maximumFractionDigits: 1,
});
const formattedUnit = uom
? `${blankBeforeUnit(uom, this.hass.locale)}${uom}`
? `${blankBeforeUnit(uom, this.hass!.locale)}${uom}`
: "";
return `${formattedValue}${formattedUnit}`;
return `${formattedAverage}${formattedUnit}`;
})
.filter(Boolean)
.join(" · ");
@@ -482,25 +418,20 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
return sensorStates;
}
private _getAttributeUnit(sensorClass: string, domain: string): string {
// Return the expected unit for attributes from specific domains
if (sensorClass === "temperature" && domain === "climate") {
return this.hass.config.unit_system.temperature;
}
if (sensorClass === "humidity") {
return "%";
}
return "";
private _computeSumState(entities: HassEntity[]): number {
return entities.reduce((acc, entity) => acc + Number(entity.state), 0);
}
private _computeMedianValue(values: number[]): number {
const sortedValues = [...values].sort((a, b) => a - b);
if (sortedValues.length % 2 === 0) {
const medianIndex = sortedValues.length / 2;
return (sortedValues[medianIndex] + sortedValues[medianIndex - 1]) / 2;
private _computeMedianState(entities: HassEntity[]): number {
const sortedStates = entities
.map((entity) => Number(entity.state))
.sort((a, b) => a - b);
if (sortedStates.length % 2 === 0) {
const medianIndex = sortedStates.length / 2;
return (sortedStates[medianIndex] + sortedStates[medianIndex - 1]) / 2;
}
const medianIndex = Math.floor(sortedValues.length / 2);
return sortedValues[medianIndex];
const medianIndex = Math.floor(sortedStates.length / 2);
return sortedStates[medianIndex];
}
private _featurePosition = memoizeOne((config: AreaCardConfig) => {

View File

@@ -94,12 +94,10 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard {
(changedProps.has("_config") && this._config?.entities)
) {
const computedStyles = getComputedStyle(this);
if (this._config?.entities) {
this._calendars = this._config.entities.map((entity, idx) => ({
entity_id: entity,
backgroundColor: getColorByIndex(idx, computedStyles),
}));
}
this._calendars = this._config!.entities.map((entity, idx) => ({
entity_id: entity,
backgroundColor: getColorByIndex(idx, computedStyles),
}));
}
}

View File

@@ -1,116 +1,63 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { customElement, property } from "lit/decorators";
import "../../../components/ha-card";
import "../../../components/ha-button";
import "../../../components/ha-icon";
import type { HomeAssistant } from "../../../types";
import { handleAction } from "../common/handle-action";
import type { LovelaceCard, LovelaceCardEditor } from "../types";
import type { LovelaceCard } from "../types";
import type { EmptyStateCardConfig } from "./types";
@customElement("hui-empty-state-card")
export class HuiEmptyStateCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import("../editor/config-elements/hui-empty-state-card-editor");
return document.createElement("hui-empty-state-card-editor");
}
public static getStubConfig(): EmptyStateCardConfig {
return {
type: "empty-state",
title: "Welcome Home",
content: "This is an empty state card.",
};
}
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: EmptyStateCardConfig;
public getCardSize(): number {
return 2;
}
public setConfig(config: EmptyStateCardConfig): void {
this._config = config;
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
public setConfig(_config: EmptyStateCardConfig): void {}
protected render() {
if (!this.hass || !this._config) {
if (!this.hass) {
return nothing;
}
return html`
<ha-card
class=${classMap({
"content-only": this._config.content_only ?? false,
})}
.header=${this.hass.localize(
"ui.panel.lovelace.cards.empty_state.title"
)}
>
<div class="container">
${this._config.icon
? html`<ha-icon .icon=${this._config.icon}></ha-icon>`
: nothing}
${this._config.title ? html`<h1>${this._config.title}</h1>` : nothing}
${this._config.content
? html`<p>${this._config.content}</p>`
: nothing}
${this._config.tap_action && this._config.action_button_text
? html`
<ha-button @click=${this._handleAction}>
${this._config.action_button_text}
</ha-button>
`
: nothing}
<div class="card-content">
${this.hass.localize(
"ui.panel.lovelace.cards.empty_state.no_devices"
)}
</div>
<div class="card-actions">
<ha-button appearance="plain" href="/config/integrations/dashboard">
${this.hass.localize(
"ui.panel.lovelace.cards.empty_state.go_to_integrations_page"
)}
</ha-button>
</div>
</ha-card>
`;
}
private _handleAction(): void {
if (this._config?.tap_action && this.hass) {
handleAction(this, this.hass, this._config, "tap");
}
}
static styles = css`
:host {
display: block;
height: 100%;
.content {
margin-top: -1em;
padding: 16px;
}
ha-card {
height: 100%;
.card-actions a {
text-decoration: none;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
height: 100%;
padding: var(--ha-space-8) var(--ha-space-4);
box-sizing: border-box;
gap: var(--ha-space-4);
max-width: 640px;
margin: 0 auto;
}
ha-icon {
--mdc-icon-size: var(--ha-space-12);
color: var(--secondary-text-color);
}
h1 {
margin: 0;
font-size: var(--ha-font-size-xl);
font-weight: 500;
}
p {
margin: 0;
color: var(--secondary-text-color);
}
.content-only {
background: none;
box-shadow: none;
border: none;
ha-button {
margin-left: -8px;
margin-inline-start: -8px;
margin-inline-end: initial;
}
`;
}

View File

@@ -11,10 +11,7 @@ import { findEntities } from "../common/find-entities";
import type { LovelaceElement, LovelaceElementConfig } from "../elements/types";
import type { LovelaceCard, LovelaceCardEditor } from "../types";
import { createStyledHuiElement } from "./picture-elements/create-styled-hui-element";
import {
PREVIEW_CLICK_CALLBACK,
type PictureElementsCardConfig,
} from "./types";
import type { PictureElementsCardConfig } from "./types";
import type { PersonEntity } from "../../../data/person";
@customElement("hui-picture-elements-card")
@@ -169,7 +166,6 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
.aspectRatio=${this._config.aspect_ratio}
.darkModeFilter=${this._config.dark_mode_filter}
.darkModeImage=${darkModeImage}
@click=${this._handleImageClick}
></hui-image>
${this._elements}
</div>
@@ -225,19 +221,6 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard {
curCardEl === elToReplace ? newCardEl : curCardEl
);
}
private _handleImageClick(ev: MouseEvent): void {
if (!this.preview || !this._config?.[PREVIEW_CLICK_CALLBACK]) {
return;
}
const rect = (ev.currentTarget as HTMLElement).getBoundingClientRect();
const x = ((ev.clientX - rect.left) / rect.width) * 100;
const y = ((ev.clientY - rect.top) / rect.height) * 100;
// only the edited card has this callback
this._config[PREVIEW_CLICK_CALLBACK](x, y);
}
}
declare global {

View File

@@ -8,10 +8,7 @@ import { createSearchParam } from "../../../common/url/search-params";
import "../../../components/ha-card";
import "../../../components/ha-icon-next";
import "../../../components/ha-tooltip";
import {
getEnergyDataCollection,
getSuggestedPeriod,
} from "../../../data/energy";
import { getEnergyDataCollection } from "../../../data/energy";
import type {
Statistics,
StatisticsMetaData,
@@ -29,7 +26,10 @@ import { hasConfigOrEntitiesChanged } from "../common/has-changed";
import { processConfigEntities } from "../common/process-config-entities";
import type { EntityConfig } from "../entity-rows/types";
import type { LovelaceCard, LovelaceGridOptions } from "../types";
import { getSuggestedMax } from "./energy/common/energy-chart-options";
import {
getSuggestedMax,
getSuggestedPeriod,
} from "./energy/common/energy-chart-options";
import type { StatisticsGraphCardConfig } from "./types";
export const DEFAULT_DAYS_TO_SHOW = 30;
@@ -268,7 +268,9 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
return (
this._config?.period ??
(this._energyStart && this._energyEnd
? getSuggestedPeriod(this._energyStart, this._energyEnd)
? getSuggestedPeriod(
differenceInDays(this._energyEnd, this._energyStart)
)
: undefined)
);
}

View File

@@ -58,12 +58,8 @@ export interface ConditionalCardConfig extends LovelaceCardConfig {
}
export interface EmptyStateCardConfig extends LovelaceCardConfig {
content_only?: boolean;
icon?: string;
content: string;
title?: string;
content?: string;
action_button_text?: string;
tap_action?: ActionConfig;
}
export interface EntityCardConfig extends LovelaceCardConfig {
@@ -487,10 +483,6 @@ export interface PictureCardConfig extends LovelaceCardConfig {
alt_text?: string;
}
// Symbol for preview click callback - preserved through spreads, not serialized
// This allows the editor to attach a callback that only exists on the edited card's config
export const PREVIEW_CLICK_CALLBACK = Symbol("previewClickCallback");
export interface PictureElementsCardConfig extends LovelaceCardConfig {
title?: string;
image?: string | MediaSelectorValue;
@@ -505,7 +497,6 @@ export interface PictureElementsCardConfig extends LovelaceCardConfig {
theme?: string;
dark_mode_image?: string | MediaSelectorValue;
dark_mode_filter?: string;
[PREVIEW_CLICK_CALLBACK]?: (x: number, y: number) => void;
}
export interface PictureEntityCardConfig extends LovelaceCardConfig {

View File

@@ -21,8 +21,5 @@ export const confirmAction = async (
hass.localize("ui.panel.lovelace.cards.actions.action_confirmation", {
action,
}),
title: config.title,
dismissText: config.dismiss_text,
confirmText: config.confirm_text,
});
};

View File

@@ -368,7 +368,6 @@ export const generateViewConfig = (
path: string,
title: string | undefined,
icon: string | undefined,
show_icon_and_title: boolean | undefined,
entities: HassEntities
): LovelaceViewConfig => {
const ungroupedEntitites: Record<string, string[]> = {};
@@ -498,9 +497,6 @@ export const generateViewConfig = (
if (icon) {
view.icon = icon;
}
if (show_icon_and_title) {
view.show_icon_and_title = show_icon_and_title;
}
return view;
};
@@ -521,7 +517,6 @@ export const generateDefaultViewConfig = (
const path = "default_view";
const title = "Home";
const icon = undefined;
const show_icon_and_title = undefined;
// In the case of a default view, we want to use the group order attribute
const groupOrders = {};
@@ -571,7 +566,6 @@ export const generateDefaultViewConfig = (
path,
title,
icon,
show_icon_and_title,
splittedByGroups.ungrouped
);

View File

@@ -89,9 +89,6 @@ export const handleAction = async (
) ||
actionConfig.action,
}),
title: actionConfig.confirmation.title,
dismissText: actionConfig.confirmation.dismiss_text,
confirmText: actionConfig.confirmation.confirm_text,
}))
) {
return;

View File

@@ -1,153 +0,0 @@
import { mdiGestureTap } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { assert, assign, boolean, object, optional, string } from "superstruct";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
import type {
HaFormSchema,
SchemaUnion,
} from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types";
import type { EmptyStateCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { actionConfigStruct } from "../structs/action-struct";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
content_only: optional(boolean()),
icon: optional(string()),
title: optional(string()),
content: optional(string()),
action_button_text: optional(string()),
tap_action: optional(actionConfigStruct),
})
);
@customElement("hui-empty-state-card-editor")
export class HuiEmptyStateCardEditor
extends LitElement
implements LovelaceCardEditor
{
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: EmptyStateCardConfig;
public setConfig(config: EmptyStateCardConfig): void {
assert(config, cardConfigStruct);
this._config = config;
}
private _schema = memoizeOne(
(localize: LocalizeFunc) =>
[
{
name: "style",
selector: {
select: {
mode: "box",
options: (
[
{ value: "card", image: "card" },
{ value: "content-only", image: "text_only" },
] as const
).map((style) => ({
label: localize(
`ui.panel.lovelace.editor.card.empty_state.style_options.${style.value}`
),
image: {
src: `/static/images/form/markdown_${style.image}.svg`,
src_dark: `/static/images/form/markdown_${style.image}_dark.svg`,
flip_rtl: true,
},
value: style.value,
})),
},
},
},
{ name: "icon", selector: { icon: {} } },
{ name: "title", selector: { text: {} } },
{ name: "content", selector: { text: { multiline: true } } },
{
name: "interactions",
type: "expandable",
flatten: true,
iconPath: mdiGestureTap,
schema: [
{ name: "action_button_text", selector: { text: {} } },
{
name: "tap_action",
selector: {
ui_action: {
default_action: "none",
},
},
},
],
},
] as const satisfies readonly HaFormSchema[]
);
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
const data = {
...this._config,
style: this._config.content_only ? "content-only" : "card",
};
const schema = this._schema(this.hass.localize);
return html`
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${schema}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _valueChanged(ev: CustomEvent): void {
const config = { ...ev.detail.value };
if (config.style === "content-only") {
config.content_only = true;
} else {
delete config.content_only;
}
delete config.style;
fireEvent(this, "config-changed", { config });
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
switch (schema.name) {
case "style":
case "content":
case "action_button_text":
return this.hass!.localize(
`ui.panel.lovelace.editor.card.empty_state.${schema.name}`
);
default:
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
);
}
};
}
declare global {
interface HTMLElementTagNameMap {
"hui-empty-state-card-editor": HuiEmptyStateCardEditor;
}
}

View File

@@ -28,16 +28,11 @@ import type { HomeAssistant, ValueChangedEvent } from "../../../../types";
import { DEFAULT_HOURS_TO_SHOW, DEFAULT_ZOOM } from "../../cards/hui-map-card";
import type { MapCardConfig, MapEntityConfig } from "../../cards/types";
import "../../components/hui-entity-editor";
import "../hui-sub-element-editor";
import type {
EditDetailElementEvent,
SubElementEditorConfig,
EntitiesEditorEvent,
} from "../types";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import type { EntityConfig } from "../../entity-rows/types";
import type { LovelaceCardEditor } from "../../types";
import { processEditorEntities } from "../process-editor-entities";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
import type { EntitiesEditorEvent } from "../types";
import { configElementStyle } from "./config-elements-style";
export const mapEntitiesConfigStruct = union([
@@ -81,20 +76,13 @@ const cardConfigStruct = assign(
const themeModes = ["auto", "light", "dark"] as const;
const SUB_SCHEMA = [
{ name: "entity", selector: { entity: {} }, required: true },
{ name: "name", selector: { text: {} } },
] as const;
@customElement("hui-map-card-editor")
export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: MapCardConfig;
@state() private _subElementEditorConfig?: SubElementEditorConfig;
@state() private _configEntities?: MapEntityConfig[];
@state() private _configEntities?: EntityConfig[];
@state() private _possibleGeoSources?: { value: string; label?: string }[];
@@ -162,7 +150,7 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
this._config = config;
this._configEntities = config.entities
? (processEditorEntities(config.entities) as MapEntityConfig[])
? processEditorEntities(config.entities)
: [];
}
@@ -179,19 +167,6 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
return nothing;
}
if (this._subElementEditorConfig) {
return html`
<hui-sub-element-editor
.hass=${this.hass}
.config=${this._subElementEditorConfig}
.schema=${SUB_SCHEMA}
@go-back=${this._goBack}
@config-changed=${this._handleSubEntityChanged}
>
</hui-sub-element-editor>
`;
}
return html`
<ha-form
.hass=${this.hass}
@@ -205,9 +180,7 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
.hass=${this.hass}
.entities=${this._configEntities}
.entityFilter=${hasLocation}
can-edit
@entities-changed=${this._entitiesValueChanged}
@edit-detail-element=${this._editDetailElement}
></hui-entity-editor>
<h3>
@@ -230,36 +203,6 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
`;
}
private _goBack(): void {
this._subElementEditorConfig = undefined;
}
private _editDetailElement(ev: HASSDomEvent<EditDetailElementEvent>): void {
this._subElementEditorConfig = ev.detail.subElementConfig;
}
private _handleSubEntityChanged(ev: CustomEvent): void {
ev.stopPropagation();
const index = this._subElementEditorConfig!.index!;
const newEntities = this._configEntities!.concat();
const newConfig = ev.detail.config as MapEntityConfig;
this._subElementEditorConfig = {
...this._subElementEditorConfig!,
elementConfig: newConfig,
};
newEntities[index] = newConfig;
let config = this._config!;
config = { ...config, entities: newEntities };
this._config = config;
this._configEntities = processEditorEntities(
config.entities as any[]
) as MapEntityConfig[];
fireEvent(this, "config-changed", { config });
}
private _selectSchema = memoizeOne(
(options, localize: LocalizeFunc): SelectSelector => ({
select: {
@@ -286,9 +229,7 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
if (ev.detail && ev.detail.entities) {
this._config = { ...this._config!, entities: ev.detail.entities };
this._configEntities = processEditorEntities(
this._config.entities || []
) as MapEntityConfig[];
this._configEntities = processEditorEntities(this._config.entities || []);
fireEvent(this, "config-changed", { config: this._config! });
}
}

View File

@@ -15,16 +15,12 @@ import {
} from "superstruct";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-alert";
import "../../../../components/ha-card";
import "../../../../components/ha-form/ha-form";
import "../../../../components/ha-icon";
import "../../../../components/ha-switch";
import type { HomeAssistant } from "../../../../types";
import {
PREVIEW_CLICK_CALLBACK,
type PictureElementsCardConfig,
} from "../../cards/types";
import type { PictureElementsCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import "../hui-sub-element-editor";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
@@ -32,6 +28,7 @@ import type { EditDetailElementEvent, SubElementEditorConfig } from "../types";
import { configElementStyle } from "./config-elements-style";
import "../hui-picture-elements-card-row-editor";
import type { LovelaceElementConfig } from "../../elements/types";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type { LocalizeFunc } from "../../../../common/translations/localize";
const genericElementConfigStruct = type({
@@ -69,44 +66,6 @@ export class HuiPictureElementsCardEditor
this._config = config;
}
private _onPreviewClick = (x: number, y: number): void => {
if (this._subElementEditorConfig?.type === "element") {
this._handlePositionClick(x, y);
}
};
private _handlePositionClick(x: number, y: number): void {
if (
!this._subElementEditorConfig?.elementConfig ||
this._subElementEditorConfig.type !== "element" ||
this._subElementEditorConfig.elementConfig.type === "conditional"
) {
return;
}
const elementConfig = this._subElementEditorConfig
.elementConfig as LovelaceElementConfig;
const currentPosition = (elementConfig.style as Record<string, string>)
?.position;
if (currentPosition && currentPosition !== "absolute") {
return;
}
const newElement = {
...elementConfig,
style: {
...((elementConfig.style as Record<string, string>) || {}),
left: `${Math.round(x)}%`,
top: `${Math.round(y)}%`,
},
};
const updateEvent = new CustomEvent("config-changed", {
detail: { config: newElement },
});
this._handleSubElementChanged(updateEvent);
}
private _schema = memoizeOne(
(localize: LocalizeFunc) =>
[
@@ -179,16 +138,6 @@ export class HuiPictureElementsCardEditor
if (this._subElementEditorConfig) {
return html`
${this._subElementEditorConfig.type === "element" &&
this._subElementEditorConfig.elementConfig?.type !== "conditional"
? html`
<ha-alert alert-type="info">
${this.hass.localize(
"ui.panel.lovelace.editor.card.picture-elements.position_hint"
)}
</ha-alert>
`
: nothing}
<hui-sub-element-editor
.hass=${this.hass}
.config=${this._subElementEditorConfig}
@@ -232,7 +181,6 @@ export class HuiPictureElementsCardEditor
return;
}
// no need to attach the preview click callback here, no element is being edited
fireEvent(this, "config-changed", { config: ev.detail.value });
}
@@ -243,8 +191,7 @@ export class HuiPictureElementsCardEditor
const config = {
...this._config,
elements: ev.detail.elements as LovelaceElementConfig[],
[PREVIEW_CLICK_CALLBACK]: this._onPreviewClick,
} as PictureElementsCardConfig;
} as LovelaceCardConfig;
fireEvent(this, "config-changed", { config });
@@ -285,12 +232,7 @@ export class HuiPictureElementsCardEditor
elementConfig: value,
};
fireEvent(this, "config-changed", {
config: {
...this._config,
[PREVIEW_CLICK_CALLBACK]: this._onPreviewClick,
},
});
fireEvent(this, "config-changed", { config: this._config });
}
private _editDetailElement(ev: HASSDomEvent<EditDetailElementEvent>): void {

View File

@@ -10,9 +10,7 @@ export const getElementStubConfig = async (
): Promise<LovelaceElementConfig> => {
let elementConfig: LovelaceElementConfig = { type };
if (type === "conditional") {
elementConfig = { type, conditions: [], elements: [] };
} else {
if (type !== "conditional") {
elementConfig.style = { left: "50%", top: "50%" };
}

View File

@@ -89,11 +89,7 @@ export abstract class HuiElementEditor<
}
public set value(config: T | undefined) {
// Compare symbols to detect callback changes (e.g., preview click handlers)
if (
this._config &&
deepEqual(config, this._config, { compareSymbols: true })
) {
if (this._config && deepEqual(config, this._config)) {
return;
}
this._config = config;

View File

@@ -73,12 +73,6 @@ export class HuiViewEditor extends LitElement {
icon: {},
},
},
{
name: "show_icon_and_title",
selector: {
boolean: {},
},
},
{ name: "path", selector: { text: {} } },
{ name: "theme", selector: { theme: {} } },
{
@@ -213,7 +207,6 @@ export class HuiViewEditor extends LitElement {
case "path":
return this.hass!.localize("ui.panel.lovelace.editor.card.generic.url");
case "type":
case "show_icon_and_title":
case "subview":
case "max_columns":
case "dense_section_placement":
@@ -234,7 +227,6 @@ export class HuiViewEditor extends LitElement {
) => {
switch (schema.name) {
case "path":
case "show_icon_and_title":
case "subview":
case "dense_section_placement":
case "top_margin":

View File

@@ -112,20 +112,7 @@ class HuiSelectEntityRow extends LitElement implements LovelaceRow {
forwardHaptic(this, "light");
setSelectOption(this.hass!, stateObj.entity_id, option)
.catch((_err) => {
// silently swallow exception
})
.finally(() =>
setTimeout(() => {
const newStateObj = this.hass!.states[this._config!.entity];
if (newStateObj === stateObj) {
const select = this.shadowRoot?.querySelector("ha-select");
const index = select?.options.indexOf(stateObj.state) ?? -1;
select?.select(index);
}
}, 2000)
);
setSelectOption(this.hass!, stateObj.entity_id, option);
}
}

View File

@@ -496,10 +496,6 @@ class HUIRoot extends LitElement {
const tabs = html`<ha-tab-group @wa-tab-show=${this._handleViewSelected}>
${views.map((view, index) => {
const icon_and_title =
view.show_icon_and_title && view.icon && view.title;
const icon_only = view.icon && !icon_and_title;
const title_only = !icon_only && !icon_and_title;
const hidden =
!this._editMode && (view.subview || _isTabHiddenForUser(view));
return html`
@@ -510,8 +506,7 @@ class HUIRoot extends LitElement {
.disabled=${hidden}
aria-label=${ifDefined(view.title)}
class=${classMap({
"icon-only": Boolean(icon_only),
"icon-and-title": Boolean(icon_and_title),
icon: Boolean(view.icon),
"hide-tab": Boolean(hidden),
})}
>
@@ -528,20 +523,18 @@ class HUIRoot extends LitElement {
></ha-icon-button-arrow-prev>
`
: nothing}
${icon_only || icon_and_title
? html`<ha-icon
class=${classMap({
"child-view-icon": Boolean(view.subview),
})}
title=${ifDefined(view.title)}
.icon=${view.icon}
></ha-icon>`
: nothing}
${icon_and_title ? view.title : nothing}
${title_only
? view.title ||
this.hass.localize("ui.panel.lovelace.views.unnamed_view")
: nothing}
${view.icon
? html`
<ha-icon
class=${classMap({
"child-view-icon": Boolean(view.subview),
})}
title=${ifDefined(view.title)}
.icon=${view.icon}
></ha-icon>
`
: view.title ||
this.hass.localize("ui.panel.lovelace.views.unnamed_view")}
${this._editMode
? html`
<ha-icon-button
@@ -1496,27 +1489,24 @@ class HUIRoot extends LitElement {
ha-tab-group-tab {
--ha-tab-group-tab-height: var(--header-height, 56px);
}
.tab-bar ha-tab-group-tab {
--ha-tab-group-tab-height: var(--tab-bar-height, 56px);
}
ha-tab-group-tab[aria-selected="true"] .edit-icon {
display: inline-flex;
}
ha-tab-group-tab::part(base) {
padding-inline-start: var(--ha-tab-padding-start, var(--wa-space-l));
padding-inline-end: var(--ha-tab-padding-end, var(--wa-space-l));
}
ha-tab-group-tab::part(base) {
padding-top: calc((var(--ha-tab-group-tab-height) - 20px) / 2);
}
ha-tab-group-tab.icon-only::part(base),
ha-tab-group-tab.icon-and-title::part(base) {
ha-tab-group-tab.icon::part(base) {
padding-top: calc((var(--ha-tab-group-tab-height) - 20px) / 2 - 2px);
padding-bottom: calc(
(var(--ha-tab-group-tab-height) - 20px) / 2 - 4px
);
}
ha-tab-group-tab.icon-and-title ha-icon {
margin-inline-end: var(--ha-space-2);
.tab-bar ha-tab-group-tab {
--ha-tab-group-tab-height: var(--tab-bar-height, 56px);
}
.edit-mode ha-tab-group-tab[aria-selected="true"]::part(base) {
padding: 0;

View File

@@ -11,10 +11,7 @@ import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
import type {
EmptyStateCardConfig,
HeadingCardConfig,
} from "../../cards/types";
import type { HeadingCardConfig } from "../../cards/types";
import { computeAreaTileCardConfig } from "../areas/helpers/areas-strategy-helper";
import {
getSummaryLabel,
@@ -357,26 +354,6 @@ export class HomeAreaViewStrategy extends ReactiveElement {
});
}
// No sections, show empty state
if (sections.length === 0) {
return {
type: "panel",
cards: [
{
type: "empty-state",
icon: "mdi:sofa-outline",
content_only: true,
title: hass.localize(
"ui.panel.lovelace.strategy.areas.empty_state_title"
),
content: hass.localize(
"ui.panel.lovelace.strategy.areas.empty_state_content"
),
} as EmptyStateCardConfig,
],
};
}
// Allow between 2 and 3 columns (the max should be set to define the width of the header)
const maxColumns = clamp(sections.length, 2, 3);

View File

@@ -6,7 +6,6 @@ import type { AreasDisplayValue } from "../../../../components/ha-areas-display-
import { getEnergyPreferences } from "../../../../data/energy";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
import type { EmptyStateCardConfig } from "../../cards/types";
import { generateDefaultViewConfig } from "../../common/generate-lovelace-config";
export interface OriginalStatesViewStrategyConfig {
@@ -65,33 +64,9 @@ export class OriginalStatesViewStrategy extends ReactiveElement {
// User has no entities
if (view.cards!.length === 0) {
return {
type: "panel",
cards: [
{
type: "empty-state",
icon: "mdi:home-assistant",
content_only: true,
title: hass.localize(
"ui.panel.lovelace.strategy.original-states.empty_state_title"
),
content: hass.localize(
"ui.panel.lovelace.strategy.original-states.empty_state_content"
),
...(hass.user?.is_admin
? {
action_button_text: hass.localize(
"ui.panel.lovelace.strategy.original-states.empty_state_action"
),
tap_action: {
action: "navigate",
navigation_path: "/config/integrations/dashboard",
},
}
: {}),
} as EmptyStateCardConfig,
],
};
view.cards!.push({
type: "empty-state",
});
}
return view;

View File

@@ -15,11 +15,7 @@ import { tags } from "@lezer/highlight";
export { autocompletion } from "@codemirror/autocomplete";
export { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
export { highlightingFor, foldGutter } from "@codemirror/language";
export {
highlightSelectionMatches,
search,
searchKeymap,
} from "@codemirror/search";
export { highlightSelectionMatches, searchKeymap } from "@codemirror/search";
export { EditorState } from "@codemirror/state";
export {
crosshairCursor,

View File

@@ -50,9 +50,7 @@ export class HaStateControlHumidifierHumidity extends LitElement {
}
}
private get _step() {
return this.stateObj.attributes.target_humidity_step ?? 1;
}
private _step = 1;
private get _min() {
return this.stateObj.attributes.min_humidity ?? 0;

View File

@@ -137,9 +137,9 @@
},
"counter": {
"actions": {
"increment": "Increment",
"decrement": "Decrement",
"reset": "Reset"
"increment": "increment",
"decrement": "decrement",
"reset": "reset"
}
},
"cover": {
@@ -2172,8 +2172,7 @@
},
"other": {
"title": "Other",
"my_link": "get My Home Assistant link",
"show_shortcuts": "show keyboard shortcuts"
"my_link": "get My Home Assistant link"
}
}
},
@@ -2319,7 +2318,7 @@
},
"companion": {
"main": "Companion app",
"secondary": "Sensors, widgets, notifications and more"
"secondary": "Location and notifications"
},
"system": {
"main": "System",
@@ -2383,7 +2382,6 @@
"updates_refreshed": "State of {count} {count, plural,\n one {update}\n other {updates}\n} refreshed",
"checking_updates": "Checking for updates...",
"title": "{count} {count, plural,\n one {update}\n other {updates}\n}",
"title_not_installable": "{count} not installable {count, plural,\n one {update}\n other {updates}\n}",
"unable_to_fetch": "Unable to load updates",
"more_updates": "Show all updates",
"show": "show",
@@ -3341,8 +3339,7 @@
"type": "Type",
"editable": "Editable",
"category": "Category",
"area": "Area",
"voice_assistants": "Voice assistants"
"area": "Area"
},
"create_helper": "Create helper",
"no_helpers": "Looks like you don't have any helpers yet!",
@@ -3983,8 +3980,7 @@
"state": "State",
"category": "Category",
"area": "Area",
"icon": "Icon",
"voice_assistants": "Voice assistants"
"icon": "Icon"
},
"bulk_action": "Action",
"bulk_actions": {
@@ -5001,8 +4997,7 @@
"state": "State",
"category": "Category",
"area": "Area",
"icon": "Icon",
"voice_assistants": "Voice assistants"
"icon": "Icon"
},
"edit_category": "[%key:ui::panel::config::automation::picker::edit_category%]",
"assign_category": "[%key:ui::panel::config::automation::picker::assign_category%]",
@@ -5130,8 +5125,7 @@
"category": "Category",
"editable": "[%key:ui::panel::config::helpers::picker::headers::editable%]",
"area": "Area",
"icon": "Icon",
"voice_assistants": "Voice assistants"
"icon": "Icon"
},
"edit_category": "[%key:ui::panel::config::automation::picker::edit_category%]",
"assign_category": "[%key:ui::panel::config::automation::picker::assign_category%]",
@@ -5584,8 +5578,7 @@
"domain": "Domain",
"availability": "Availability",
"visibility": "Visibility",
"enabled": "Enabled",
"voice_assistants": "Voice assistants"
"enabled": "Enabled"
},
"selected": "{number} selected",
"enable_selected": {
@@ -6845,15 +6838,9 @@
},
"matter": {
"panel": {
"thread_panel": "Visit Thread panel",
"add_device": "Add device",
"status_title": "status",
"status_online": "online",
"status_offline": "offline",
"devices": "{count, plural,\n one {# device}\n other {# devices}\n}",
"developer_tools_title": "Developer tools",
"developer_tools_description": "Advanced commissioning options for development",
"developer_tools_info": "These options are intended for developers and advanced users. In most cases, use the \"Add device\" button in the bottom right corner instead.",
"thread_panel": "Visit Thread Panel",
"experimental_note": "Matter is still in the early phase of development, it is not meant to be used in production. This panel is for development only.",
"add_devices": "You can add Matter devices by commissioning them if they are not set up yet, or share them from another controller and enter the sharing code.",
"mobile_app_commisioning": "Commission device with mobile app",
"commission_device": "Commission device",
"add_shared_device": "Add shared device",
@@ -7233,14 +7220,10 @@
"lovelace": {
"strategy": {
"original-states": {
"helpers": "[%key:ui::panel::config::helpers::caption%]",
"empty_state_title": "Welcome Home",
"empty_state_content": "This page allows you to control your devices, however it looks like you have no devices set up yet.",
"empty_state_action": "Go to the integrations page"
"helpers": "[%key:ui::panel::config::helpers::caption%]"
},
"areas": {
"empty_state_title": "No devices",
"empty_state_content": "There are no devices assigned to this area yet. Assign devices to this area to see them here.",
"no_entities": "No entities in this area.",
"sensors": "Sensors",
"sensors_description": "To display temperature and humidity sensors in the overview and in the area view, add a sensor to that area and {edit_the_area} to configure related sensors.",
"edit_the_area": "edit the area",
@@ -7312,6 +7295,11 @@
"no_url": "No URL to open specified",
"no_action": "No action to run specified"
},
"empty_state": {
"title": "Welcome Home",
"no_devices": "This page allows you to control your devices, however it looks like you have no devices set up yet. Head to the integrations page to get started.",
"go_to_integrations_page": "Go to the integrations page."
},
"entities": {
"never_triggered": "Never triggered"
},
@@ -7360,7 +7348,7 @@
"energy_usage_graph": {
"total_consumed": "Total consumed {num} kWh",
"total_returned": "Total returned {num} kWh",
"total_usage": "+{num} kWh",
"total_usage": "{num} kWh used",
"combined_from_grid": "Combined from grid",
"consumed_solar": "Consumed solar",
"consumed_battery": "Consumed battery"
@@ -7609,7 +7597,6 @@
"sidebar": "Sidebar",
"panel": "Panel (single card)"
},
"show_icon_and_title": "Show icon and title",
"subview": "Subview",
"max_columns": "Max number of sections wide",
"section_specifics": "Sections view specific settings",
@@ -7617,7 +7604,6 @@
"dense_section_placement_helper": "Will try to fill gaps with sections that fit the gap. This may make section placement less predictable.",
"top_margin": "Add additional space above",
"top_margin_helper": "Helps reveal more of the background",
"show_icon_and_title_helper": "Show both icon and text title.",
"subview_helper": "Subviews don't appear in tabs and have a back button.",
"path_helper": "This value will become part of the URL path to open this view.",
"edit_ui": "Edit in visual editor",
@@ -7983,17 +7969,6 @@
"name": "Entity",
"description": "The Entity card gives you a quick overview of your entity's state."
},
"empty_state": {
"name": "Empty state",
"description": "The Empty state card displays a centered message with an optional icon and action button.",
"style": "Style",
"style_options": {
"card": "Card",
"content-only": "Content only"
},
"content": "Content",
"action_text": "Action button text"
},
"button": {
"name": "Button",
"description": "The Button card allows you to add buttons to perform tasks.",
@@ -8326,7 +8301,6 @@
"dark_mode_image": "Dark mode image path",
"state_filter": "State filter",
"dark_mode_filter": "Dark mode state filter",
"position_hint": "Click on the image preview to position this element",
"element_types": {
"state-badge": "State badge",
"state-icon": "State icon",
@@ -9261,9 +9235,6 @@
"active_listeners": "Active listeners",
"count_listeners": "({count} {count, plural,\n one {listener}\n other {listeners}\n})",
"listen_to_events": "Listen to events",
"filter_events": "Filter events",
"filter_helper": "Only capture events containing the filter string.",
"filter_ignored": "Ignored {count} events.",
"listening_to": "Listening to",
"subscribe_to": "Event to subscribe to",
"start_listening": "Start listening",

690
yarn.lock

File diff suppressed because it is too large Load Diff