mirror of
https://github.com/home-assistant/frontend.git
synced 2026-01-09 16:57:29 +00:00
Compare commits
11 Commits
generic-pi
...
quick-bar-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
126e0b76c0 | ||
|
|
167dffda8f | ||
|
|
bc057b5187 | ||
|
|
dc0d8932ad | ||
|
|
84472483ab | ||
|
|
8d9ab20c73 | ||
|
|
8c0d8d5b3b | ||
|
|
92156663ab | ||
|
|
7d67ea5c61 | ||
|
|
d6bcb143cc | ||
|
|
41054400bf |
4
.github/copilot-instructions.md
vendored
4
.github/copilot-instructions.md
vendored
@@ -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
|
||||
|
||||
2
.github/workflows/relative-ci.yaml
vendored
2
.github/workflows/relative-ci.yaml
vendored
@@ -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 }}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AreaRegistryEntry } from "../../../src/data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../src/data/area/area_registry";
|
||||
import type { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
|
||||
|
||||
export const mockAreaRegistry = (
|
||||
|
||||
@@ -187,11 +187,5 @@ export default tseslint.config(
|
||||
],
|
||||
"no-use-before-define": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/util/recorder-worklet.js"],
|
||||
languageOptions: {
|
||||
globals: globals.audioWorklet,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervis
|
||||
import { computeInitialHaFormData } from "../../../../src/components/ha-form/compute-initial-ha-form-data";
|
||||
import "../../../../src/components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../../../../src/components/ha-form/types";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
|
||||
import { getEntity } from "../../../../src/fake_data/entity";
|
||||
import { provideHass } from "../../../../src/fake_data/provide_hass";
|
||||
|
||||
@@ -11,7 +11,7 @@ import { mockLabelRegistry } from "../../../../demo/src/stubs/label_registry";
|
||||
import "../../../../src/components/ha-formfield";
|
||||
import "../../../../src/components/ha-selector/ha-selector";
|
||||
import "../../../../src/components/ha-settings-row";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
|
||||
import type { BlueprintInput } from "../../../../src/data/blueprint";
|
||||
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
|
||||
import type { FloorRegistryEntry } from "../../../../src/data/floor_registry";
|
||||
|
||||
42
package.json
42
package.json
@@ -34,18 +34,18 @@
|
||||
"@codemirror/legacy-modes": "6.5.2",
|
||||
"@codemirror/search": "6.5.11",
|
||||
"@codemirror/state": "6.5.3",
|
||||
"@codemirror/view": "6.39.8",
|
||||
"@codemirror/view": "6.39.7",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.1.1",
|
||||
"@formatjs/intl-displaynames": "7.1.1",
|
||||
"@formatjs/intl-durationformat": "0.9.1",
|
||||
"@formatjs/intl-getcanonicallocales": "3.1.1",
|
||||
"@formatjs/intl-listformat": "8.1.1",
|
||||
"@formatjs/intl-locale": "5.1.1",
|
||||
"@formatjs/intl-numberformat": "9.1.1",
|
||||
"@formatjs/intl-pluralrules": "6.1.1",
|
||||
"@formatjs/intl-relativetimeformat": "12.1.1",
|
||||
"@formatjs/intl-datetimeformat": "7.0.6",
|
||||
"@formatjs/intl-displaynames": "7.0.6",
|
||||
"@formatjs/intl-durationformat": "0.8.6",
|
||||
"@formatjs/intl-getcanonicallocales": "3.0.4",
|
||||
"@formatjs/intl-listformat": "8.0.6",
|
||||
"@formatjs/intl-locale": "5.0.6",
|
||||
"@formatjs/intl-numberformat": "9.0.7",
|
||||
"@formatjs/intl-pluralrules": "6.0.6",
|
||||
"@formatjs/intl-relativetimeformat": "12.0.7",
|
||||
"@fullcalendar/core": "6.1.20",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"@fullcalendar/interaction": "6.1.20",
|
||||
@@ -85,7 +85,7 @@
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@swc/helpers": "0.5.18",
|
||||
"@swc/helpers": "0.5.17",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "3.9.1",
|
||||
"@tsparticles/preset-links": "3.2.0",
|
||||
@@ -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.8",
|
||||
"intl-messageformat": "11.0.6",
|
||||
"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.1",
|
||||
"lit-html": "3.3.1",
|
||||
"lit": "3.3.2",
|
||||
"lit-html": "3.3.2",
|
||||
"luxon": "3.7.2",
|
||||
"marked": "17.0.1",
|
||||
"memoize-one": "6.0.0",
|
||||
@@ -156,8 +156,8 @@
|
||||
"@octokit/plugin-retry": "8.0.3",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.4.0",
|
||||
"@rspack/core": "1.7.0",
|
||||
"@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,7 +215,7 @@
|
||||
"terser-webpack-plugin": "5.3.16",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.51.0",
|
||||
"typescript-eslint": "8.50.1",
|
||||
"vite-tsconfig-paths": "6.0.3",
|
||||
"vitest": "4.0.16",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
@@ -224,12 +224,12 @@
|
||||
},
|
||||
"resolutions": {
|
||||
"@material/mwc-button@^0.25.3": "^0.27.0",
|
||||
"lit": "3.3.1",
|
||||
"lit-html": "3.3.1",
|
||||
"lit": "3.3.2",
|
||||
"lit-html": "3.3.2",
|
||||
"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"
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20251229.0"
|
||||
version = "20251203.0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AreaRegistryEntry } from "../../data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../data/area/area_registry";
|
||||
import type { FloorRegistryEntry } from "../../data/floor_registry";
|
||||
|
||||
export interface AreasFloorHierarchy {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AreaRegistryEntry } from "../../data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../data/area/area_registry";
|
||||
|
||||
export const computeAreaName = (area: AreaRegistryEntry): string | undefined =>
|
||||
area.name?.trim();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
|
||||
@@ -18,7 +18,10 @@ import "../ha-combo-box-item";
|
||||
import "../ha-generic-picker";
|
||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||
import "../ha-input-helper-text";
|
||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
||||
import {
|
||||
NO_ITEMS_AVAILABLE_ID,
|
||||
type PickerComboBoxItem,
|
||||
} from "../ha-picker-combo-box";
|
||||
import "../ha-sortable";
|
||||
|
||||
const rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
|
||||
@@ -181,17 +184,18 @@ export class HaEntityNamePicker extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required && !value.length}
|
||||
.getItems=${this._getFilteredItems}
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
.rowRenderer=${rowRenderer}
|
||||
.searchFn=${this._searchFn}
|
||||
.notFoundLabel=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.no_match"
|
||||
)}
|
||||
.value=${this._getPickerValue()}
|
||||
allow-custom-value
|
||||
.customValueLabel=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.custom_name"
|
||||
)}
|
||||
@value-changed=${this._pickerValueChanged}
|
||||
.searchFn=${this._searchFn}
|
||||
.searchLabel=${this.hass.localize(
|
||||
"ui.components.entity.entity-name-picker.search"
|
||||
)}
|
||||
>
|
||||
<div slot="field" class="container">
|
||||
<ha-sortable
|
||||
@@ -312,7 +316,10 @@ export class HaEntityNamePicker extends LitElement {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _getFilteredItems = (): PickerComboBoxItem[] => {
|
||||
private _getFilteredItems = (
|
||||
searchString?: string,
|
||||
_section?: string
|
||||
): PickerComboBoxItem[] => {
|
||||
const items = this._getItems(this.entityId);
|
||||
const currentItem =
|
||||
this._editIndex != null ? this._items[this._editIndex] : undefined;
|
||||
@@ -329,27 +336,49 @@ export class HaEntityNamePicker extends LitElement {
|
||||
);
|
||||
|
||||
// When editing an existing text item, include it in the base items
|
||||
if (currentItem?.type === "text" && currentItem.text) {
|
||||
if (currentItem?.type === "text" && currentItem.text && !searchString) {
|
||||
filteredItems.push(this._customNameOption(currentItem.text));
|
||||
}
|
||||
|
||||
return filteredItems;
|
||||
};
|
||||
|
||||
private _searchFn = (
|
||||
searchString: string,
|
||||
filteredItems: PickerComboBoxItem[]
|
||||
private _getAdditionalItems = (
|
||||
searchString?: string
|
||||
): PickerComboBoxItem[] => {
|
||||
if (!searchString) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const currentItem =
|
||||
this._editIndex != null ? this._items[this._editIndex] : undefined;
|
||||
const currentId =
|
||||
currentItem?.type === "text" && currentItem.text
|
||||
? this._customNameOption(currentItem.text).id
|
||||
: undefined;
|
||||
|
||||
// Remove custom name option if search string is present to avoid duplicates
|
||||
if (searchString && currentId) {
|
||||
return filteredItems.filter((item) => item.id !== currentId);
|
||||
// Don't add if it's the same as the current item being edited
|
||||
if (
|
||||
currentItem?.type === "text" &&
|
||||
currentItem.text &&
|
||||
currentItem.text === searchString
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Always return custom name option when there's a search string
|
||||
// This prevents "No matching items found" from showing
|
||||
return [this._customNameOption(searchString)];
|
||||
};
|
||||
|
||||
private _searchFn = (
|
||||
search: string,
|
||||
filteredItems: PickerComboBoxItem[],
|
||||
_allItems: PickerComboBoxItem[]
|
||||
): PickerComboBoxItem[] => {
|
||||
// Remove NO_ITEMS_AVAILABLE_ID if we have additional items (custom name option)
|
||||
// This prevents "No matching items found" from showing when custom values are allowed
|
||||
const hasAdditionalItems = this._getAdditionalItems(search).length > 0;
|
||||
if (hasAdditionalItems) {
|
||||
return filteredItems.filter(
|
||||
(item) => typeof item !== "string" || item !== NO_ITEMS_AVAILABLE_ID
|
||||
);
|
||||
}
|
||||
return filteredItems;
|
||||
};
|
||||
|
||||
@@ -19,10 +19,7 @@ import "../ha-combo-box-item";
|
||||
import "../ha-generic-picker";
|
||||
import type { HaGenericPicker } from "../ha-generic-picker";
|
||||
import "../ha-input-helper-text";
|
||||
import {
|
||||
NO_ITEMS_AVAILABLE_ID,
|
||||
type PickerComboBoxItem,
|
||||
} from "../ha-picker-combo-box";
|
||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
||||
import "../ha-sortable";
|
||||
|
||||
const HIDDEN_ATTRIBUTES = [
|
||||
@@ -202,7 +199,11 @@ export class HaStateContentPicker extends LitElement {
|
||||
.value=${this._getPickerValue()}
|
||||
.getItems=${this._getFilteredItems}
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
.searchFn=${this._searchFn}
|
||||
.notFoundLabel=${this.hass.localize("ui.components.combo-box.no_match")}
|
||||
allow-custom-value
|
||||
.customValueLabel=${this.hass.localize(
|
||||
"ui.components.entity.entity-state-content-picker.custom_state"
|
||||
)}
|
||||
@value-changed=${this._pickerValueChanged}
|
||||
>
|
||||
<div slot="field" class="container">
|
||||
@@ -327,7 +328,7 @@ export class HaStateContentPicker extends LitElement {
|
||||
(text: string): PickerComboBoxItem => ({
|
||||
id: text,
|
||||
primary: this.hass.localize(
|
||||
"ui.components.entity.entity-state-content-picker.custom_attribute"
|
||||
"ui.components.entity.entity-state-content-picker.custom_state"
|
||||
),
|
||||
secondary: `"${text}"`,
|
||||
search_labels: {
|
||||
@@ -339,7 +340,10 @@ export class HaStateContentPicker extends LitElement {
|
||||
})
|
||||
);
|
||||
|
||||
private _getFilteredItems = (): PickerComboBoxItem[] => {
|
||||
private _getFilteredItems = (
|
||||
searchString?: string,
|
||||
_section?: string
|
||||
): PickerComboBoxItem[] => {
|
||||
const stateObj = this.entityId
|
||||
? this.hass.states[this.entityId]
|
||||
: undefined;
|
||||
@@ -354,7 +358,11 @@ export class HaStateContentPicker extends LitElement {
|
||||
);
|
||||
|
||||
// When editing an existing custom value, include it in the base items
|
||||
if (currentValue && !items.find((item) => item.id === currentValue)) {
|
||||
if (
|
||||
currentValue &&
|
||||
!items.find((item) => item.id === currentValue) &&
|
||||
!searchString
|
||||
) {
|
||||
filteredItems.push(this._customValueOption(currentValue));
|
||||
}
|
||||
|
||||
@@ -364,34 +372,33 @@ export class HaStateContentPicker extends LitElement {
|
||||
private _getAdditionalItems = (
|
||||
searchString?: string
|
||||
): PickerComboBoxItem[] => {
|
||||
if (!searchString) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const currentValue =
|
||||
this._editIndex != null ? this._value[this._editIndex] : undefined;
|
||||
|
||||
// Don't add if it's the same as the current item being edited
|
||||
if (currentValue && currentValue === searchString) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Check if the search string matches an existing item
|
||||
const stateObj = this.entityId
|
||||
? this.hass.states[this.entityId]
|
||||
: undefined;
|
||||
const items = this._getItems(this.entityId, stateObj, this.allowName);
|
||||
|
||||
// If the search string does not match with the id of any of the items,
|
||||
// offer to add it as a custom attribute
|
||||
const existingItem = items.find((item) => item.id === searchString);
|
||||
if (searchString && !existingItem) {
|
||||
|
||||
// Only return custom value option if it doesn't match an existing item
|
||||
if (!existingItem) {
|
||||
return [this._customValueOption(searchString)];
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
private _searchFn = (
|
||||
search: string,
|
||||
filteredItems: PickerComboBoxItem[],
|
||||
_allItems: PickerComboBoxItem[]
|
||||
): PickerComboBoxItem[] => {
|
||||
if (!search) {
|
||||
return filteredItems;
|
||||
}
|
||||
|
||||
// Always exclude NO_ITEMS_AVAILABLE_ID (since custom values are allowed) and currentValue (the custom value being edited)
|
||||
return filteredItems.filter((item) => item.id !== NO_ITEMS_AVAILABLE_ID);
|
||||
};
|
||||
|
||||
private async _moveItem(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
|
||||
@@ -7,7 +7,6 @@ import { getStates } from "../../common/entity/get_states";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
|
||||
import "../ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
||||
import type { PickerValueRenderer } from "../ha-picker-field";
|
||||
|
||||
@customElement("ha-entity-state-picker")
|
||||
export class HaEntityStatePicker extends LitElement {
|
||||
@@ -109,12 +108,6 @@ export class HaEntityStatePicker extends LitElement {
|
||||
this.extraOptions
|
||||
);
|
||||
|
||||
private _valueRenderer: PickerValueRenderer = (value: string) => {
|
||||
const items = this._getFilteredItems();
|
||||
const item = items.find((option) => option.id === value);
|
||||
return html`<span slot="headline">${item?.primary ?? value}</span>`;
|
||||
};
|
||||
|
||||
protected render() {
|
||||
if (!this.hass) {
|
||||
return nothing;
|
||||
@@ -132,7 +125,6 @@ export class HaEntityStatePicker extends LitElement {
|
||||
.helper=${this.helper}
|
||||
.value=${this.value}
|
||||
.getItems=${this._getFilteredItems}
|
||||
.valueRenderer=${this._valueRenderer}
|
||||
.notFoundLabel=${this.hass.localize("ui.components.combo-box.no_match")}
|
||||
.customValueLabel=${this.hass.localize(
|
||||
"ui.components.entity.entity-state-picker.add_custom_state"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { listenMediaQuery } from "../common/dom/media_query";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-bottom-sheet";
|
||||
import "./ha-dialog-header";
|
||||
import "./ha-icon-button";
|
||||
@@ -118,27 +118,29 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
if (this._mode === "bottom-sheet") {
|
||||
return html`
|
||||
<ha-bottom-sheet .open=${this.open} flexcontent>
|
||||
<ha-dialog-header
|
||||
slot="header"
|
||||
.subtitlePosition=${this.headerSubtitlePosition}
|
||||
>
|
||||
<slot name="headerNavigationIcon" slot="navigationIcon">
|
||||
<ha-icon-button
|
||||
data-drawer="close"
|
||||
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
${this.headerTitle !== undefined
|
||||
? html`<span slot="title" class="title" id="ha-wa-dialog-title">
|
||||
${this.headerTitle}
|
||||
</span>`
|
||||
: html`<slot name="headerTitle" slot="title"></slot>`}
|
||||
${this.headerSubtitle !== undefined
|
||||
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
|
||||
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
|
||||
<slot name="headerActionItems" slot="actionItems"></slot>
|
||||
</ha-dialog-header>
|
||||
<slot name="header" slot="header">
|
||||
<ha-dialog-header
|
||||
slot="header"
|
||||
.subtitlePosition=${this.headerSubtitlePosition}
|
||||
>
|
||||
<slot name="headerNavigationIcon" slot="navigationIcon">
|
||||
<ha-icon-button
|
||||
data-drawer="close"
|
||||
.label=${this.hass?.localize("ui.common.close") ?? "Close"}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
${this.headerTitle !== undefined
|
||||
? html`<span slot="title" class="title" id="ha-wa-dialog-title">
|
||||
${this.headerTitle}
|
||||
</span>`
|
||||
: html`<slot name="headerTitle" slot="title"></slot>`}
|
||||
${this.headerSubtitle !== undefined
|
||||
? html`<span slot="subtitle">${this.headerSubtitle}</span>`
|
||||
: html`<slot name="headerSubtitle" slot="subtitle"></slot>`}
|
||||
<slot name="headerActionItems" slot="actionItems"></slot>
|
||||
</ha-dialog-header>
|
||||
</slot>
|
||||
<slot></slot>
|
||||
<slot name="footer" slot="footer"></slot>
|
||||
</ha-bottom-sheet>
|
||||
@@ -157,16 +159,18 @@ export class HaAdaptiveDialog extends LitElement {
|
||||
.headerSubtitlePosition=${this.headerSubtitlePosition}
|
||||
flexcontent
|
||||
>
|
||||
<slot name="headerNavigationIcon" slot="headerNavigationIcon">
|
||||
<ha-icon-button
|
||||
data-dialog="close"
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
<slot name="header" slot="header">
|
||||
<slot name="headerNavigationIcon" slot="headerNavigationIcon">
|
||||
<ha-icon-button
|
||||
data-dialog="close"
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
.path=${mdiClose}
|
||||
></ha-icon-button>
|
||||
</slot>
|
||||
<slot name="headerTitle" slot="headerTitle"></slot>
|
||||
<slot name="headerSubtitle" slot="headerSubtitle"></slot>
|
||||
<slot name="headerActionItems" slot="headerActionItems"></slot>
|
||||
</slot>
|
||||
<slot name="headerTitle" slot="headerTitle"></slot>
|
||||
<slot name="headerSubtitle" slot="headerSubtitle"></slot>
|
||||
<slot name="headerActionItems" slot="headerActionItems"></slot>
|
||||
<slot></slot>
|
||||
<slot name="footer" slot="footer"></slot>
|
||||
</ha-wa-dialog>
|
||||
|
||||
@@ -6,16 +6,10 @@ import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeAreaName } from "../common/entity/compute_area_name";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||
import { getAreaContext } from "../common/entity/context/get_area_context";
|
||||
import { createAreaRegistryEntry } from "../data/area_registry";
|
||||
import type {
|
||||
DeviceEntityDisplayLookup,
|
||||
DeviceRegistryEntry,
|
||||
} from "../data/device/device_registry";
|
||||
import { getDeviceEntityDisplayLookup } from "../data/device/device_registry";
|
||||
import type { EntityRegistryDisplayEntry } from "../data/entity/entity_registry";
|
||||
import { areaComboBoxKeys, getAreas } from "../data/area/area_picker";
|
||||
import { createAreaRegistryEntry } from "../data/area/area_registry";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { showAreaRegistryDetailDialog } from "../panels/config/areas/show-dialog-area-registry-detail";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
@@ -30,12 +24,6 @@ import "./ha-svg-icon";
|
||||
|
||||
const ADD_NEW_ID = "___ADD_NEW___";
|
||||
|
||||
const SEARCH_KEYS = [
|
||||
{ name: "search_labels.areaName", weight: 10 },
|
||||
{ name: "search_labels.aliases", weight: 8 },
|
||||
{ name: "search_labels.floorName", weight: 6 },
|
||||
{ name: "search_labels.id", weight: 3 },
|
||||
];
|
||||
@customElement("ha-area-picker")
|
||||
export class HaAreaPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -102,6 +90,8 @@ export class HaAreaPicker extends LitElement {
|
||||
await this._picker?.open();
|
||||
}
|
||||
|
||||
private _getAreasMemoized = memoizeOne(getAreas);
|
||||
|
||||
// Recompute value renderer when the areas change
|
||||
private _computeValueRenderer = memoizeOne(
|
||||
(_haAreas: HomeAssistant["areas"]): PickerValueRenderer =>
|
||||
@@ -137,183 +127,13 @@ export class HaAreaPicker extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _getAreas = memoizeOne(
|
||||
(
|
||||
haAreas: HomeAssistant["areas"],
|
||||
haDevices: HomeAssistant["devices"],
|
||||
haEntities: HomeAssistant["entities"],
|
||||
includeDomains: this["includeDomains"],
|
||||
excludeDomains: this["excludeDomains"],
|
||||
includeDeviceClasses: this["includeDeviceClasses"],
|
||||
deviceFilter: this["deviceFilter"],
|
||||
entityFilter: this["entityFilter"],
|
||||
excludeAreas: this["excludeAreas"]
|
||||
): PickerComboBoxItem[] => {
|
||||
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
|
||||
|
||||
const areas = Object.values(haAreas);
|
||||
const devices = Object.values(haDevices);
|
||||
const entities = Object.values(haEntities);
|
||||
|
||||
if (
|
||||
includeDomains ||
|
||||
excludeDomains ||
|
||||
includeDeviceClasses ||
|
||||
deviceFilter ||
|
||||
entityFilter
|
||||
) {
|
||||
deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
|
||||
inputDevices = devices;
|
||||
inputEntities = entities.filter((entity) => entity.area_id);
|
||||
|
||||
if (includeDomains) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) =>
|
||||
includeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) =>
|
||||
includeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeDomains) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return true;
|
||||
}
|
||||
return entities.every(
|
||||
(entity) =>
|
||||
!excludeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
inputEntities = inputEntities!.filter(
|
||||
(entity) =>
|
||||
!excludeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
}
|
||||
|
||||
if (includeDeviceClasses) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (deviceFilter) {
|
||||
inputDevices = inputDevices!.filter((device) =>
|
||||
deviceFilter!(device)
|
||||
);
|
||||
}
|
||||
|
||||
if (entityFilter) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return entityFilter(stateObj);
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = this.hass.states[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return entityFilter!(stateObj);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let outputAreas = areas;
|
||||
|
||||
let areaIds: string[] | undefined;
|
||||
|
||||
if (inputDevices) {
|
||||
areaIds = inputDevices
|
||||
.filter((device) => device.area_id)
|
||||
.map((device) => device.area_id!);
|
||||
}
|
||||
|
||||
if (inputEntities) {
|
||||
areaIds = (areaIds ?? []).concat(
|
||||
inputEntities
|
||||
.filter((entity) => entity.area_id)
|
||||
.map((entity) => entity.area_id!)
|
||||
);
|
||||
}
|
||||
|
||||
if (areaIds) {
|
||||
outputAreas = outputAreas.filter((area) =>
|
||||
areaIds!.includes(area.area_id)
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeAreas) {
|
||||
outputAreas = outputAreas.filter(
|
||||
(area) => !excludeAreas!.includes(area.area_id)
|
||||
);
|
||||
}
|
||||
|
||||
const items = outputAreas.map<PickerComboBoxItem>((area) => {
|
||||
const { floor } = getAreaContext(area, this.hass.floors);
|
||||
const floorName = floor ? computeFloorName(floor) : undefined;
|
||||
const areaName = computeAreaName(area);
|
||||
return {
|
||||
id: area.area_id,
|
||||
primary: areaName || area.area_id,
|
||||
secondary: floorName,
|
||||
icon: area.icon || undefined,
|
||||
icon_path: area.icon ? undefined : mdiTextureBox,
|
||||
search_labels: {
|
||||
areaName: areaName || null,
|
||||
floorName: floorName || null,
|
||||
id: area.area_id,
|
||||
aliases: area.aliases.join(" "),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
);
|
||||
|
||||
private _getItems = () =>
|
||||
this._getAreas(
|
||||
this._getAreasMemoized(
|
||||
this.hass.areas,
|
||||
this.hass.floors,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
this.hass.states,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
this.includeDeviceClasses,
|
||||
@@ -394,7 +214,7 @@ export class HaAreaPicker extends LitElement {
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
.valueRenderer=${valueRenderer}
|
||||
.addButtonLabel=${this.addButtonLabel}
|
||||
.searchKeys=${SEARCH_KEYS}
|
||||
.searchKeys=${areaComboBoxKeys}
|
||||
.unknownItemText=${this.hass.localize(
|
||||
"ui.components.area-picker.unknown"
|
||||
)}
|
||||
|
||||
@@ -8,7 +8,7 @@ import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeFloorName } from "../common/entity/compute_floor_name";
|
||||
import { updateAreaRegistryEntry } from "../data/area_registry";
|
||||
import { updateAreaRegistryEntry } from "../data/area/area_registry";
|
||||
import type {
|
||||
DeviceEntityDisplayLookup,
|
||||
DeviceRegistryEntry,
|
||||
|
||||
@@ -124,6 +124,9 @@ export class HaIconPicker extends LitElement {
|
||||
.label=${this.label}
|
||||
.value=${this._value}
|
||||
.searchFn=${this._filterIcons}
|
||||
.notFoundLabel=${this.hass?.localize(
|
||||
"ui.components.icon-picker.no_match"
|
||||
)}
|
||||
popover-placement="bottom-start"
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
@@ -170,6 +173,20 @@ export class HaIconPicker extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
// Allow preview for custom icon not in list
|
||||
if (rankedItems.length === 0) {
|
||||
rankedItems.push({
|
||||
item: {
|
||||
id: filter,
|
||||
primary: filter,
|
||||
icon: filter,
|
||||
search_labels: { keyword: filter },
|
||||
sorting_label: filter,
|
||||
},
|
||||
rank: 0,
|
||||
});
|
||||
}
|
||||
|
||||
return rankedItems
|
||||
.sort((itemA, itemB) => itemA.rank - itemB.rank)
|
||||
.map((item) => item.item);
|
||||
|
||||
@@ -86,11 +86,9 @@ class HaMenuButton extends LitElement {
|
||||
: this.narrow;
|
||||
|
||||
const oldShowButton =
|
||||
oldHass?.kioskMode === false &&
|
||||
(oldNarrow || oldHass?.dockedSidebar === "always_hidden");
|
||||
oldNarrow || oldHass?.dockedSidebar === "always_hidden";
|
||||
const showButton =
|
||||
this.hass.kioskMode === false &&
|
||||
(this.narrow || this.hass.dockedSidebar === "always_hidden");
|
||||
this.narrow || this.hass.dockedSidebar === "always_hidden";
|
||||
|
||||
if (this.hasUpdated && oldShowButton === showButton) {
|
||||
return;
|
||||
|
||||
@@ -42,11 +42,7 @@ class HaNavigationList extends LitElement {
|
||||
class=${page.iconColor ? "icon-background" : ""}
|
||||
.style="background-color: ${page.iconColor || "undefined"}"
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${page.iconPath}
|
||||
.secondaryPath=${page.iconSecondaryPath}
|
||||
.viewBox=${page.iconViewBox}
|
||||
></ha-svg-icon>
|
||||
<ha-svg-icon .path=${page.iconPath}></ha-svg-icon>
|
||||
</div>
|
||||
<span slot="headline">${page.name}</span>
|
||||
${this.hasSecondary
|
||||
|
||||
@@ -55,7 +55,6 @@ export interface PickerComboBoxItem {
|
||||
}
|
||||
|
||||
export const NO_ITEMS_AVAILABLE_ID = "___no_items_available___";
|
||||
const PADDING_ID = "___padding___";
|
||||
|
||||
const DEFAULT_ROW_RENDERER: RenderItemFunction<PickerComboBoxItem> = (
|
||||
item
|
||||
@@ -109,7 +108,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
public getItems!: (
|
||||
searchString?: string,
|
||||
section?: string
|
||||
) => PickerComboBoxItem[];
|
||||
) => (PickerComboBoxItem | string)[];
|
||||
|
||||
@property({ attribute: false, type: Array })
|
||||
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
|
||||
@@ -147,21 +146,21 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
@property({ attribute: "selected-section" }) public selectedSection?: string;
|
||||
|
||||
@query("lit-virtualizer") private _virtualizerElement?: LitVirtualizer;
|
||||
@query("lit-virtualizer") public virtualizerElement?: LitVirtualizer;
|
||||
|
||||
@query("ha-textfield") private _searchFieldElement?: HaTextField;
|
||||
|
||||
@state() private _items: PickerComboBoxItem[] = [];
|
||||
@state() private _items: (PickerComboBoxItem | string)[] = [];
|
||||
|
||||
protected get scrollableElement(): HTMLElement | null {
|
||||
return this._virtualizerElement as HTMLElement | null;
|
||||
return this.virtualizerElement as HTMLElement | null;
|
||||
}
|
||||
|
||||
@state() private _sectionTitle?: string;
|
||||
|
||||
@state() private _valuePinned = true;
|
||||
|
||||
private _allItems: PickerComboBoxItem[] = [];
|
||||
private _allItems: (PickerComboBoxItem | string)[] = [];
|
||||
|
||||
private _selectedItemIndex = -1;
|
||||
|
||||
@@ -201,6 +200,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
return html`<ha-textfield
|
||||
.label=${searchLabel}
|
||||
@blur=${this._resetSelectedItem}
|
||||
@input=${this._filterChanged}
|
||||
></ha-textfield>
|
||||
${this._renderSectionButtons()}
|
||||
@@ -238,6 +238,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
@unpinned=${this._handleUnpinned}
|
||||
@scroll=${this._onScrollList}
|
||||
@focus=${this._focusList}
|
||||
@blur=${this._resetSelectedItem}
|
||||
@visibilityChanged=${this._visibilityChanged}
|
||||
>
|
||||
</lit-virtualizer>
|
||||
@@ -270,18 +271,18 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
@eventOptions({ passive: true })
|
||||
private _visibilityChanged(ev) {
|
||||
if (
|
||||
this._virtualizerElement &&
|
||||
this.virtualizerElement &&
|
||||
this.sectionTitleFunction &&
|
||||
this.sections?.length
|
||||
) {
|
||||
const firstItem = this._virtualizerElement.items[ev.first];
|
||||
const secondItem = this._virtualizerElement.items[ev.first + 1];
|
||||
const firstItem = this.virtualizerElement.items[ev.first];
|
||||
const secondItem = this.virtualizerElement.items[ev.first + 1];
|
||||
this._sectionTitle = this.sectionTitleFunction({
|
||||
firstIndex: ev.first,
|
||||
lastIndex: ev.last,
|
||||
firstItem: firstItem as PickerComboBoxItem,
|
||||
secondItem: secondItem as PickerComboBoxItem,
|
||||
itemsCount: this._virtualizerElement.items.length,
|
||||
firstItem: firstItem as PickerComboBoxItem | string,
|
||||
secondItem: secondItem as PickerComboBoxItem | string,
|
||||
itemsCount: this.virtualizerElement.items.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -324,28 +325,28 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
if (!items.length && !this.allowCustomValue) {
|
||||
items.push({ id: NO_ITEMS_AVAILABLE_ID, primary: "" });
|
||||
if (!items.length) {
|
||||
items.push(NO_ITEMS_AVAILABLE_ID);
|
||||
}
|
||||
|
||||
const additionalItems = this._getAdditionalItems();
|
||||
items.push(...additionalItems);
|
||||
|
||||
if (this.mode === "dialog") {
|
||||
items.push({ id: PADDING_ID, primary: "" }); // padding for safe area inset
|
||||
items.push("padding"); // padding for safe area inset
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
private _renderItem = (item: PickerComboBoxItem, index: number) => {
|
||||
private _renderItem = (item: PickerComboBoxItem | string, index: number) => {
|
||||
if (!item) {
|
||||
return nothing;
|
||||
}
|
||||
if (item.id === PADDING_ID) {
|
||||
if (item === "padding") {
|
||||
return html`<div class="bottom-padding"></div>`;
|
||||
}
|
||||
if (item.id === NO_ITEMS_AVAILABLE_ID) {
|
||||
if (item === NO_ITEMS_AVAILABLE_ID) {
|
||||
return html`
|
||||
<div class="combo-box-row">
|
||||
<ha-combo-box-item type="text" compact>
|
||||
@@ -397,11 +398,17 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
private _valueSelected = (ev: Event) => {
|
||||
ev.stopPropagation();
|
||||
const value = (ev.currentTarget as any).value as string;
|
||||
const index = Number((ev.currentTarget as any).index);
|
||||
const newValue = value?.trim();
|
||||
|
||||
fireEvent(this, "value-changed", { value: newValue });
|
||||
this._fireSelectedEvents(newValue, index);
|
||||
};
|
||||
|
||||
private _fireSelectedEvents(value: string, index: number) {
|
||||
fireEvent(this, "value-changed", { value });
|
||||
fireEvent(this, "index-selected", { index });
|
||||
}
|
||||
|
||||
private _fuseIndex = memoizeOne(
|
||||
(states: PickerComboBoxItem[], searchKeys?: FuseWeightedKey[]) =>
|
||||
Fuse.createIndex(searchKeys || DEFAULT_SEARCH_KEYS, states)
|
||||
@@ -420,18 +427,21 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = this._fuseIndex(this._allItems, this.searchKeys);
|
||||
const index = this._fuseIndex(
|
||||
this._allItems as PickerComboBoxItem[],
|
||||
this.searchKeys
|
||||
);
|
||||
|
||||
let filteredItems = multiTermSortedSearch<PickerComboBoxItem>(
|
||||
this._allItems,
|
||||
this._allItems as PickerComboBoxItem[],
|
||||
searchString,
|
||||
this.searchKeys || DEFAULT_SEARCH_KEYS,
|
||||
(item) => item.id,
|
||||
index
|
||||
);
|
||||
) as (PickerComboBoxItem | string)[];
|
||||
|
||||
if (!filteredItems.length && !this.allowCustomValue) {
|
||||
filteredItems.push({ id: NO_ITEMS_AVAILABLE_ID, primary: "" });
|
||||
if (!filteredItems.length) {
|
||||
filteredItems.push(NO_ITEMS_AVAILABLE_ID);
|
||||
}
|
||||
|
||||
const additionalItems = this._getAdditionalItems(searchString);
|
||||
@@ -440,8 +450,8 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
if (this.searchFn) {
|
||||
filteredItems = this.searchFn(
|
||||
searchString,
|
||||
filteredItems,
|
||||
this._allItems
|
||||
filteredItems as PickerComboBoxItem[],
|
||||
this._allItems as PickerComboBoxItem[]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -457,7 +467,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
this._items = filteredItems;
|
||||
this._items = filteredItems as PickerComboBoxItem[];
|
||||
}
|
||||
|
||||
this._selectedItemIndex = -1;
|
||||
@@ -481,8 +491,8 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
this._items = this._getItems();
|
||||
|
||||
// Reset scroll position when filter changes
|
||||
if (this._virtualizerElement) {
|
||||
this._virtualizerElement.scrollToIndex(0);
|
||||
if (this.virtualizerElement) {
|
||||
this.virtualizerElement.scrollToIndex(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,13 +515,13 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
private _selectNextItem = (ev?: KeyboardEvent) => {
|
||||
ev?.stopPropagation();
|
||||
ev?.preventDefault();
|
||||
if (!this._virtualizerElement) {
|
||||
if (!this.virtualizerElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._searchFieldElement?.focus();
|
||||
|
||||
const items = this._virtualizerElement.items as PickerComboBoxItem[];
|
||||
const items = this.virtualizerElement.items as PickerComboBoxItem[];
|
||||
|
||||
const maxItems = items.length - 1;
|
||||
|
||||
@@ -545,14 +555,14 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
private _selectPreviousItem = (ev: KeyboardEvent) => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
if (!this._virtualizerElement) {
|
||||
if (!this.virtualizerElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._selectedItemIndex > 0) {
|
||||
const nextIndex = this._selectedItemIndex - 1;
|
||||
|
||||
const items = this._virtualizerElement.items as PickerComboBoxItem[];
|
||||
const items = this.virtualizerElement.items as PickerComboBoxItem[];
|
||||
|
||||
if (!items[nextIndex]) {
|
||||
return;
|
||||
@@ -574,13 +584,13 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
private _selectFirstItem = (ev: KeyboardEvent) => {
|
||||
ev.stopPropagation();
|
||||
if (!this._virtualizerElement || !this._virtualizerElement.items.length) {
|
||||
if (!this.virtualizerElement || !this.virtualizerElement.items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextIndex = 0;
|
||||
|
||||
if (typeof this._virtualizerElement.items[nextIndex] === "string") {
|
||||
if (typeof this.virtualizerElement.items[nextIndex] === "string") {
|
||||
this._selectedItemIndex = nextIndex + 1;
|
||||
} else {
|
||||
this._selectedItemIndex = nextIndex;
|
||||
@@ -591,13 +601,13 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
private _selectLastItem = (ev: KeyboardEvent) => {
|
||||
ev.stopPropagation();
|
||||
if (!this._virtualizerElement || !this._virtualizerElement.items.length) {
|
||||
if (!this.virtualizerElement || !this.virtualizerElement.items.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextIndex = this._virtualizerElement.items.length - 1;
|
||||
const nextIndex = this.virtualizerElement.items.length - 1;
|
||||
|
||||
if (typeof this._virtualizerElement.items[nextIndex] === "string") {
|
||||
if (typeof this.virtualizerElement.items[nextIndex] === "string") {
|
||||
this._selectedItemIndex = nextIndex - 1;
|
||||
} else {
|
||||
this._selectedItemIndex = nextIndex;
|
||||
@@ -607,14 +617,14 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
};
|
||||
|
||||
private _scrollToSelectedItem = () => {
|
||||
this._virtualizerElement
|
||||
this.virtualizerElement
|
||||
?.querySelector(".selected")
|
||||
?.classList.remove("selected");
|
||||
|
||||
this._virtualizerElement?.scrollToIndex(this._selectedItemIndex, "end");
|
||||
this.virtualizerElement?.scrollToIndex(this._selectedItemIndex, "end");
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this._virtualizerElement
|
||||
this.virtualizerElement
|
||||
?.querySelector(`#list-item-${this._selectedItemIndex}`)
|
||||
?.classList.add("selected");
|
||||
});
|
||||
@@ -622,12 +632,20 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
private _pickSelectedItem = (ev: KeyboardEvent) => {
|
||||
ev.stopPropagation();
|
||||
const firstItem = this._virtualizerElement?.items[0] as PickerComboBoxItem;
|
||||
|
||||
if (this._virtualizerElement?.items.length === 1) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: firstItem.id,
|
||||
if (
|
||||
this.virtualizerElement?.items?.length !== undefined &&
|
||||
this.virtualizerElement.items.length < 4 && // it still can have a section title and a padding item
|
||||
this.virtualizerElement.items.filter((item) => typeof item !== "string")
|
||||
.length === 1
|
||||
) {
|
||||
(
|
||||
this.virtualizerElement?.items as (PickerComboBoxItem | string)[]
|
||||
).forEach((item, index) => {
|
||||
if (typeof item !== "string") {
|
||||
this._fireSelectedEvents(item.id, index);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._selectedItemIndex === -1) {
|
||||
@@ -637,16 +655,16 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
// if filter button is focused
|
||||
ev.preventDefault();
|
||||
|
||||
const item = this._virtualizerElement?.items[
|
||||
const item = this.virtualizerElement?.items[
|
||||
this._selectedItemIndex
|
||||
] as PickerComboBoxItem;
|
||||
if (item) {
|
||||
fireEvent(this, "value-changed", { value: item.id });
|
||||
this._fireSelectedEvents(item.id, this._selectedItemIndex);
|
||||
}
|
||||
};
|
||||
|
||||
private _resetSelectedItem() {
|
||||
this._virtualizerElement
|
||||
this.virtualizerElement
|
||||
?.querySelector(".selected")
|
||||
?.classList.remove("selected");
|
||||
this._selectedItemIndex = -1;
|
||||
@@ -656,11 +674,11 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
typeof item === "string" ? item : item?.id;
|
||||
|
||||
private _getInitialSelectedIndex() {
|
||||
if (!this._virtualizerElement || this._search || !this.value) {
|
||||
if (!this.virtualizerElement || this._search || !this.value) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const index = this._virtualizerElement.items.findIndex(
|
||||
const index = this.virtualizerElement.items.findIndex(
|
||||
(item) =>
|
||||
typeof item !== "string" &&
|
||||
(item as PickerComboBoxItem).id === this.value
|
||||
@@ -840,4 +858,8 @@ declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-picker-combo-box": HaPickerComboBox;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"index-selected": { index: number };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,8 +54,7 @@ export class HaChooseSelector extends LitElement {
|
||||
size="small"
|
||||
.buttons=${this._toggleButtons(
|
||||
this.selector.choose.choices,
|
||||
this.selector.choose.translation_key,
|
||||
this.hass.localize
|
||||
this.selector.choose.translation_key
|
||||
)}
|
||||
.active=${this._activeChoice}
|
||||
@value-changed=${this._choiceChanged}
|
||||
@@ -73,11 +72,7 @@ export class HaChooseSelector extends LitElement {
|
||||
}
|
||||
|
||||
private _toggleButtons = memoizeOne(
|
||||
(
|
||||
choices: ChooseSelector["choose"]["choices"],
|
||||
translationKey?: string,
|
||||
_localize?: HomeAssistant["localize"]
|
||||
) =>
|
||||
(choices: ChooseSelector["choose"]["choices"], translationKey?: string) =>
|
||||
Object.keys(choices).map((choice) => ({
|
||||
label:
|
||||
this.localizeValue && translationKey
|
||||
|
||||
@@ -9,7 +9,6 @@ import { stopPropagation } from "../../common/dom/stop_propagation";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import type { SelectOption, SelectSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { PickerValueRenderer } from "../ha-picker-field";
|
||||
import "../chips/ha-chip-set";
|
||||
import "../chips/ha-input-chip";
|
||||
import "../ha-checkbox";
|
||||
@@ -224,30 +223,12 @@ export class HaSelectSelector extends LitElement {
|
||||
.required=${this.required}
|
||||
.getItems=${this._getItems(options)}
|
||||
.value=${this.value as string | undefined}
|
||||
.valueRenderer=${this._getValueRenderer(options)}
|
||||
@value-changed=${this._comboBoxValueChanged}
|
||||
allow-custom-value
|
||||
></ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
if (this._mode === "dropdown" && this.selector.select?.picker) {
|
||||
return html`
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
.getItems=${this._getItems(options)}
|
||||
.value=${this.value as string | undefined}
|
||||
.valueRenderer=${this._getValueRenderer(options)}
|
||||
@value-changed=${this._comboBoxValueChanged}
|
||||
.allowCustomValue=${this.selector.select?.custom_value ?? false}
|
||||
></ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-select
|
||||
fixedMenuPosition
|
||||
@@ -304,15 +285,6 @@ export class HaSelectSelector extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _getValueRenderer = memoizeOne(
|
||||
(options: SelectOption[]): PickerValueRenderer =>
|
||||
(value: string) => {
|
||||
const option = options.find((opt) => opt.value === value);
|
||||
const label = option?.label || value;
|
||||
return html`<span slot="headline">${label}</span>`;
|
||||
}
|
||||
);
|
||||
|
||||
private get _mode(): "list" | "dropdown" | "box" {
|
||||
return (
|
||||
this.selector.select?.mode ||
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
resolveEntityIDs,
|
||||
type StateSelector,
|
||||
type TargetSelector,
|
||||
} from "../../data/selector";
|
||||
import type { StateSelector } from "../../data/selector";
|
||||
import { extractFromTarget } from "../../data/target";
|
||||
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../entity/ha-entity-state-picker";
|
||||
import "../entity/ha-entity-states-picker";
|
||||
import type { PickerComboBoxItem } from "../ha-picker-combo-box";
|
||||
|
||||
@customElement("ha-selector-state")
|
||||
export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
@@ -33,33 +28,16 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
filter_attribute?: string;
|
||||
filter_entity?: string | string[];
|
||||
filter_target?: HassServiceTarget;
|
||||
target_selector?: TargetSelector;
|
||||
};
|
||||
|
||||
@state() private _entityIds?: string | string[];
|
||||
|
||||
private _convertExtraOptions = memoizeOne(
|
||||
(
|
||||
extraOptions?: { label: string; value: any }[]
|
||||
): PickerComboBoxItem[] | undefined => {
|
||||
if (!extraOptions) {
|
||||
return undefined;
|
||||
}
|
||||
return extraOptions.map((option) => ({
|
||||
id: option.value,
|
||||
primary: option.label,
|
||||
sorting_label: option.label,
|
||||
}));
|
||||
}
|
||||
);
|
||||
|
||||
willUpdate(changedProps) {
|
||||
if (changedProps.has("selector") || changedProps.has("context")) {
|
||||
this._resolveEntityIds(
|
||||
this.selector.state?.entity_id,
|
||||
this.context?.filter_entity,
|
||||
this.context?.filter_target,
|
||||
this.context?.target_selector
|
||||
this.context?.filter_target
|
||||
).then((entityIds) => {
|
||||
this._entityIds = entityIds;
|
||||
});
|
||||
@@ -67,9 +45,6 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const extraOptions = this._convertExtraOptions(
|
||||
this.selector.state?.extra_options
|
||||
);
|
||||
if (this.selector.state?.multiple) {
|
||||
return html`
|
||||
<ha-entity-states-picker
|
||||
@@ -77,7 +52,7 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
.entityId=${this._entityIds}
|
||||
.attribute=${this.selector.state?.attribute ||
|
||||
this.context?.filter_attribute}
|
||||
.extraOptions=${extraOptions}
|
||||
.extraOptions=${this.selector.state?.extra_options}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
@@ -94,7 +69,7 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
.entityId=${this._entityIds}
|
||||
.attribute=${this.selector.state?.attribute ||
|
||||
this.context?.filter_attribute}
|
||||
.extraOptions=${extraOptions}
|
||||
.extraOptions=${this.selector.state?.extra_options}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
@@ -109,8 +84,7 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
private async _resolveEntityIds(
|
||||
selectorEntityId: string | string[] | undefined,
|
||||
contextFilterEntity: string | string[] | undefined,
|
||||
contextFilterTarget: HassServiceTarget | undefined,
|
||||
contextTargetSelector: TargetSelector | undefined
|
||||
contextFilterTarget: HassServiceTarget | undefined
|
||||
): Promise<string | string[] | undefined> {
|
||||
if (selectorEntityId !== undefined) {
|
||||
return selectorEntityId;
|
||||
@@ -119,14 +93,8 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
return contextFilterEntity;
|
||||
}
|
||||
if (contextFilterTarget !== undefined) {
|
||||
return resolveEntityIDs(
|
||||
this.hass,
|
||||
contextFilterTarget,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
contextTargetSelector
|
||||
);
|
||||
const result = await extractFromTarget(this.hass, contextFilterTarget);
|
||||
return result.referenced_entities;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { isIosApp } from "../util/is_ios";
|
||||
import "./ha-dialog-header";
|
||||
import "./ha-icon-button";
|
||||
|
||||
@@ -184,22 +185,21 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
await this.updateComplete;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
// temporary disabled because of issues with focus in iOS app, can be reenabled in 2026.2.0
|
||||
// if (isIosApp(this.hass)) {
|
||||
// const element = this.querySelector("[autofocus]");
|
||||
// if (element !== null) {
|
||||
// if (!element.id) {
|
||||
// element.id = "ha-wa-dialog-autofocus";
|
||||
// }
|
||||
// this.hass.auth.external!.fireMessage({
|
||||
// type: "focus_element",
|
||||
// payload: {
|
||||
// element_id: element.id,
|
||||
// },
|
||||
// });
|
||||
// }
|
||||
// return;
|
||||
// }
|
||||
if (isIosApp(this.hass)) {
|
||||
const element = this.querySelector("[autofocus]");
|
||||
if (element !== null) {
|
||||
if (!element.id) {
|
||||
element.id = "ha-wa-dialog-autofocus";
|
||||
}
|
||||
this.hass.auth.external!.fireMessage({
|
||||
type: "focus_element",
|
||||
payload: {
|
||||
element_id: element.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
|
||||
});
|
||||
};
|
||||
@@ -271,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(
|
||||
|
||||
@@ -20,7 +20,7 @@ import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeEntityName } from "../../common/entity/compute_entity_name";
|
||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
|
||||
import { computeRTL } from "../../common/util/compute_rtl";
|
||||
import type { AreaRegistryEntry } from "../../data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../data/area/area_registry";
|
||||
import { getConfigEntry } from "../../data/config_entries";
|
||||
import { labelsContext } from "../../data/context";
|
||||
import type { DeviceRegistryEntry } from "../../data/device/device_registry";
|
||||
|
||||
199
src/data/area/area_picker.ts
Normal file
199
src/data/area/area_picker.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { mdiTextureBox } from "@mdi/js";
|
||||
import { computeAreaName } from "../../common/entity/compute_area_name";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { computeFloorName } from "../../common/entity/compute_floor_name";
|
||||
import { getAreaContext } from "../../common/entity/context/get_area_context";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "../../components/device/ha-device-picker";
|
||||
import type { PickerComboBoxItem } from "../../components/ha-picker-combo-box";
|
||||
import type { FuseWeightedKey } from "../../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import {
|
||||
getDeviceEntityDisplayLookup,
|
||||
type DeviceEntityDisplayLookup,
|
||||
type DeviceRegistryEntry,
|
||||
} from "../device/device_registry";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../entity/entity";
|
||||
import type { EntityRegistryDisplayEntry } from "../entity/entity_registry";
|
||||
|
||||
export const getAreas = (
|
||||
haAreas: HomeAssistant["areas"],
|
||||
haFloors: HomeAssistant["floors"],
|
||||
haDevices: HomeAssistant["devices"],
|
||||
haEntities: HomeAssistant["entities"],
|
||||
haStates: HomeAssistant["states"],
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
includeDeviceClasses?: string[],
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
excludeAreas?: string[],
|
||||
idPrefix = ""
|
||||
): PickerComboBoxItem[] => {
|
||||
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
|
||||
|
||||
const areas = Object.values(haAreas);
|
||||
const devices = Object.values(haDevices);
|
||||
const entities = Object.values(haEntities);
|
||||
|
||||
if (
|
||||
includeDomains ||
|
||||
excludeDomains ||
|
||||
includeDeviceClasses ||
|
||||
deviceFilter ||
|
||||
entityFilter
|
||||
) {
|
||||
deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
|
||||
inputDevices = devices;
|
||||
inputEntities = entities.filter((entity) => entity.area_id);
|
||||
|
||||
if (includeDomains) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) =>
|
||||
includeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) =>
|
||||
includeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
}
|
||||
|
||||
if (excludeDomains) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return true;
|
||||
}
|
||||
return entities.every(
|
||||
(entity) => !excludeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
});
|
||||
inputEntities = inputEntities!.filter(
|
||||
(entity) => !excludeDomains.includes(computeDomain(entity.entity_id))
|
||||
);
|
||||
}
|
||||
|
||||
if (includeDeviceClasses) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = haStates[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = haStates[entity.entity_id];
|
||||
return (
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (deviceFilter) {
|
||||
inputDevices = inputDevices!.filter((device) => deviceFilter!(device));
|
||||
}
|
||||
|
||||
if (entityFilter) {
|
||||
inputDevices = inputDevices!.filter((device) => {
|
||||
const devEntities = deviceEntityLookup[device.id];
|
||||
if (!devEntities || !devEntities.length) {
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = haStates[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return entityFilter(stateObj);
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = haStates[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
return entityFilter!(stateObj);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let outputAreas = areas;
|
||||
|
||||
let areaIds: string[] | undefined;
|
||||
|
||||
if (inputDevices) {
|
||||
areaIds = inputDevices
|
||||
.filter((device) => device.area_id)
|
||||
.map((device) => device.area_id!);
|
||||
}
|
||||
|
||||
if (inputEntities) {
|
||||
areaIds = (areaIds ?? []).concat(
|
||||
inputEntities
|
||||
.filter((entity) => entity.area_id)
|
||||
.map((entity) => entity.area_id!)
|
||||
);
|
||||
}
|
||||
|
||||
if (areaIds) {
|
||||
outputAreas = outputAreas.filter((area) => areaIds!.includes(area.area_id));
|
||||
}
|
||||
|
||||
if (excludeAreas) {
|
||||
outputAreas = outputAreas.filter(
|
||||
(area) => !excludeAreas!.includes(area.area_id)
|
||||
);
|
||||
}
|
||||
|
||||
const items = outputAreas.map<PickerComboBoxItem>((area) => {
|
||||
const { floor } = getAreaContext(area, haFloors);
|
||||
const floorName = floor ? computeFloorName(floor) : undefined;
|
||||
const areaName = computeAreaName(area);
|
||||
return {
|
||||
id: `${idPrefix}${area.area_id}`,
|
||||
primary: areaName || area.area_id,
|
||||
secondary: floorName,
|
||||
icon: area.icon || undefined,
|
||||
icon_path: area.icon ? undefined : mdiTextureBox,
|
||||
search_labels: {
|
||||
aliases: area.aliases.join(" "),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
export const areaComboBoxKeys: FuseWeightedKey[] = [
|
||||
{
|
||||
name: "primary",
|
||||
weight: 10,
|
||||
},
|
||||
{
|
||||
name: "search_labels.aliases",
|
||||
weight: 8,
|
||||
},
|
||||
{
|
||||
name: "secondary",
|
||||
weight: 8,
|
||||
},
|
||||
{
|
||||
name: "search_labels.domain",
|
||||
weight: 4,
|
||||
},
|
||||
];
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { DeviceRegistryEntry } from "./device/device_registry";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { DeviceRegistryEntry } from "../device/device_registry";
|
||||
import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
EntityRegistryEntry,
|
||||
} from "./entity/entity_registry";
|
||||
import type { RegistryEntry } from "./registry";
|
||||
} from "../entity/entity_registry";
|
||||
import type { RegistryEntry } from "../registry";
|
||||
|
||||
export { subscribeAreaRegistry } from "./ws-area_registry";
|
||||
export { subscribeAreaRegistry } from "../ws-area_registry";
|
||||
|
||||
export interface AreaRegistryEntry extends RegistryEntry {
|
||||
aliases: string[];
|
||||
@@ -6,7 +6,7 @@ import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-dev
|
||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { AreaRegistryEntry } from "./area_registry";
|
||||
import type { AreaRegistryEntry } from "./area/area_registry";
|
||||
import {
|
||||
getDeviceEntityDisplayLookup,
|
||||
type DeviceEntityDisplayLookup,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { AreaRegistryEntry } from "./area_registry";
|
||||
import type { AreaRegistryEntry } from "./area/area_registry";
|
||||
import type { RegistryEntry } from "./registry";
|
||||
|
||||
export { subscribeAreaRegistry } from "./ws-area_registry";
|
||||
|
||||
327
src/data/quick_bar.ts
Normal file
327
src/data/quick_bar.ts
Normal file
@@ -0,0 +1,327 @@
|
||||
import {
|
||||
mdiKeyboard,
|
||||
mdiNavigationVariant,
|
||||
mdiPuzzle,
|
||||
mdiReload,
|
||||
mdiServerNetwork,
|
||||
mdiStorePlus,
|
||||
} from "@mdi/js";
|
||||
import { canShowPage } from "../common/config/can_show_page";
|
||||
import { componentsWithService } from "../common/config/components_with_service";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||
import type { PageNavigation } from "../layouts/hass-tabs-subpage";
|
||||
import { configSections } from "../panels/config/ha-panel-config";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HassioAddonInfo } from "./hassio/addon";
|
||||
import { domainToName } from "./integration";
|
||||
import { getPanelIcon, getPanelNameTranslationKey } from "./panel";
|
||||
import type { FuseWeightedKey } from "../resources/fuseMultiTerm";
|
||||
|
||||
export interface NavigationComboBoxItem extends PickerComboBoxItem {
|
||||
path: string;
|
||||
image?: string;
|
||||
iconColor?: string;
|
||||
}
|
||||
|
||||
export interface BaseNavigationCommand {
|
||||
path: string;
|
||||
primary: string;
|
||||
icon_path?: string;
|
||||
iconPath?: string;
|
||||
iconColor?: string;
|
||||
image?: string;
|
||||
}
|
||||
|
||||
export interface ActionCommandComboBoxItem extends PickerComboBoxItem {
|
||||
action: string;
|
||||
domain?: string;
|
||||
}
|
||||
|
||||
export interface NavigationInfo extends PageNavigation {
|
||||
primary: string;
|
||||
}
|
||||
|
||||
const generateNavigationPanelCommands = (
|
||||
localize: HomeAssistant["localize"],
|
||||
panels: HomeAssistant["panels"],
|
||||
addons?: HassioAddonInfo[]
|
||||
): BaseNavigationCommand[] =>
|
||||
Object.entries(panels)
|
||||
.filter(
|
||||
([panelKey]) => panelKey !== "_my_redirect" && panelKey !== "hassio"
|
||||
)
|
||||
.map(([_panelKey, panel]) => {
|
||||
const translationKey = getPanelNameTranslationKey(panel);
|
||||
const icon = getPanelIcon(panel) || "mdi:view-dashboard";
|
||||
|
||||
const primary = localize(translationKey) || panel.title || panel.url_path;
|
||||
|
||||
let image: string | undefined;
|
||||
|
||||
if (addons) {
|
||||
const addon = addons.find(({ slug }) => slug === panel.url_path);
|
||||
if (addon) {
|
||||
image = addon.icon
|
||||
? `/api/hassio/addons/${addon.slug}/icon`
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
primary,
|
||||
icon,
|
||||
image,
|
||||
path: `/${panel.url_path}`,
|
||||
};
|
||||
});
|
||||
|
||||
const getNavigationInfoFromConfig = (
|
||||
localize: HomeAssistant["localize"],
|
||||
page: PageNavigation
|
||||
): NavigationInfo | undefined => {
|
||||
const path = page.path.substring(1);
|
||||
|
||||
let name = path.substring(path.indexOf("/") + 1);
|
||||
name = name.indexOf("/") > -1 ? name.substring(0, name.indexOf("/")) : name;
|
||||
|
||||
const caption =
|
||||
(name && localize(`ui.dialogs.quick-bar.commands.navigation.${name}`)) ||
|
||||
// @ts-expect-error
|
||||
(page.translationKey && localize(page.translationKey));
|
||||
|
||||
if (caption) {
|
||||
return { ...page, primary: caption };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const generateNavigationConfigSectionCommands = (
|
||||
hass: HomeAssistant
|
||||
): BaseNavigationCommand[] => {
|
||||
if (!hass.user?.is_admin) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const items: NavigationInfo[] = [];
|
||||
|
||||
Object.values(configSections).forEach((sectionPages) => {
|
||||
sectionPages.forEach((page) => {
|
||||
if (!canShowPage(hass, page)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const info = getNavigationInfoFromConfig(hass.localize, page);
|
||||
|
||||
if (!info) {
|
||||
return;
|
||||
}
|
||||
// Add to list, but only if we do not already have an entry for the same path and component
|
||||
if (items.some((e) => e.path === info.path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
items.push(info);
|
||||
});
|
||||
});
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
const finalizeNavigationCommands = (
|
||||
localize: HomeAssistant["localize"],
|
||||
items: BaseNavigationCommand[]
|
||||
): NavigationComboBoxItem[] =>
|
||||
items.map((item, index) => {
|
||||
const secondary = localize(
|
||||
"ui.dialogs.quick-bar.commands.types.navigation"
|
||||
);
|
||||
return {
|
||||
id: `navigation_${index}_${item.path}`,
|
||||
icon_path: item.iconPath || mdiNavigationVariant,
|
||||
secondary,
|
||||
sorting_label: `${item.primary}_${secondary}`,
|
||||
...item,
|
||||
};
|
||||
});
|
||||
|
||||
export const generateNavigationCommands = (
|
||||
hass: HomeAssistant,
|
||||
addons?: HassioAddonInfo[]
|
||||
): NavigationComboBoxItem[] => {
|
||||
const panelItems = generateNavigationPanelCommands(
|
||||
hass.localize,
|
||||
hass.panels,
|
||||
addons
|
||||
);
|
||||
const sectionItems = generateNavigationConfigSectionCommands(hass);
|
||||
const supervisorItems: BaseNavigationCommand[] = [];
|
||||
if (hass.user?.is_admin && isComponentLoaded(hass, "hassio")) {
|
||||
supervisorItems.push({
|
||||
path: "/hassio/store",
|
||||
icon_path: mdiStorePlus,
|
||||
primary: hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.navigation.addon_store"
|
||||
),
|
||||
});
|
||||
supervisorItems.push({
|
||||
path: "/hassio/dashboard",
|
||||
icon_path: mdiPuzzle,
|
||||
primary: hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.navigation.addon_dashboard"
|
||||
),
|
||||
});
|
||||
if (addons) {
|
||||
for (const addon of addons.filter((a) => a.version)) {
|
||||
supervisorItems.push({
|
||||
path: `/hassio/addon/${addon.slug}`,
|
||||
image: addon.icon
|
||||
? `/api/hassio/addons/${addon.slug}/icon`
|
||||
: undefined,
|
||||
primary: hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.navigation.addon_info",
|
||||
{ addon: addon.name }
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const additionalItems = [
|
||||
{
|
||||
path: "",
|
||||
primary: hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.navigation.shortcuts"
|
||||
),
|
||||
icon_path: mdiKeyboard,
|
||||
},
|
||||
];
|
||||
|
||||
return finalizeNavigationCommands(hass.localize, [
|
||||
...panelItems,
|
||||
...sectionItems,
|
||||
...supervisorItems,
|
||||
...additionalItems,
|
||||
]);
|
||||
};
|
||||
|
||||
const generateReloadCommands = (
|
||||
hass: HomeAssistant
|
||||
): ActionCommandComboBoxItem[] => {
|
||||
// Get all domains that have a direct "reload" service
|
||||
const reloadableDomains = componentsWithService(hass, "reload");
|
||||
|
||||
const commands = reloadableDomains.map((domain) => ({
|
||||
primary:
|
||||
hass.localize(`ui.dialogs.quick-bar.commands.reload.${domain}`) ||
|
||||
hass.localize("ui.dialogs.quick-bar.commands.reload.reload", {
|
||||
domain: domainToName(hass.localize, domain),
|
||||
}),
|
||||
domain,
|
||||
action: "reload",
|
||||
icon_path: mdiReload,
|
||||
secondary: hass.localize(`ui.dialogs.quick-bar.commands.types.reload`),
|
||||
}));
|
||||
|
||||
// Add "frontend.reload_themes"
|
||||
commands.push({
|
||||
primary: hass.localize("ui.dialogs.quick-bar.commands.reload.themes"),
|
||||
domain: "frontend",
|
||||
action: "reload_themes",
|
||||
icon_path: mdiReload,
|
||||
secondary: hass.localize("ui.dialogs.quick-bar.commands.types.reload"),
|
||||
});
|
||||
|
||||
// Add "homeassistant.reload_core_config"
|
||||
commands.push({
|
||||
primary: hass.localize("ui.dialogs.quick-bar.commands.reload.core"),
|
||||
domain: "homeassistant",
|
||||
action: "reload_core_config",
|
||||
icon_path: mdiReload,
|
||||
secondary: hass.localize("ui.dialogs.quick-bar.commands.types.reload"),
|
||||
});
|
||||
|
||||
// Add "homeassistant.reload_all"
|
||||
commands.push({
|
||||
primary: hass.localize("ui.dialogs.quick-bar.commands.reload.all"),
|
||||
domain: "homeassistant",
|
||||
action: "reload_all",
|
||||
icon_path: mdiReload,
|
||||
secondary: hass.localize("ui.dialogs.quick-bar.commands.types.reload"),
|
||||
});
|
||||
|
||||
return commands.map((command, index) => ({
|
||||
...command,
|
||||
id: `command_${index}_${command.primary}`,
|
||||
sorting_label: `${command.primary}_${command.secondary}_${command.domain}`,
|
||||
}));
|
||||
};
|
||||
|
||||
const generateServerControlCommands = (
|
||||
hass: HomeAssistant
|
||||
): ActionCommandComboBoxItem[] => {
|
||||
const serverActions = ["restart", "stop"] as const;
|
||||
|
||||
return serverActions.map((action, index) => {
|
||||
const primary = hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.server_control.perform_action",
|
||||
{
|
||||
action: hass.localize(
|
||||
`ui.dialogs.quick-bar.commands.server_control.${action}`
|
||||
),
|
||||
}
|
||||
);
|
||||
|
||||
const secondary = hass.localize(
|
||||
"ui.dialogs.quick-bar.commands.types.server_control"
|
||||
);
|
||||
|
||||
return {
|
||||
id: `server_control_${index}_${action}`,
|
||||
primary,
|
||||
domain: "homeassistant",
|
||||
icon_path: mdiServerNetwork,
|
||||
secondary,
|
||||
sorting_label: `${primary}_${secondary}_${action}`,
|
||||
action,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const generateActionCommands = (
|
||||
hass: HomeAssistant
|
||||
): ActionCommandComboBoxItem[] => [
|
||||
...generateReloadCommands(hass),
|
||||
...generateServerControlCommands(hass),
|
||||
];
|
||||
|
||||
export const commandComboBoxKeys: FuseWeightedKey[] = [
|
||||
{
|
||||
name: "primary",
|
||||
weight: 10,
|
||||
},
|
||||
{
|
||||
name: "domain",
|
||||
weight: 8,
|
||||
},
|
||||
{
|
||||
name: "secondary",
|
||||
weight: 6,
|
||||
},
|
||||
];
|
||||
|
||||
export const navigateComboBoxKeys: FuseWeightedKey[] = [
|
||||
{
|
||||
name: "primary",
|
||||
weight: 10,
|
||||
},
|
||||
{
|
||||
name: "path",
|
||||
weight: 8,
|
||||
},
|
||||
{
|
||||
name: "secondary",
|
||||
weight: 6,
|
||||
},
|
||||
];
|
||||
@@ -16,8 +16,6 @@ export interface RecorderInfo {
|
||||
|
||||
export type StatisticType = "change" | "state" | "sum" | "min" | "max" | "mean";
|
||||
|
||||
export type StatisticPeriod = "5minute" | "hour" | "day" | "week" | "month";
|
||||
|
||||
export type Statistics = Record<string, StatisticValue[]>;
|
||||
|
||||
export interface StatisticValue {
|
||||
@@ -176,7 +174,7 @@ export const fetchStatistics = (
|
||||
startTime: Date,
|
||||
endTime?: Date,
|
||||
statistic_ids?: string[],
|
||||
period: StatisticPeriod = "hour",
|
||||
period: "5minute" | "hour" | "day" | "week" | "month" = "hour",
|
||||
units?: StatisticsUnitConfiguration,
|
||||
types?: StatisticsTypes
|
||||
) =>
|
||||
|
||||
@@ -388,7 +388,6 @@ export interface SelectSelector {
|
||||
multiple?: boolean;
|
||||
custom_value?: boolean;
|
||||
mode?: "list" | "dropdown" | "box";
|
||||
picker?: boolean;
|
||||
options: readonly string[] | readonly SelectOption[];
|
||||
translation_key?: string;
|
||||
sort?: boolean;
|
||||
@@ -930,13 +929,13 @@ export const resolveEntityIDs = (
|
||||
targetPickerValue: HassServiceTarget,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"],
|
||||
targetSelector: TargetSelector = { target: {} }
|
||||
areas: HomeAssistant["areas"]
|
||||
): string[] => {
|
||||
if (!targetPickerValue) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const targetSelector = { target: {} };
|
||||
const targetEntities = new Set(ensureArray(targetPickerValue.entity_id));
|
||||
const targetDevices = new Set(ensureArray(targetPickerValue.device_id));
|
||||
const targetAreas = new Set(ensureArray(targetPickerValue.area_id));
|
||||
|
||||
@@ -3,8 +3,8 @@ import { computeDomain } from "../common/entity/compute_domain";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
|
||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { AreaRegistryEntry } from "./area/area_registry";
|
||||
import type { FloorComboBoxItem } from "./area_floor_picker";
|
||||
import type { AreaRegistryEntry } from "./area_registry";
|
||||
import type { DevicePickerItem } from "./device/device_picker";
|
||||
import type { DeviceRegistryEntry } from "./device/device_registry";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "./entity/entity";
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Connection } from "home-assistant-js-websocket";
|
||||
import { createCollection } from "home-assistant-js-websocket";
|
||||
import type { Store } from "home-assistant-js-websocket/dist/store";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import type { AreaRegistryEntry } from "./area_registry";
|
||||
import type { AreaRegistryEntry } from "./area/area_registry";
|
||||
|
||||
const fetchAreaRegistry = (conn: Connection) =>
|
||||
conn.sendMessagePromise<AreaRegistryEntry[]>({
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,15 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
|
||||
export const enum QuickBarMode {
|
||||
Command = "command",
|
||||
Device = "device",
|
||||
Entity = "entity",
|
||||
}
|
||||
export type QuickBarSection =
|
||||
| "entity"
|
||||
| "device"
|
||||
| "area"
|
||||
| "navigate"
|
||||
| "command";
|
||||
|
||||
export interface QuickBarParams {
|
||||
entityFilter?: string;
|
||||
mode?: QuickBarMode;
|
||||
mode?: QuickBarSection;
|
||||
hint?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { mdiAppleKeyboardCommand } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { LocalizeKeys } from "../../common/translations/localize";
|
||||
import "../../components/ha-alert";
|
||||
import { createCloseHeading } from "../../components/ha-dialog";
|
||||
import "../../components/ha-wa-dialog";
|
||||
import "../../components/ha-svg-icon";
|
||||
import { haStyleDialog } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { isMac } from "../../util/is_mac";
|
||||
|
||||
@@ -39,6 +38,10 @@ const _SHORTCUTS: Section[] = [
|
||||
{
|
||||
textTranslationKey: "ui.dialogs.shortcuts.searching.on_any_page",
|
||||
},
|
||||
{
|
||||
shortcut: [CTRL_CMD, "K"],
|
||||
descriptionTranslationKey: "ui.dialogs.shortcuts.searching.search",
|
||||
},
|
||||
{
|
||||
shortcut: ["C"],
|
||||
descriptionTranslationKey:
|
||||
@@ -162,17 +165,22 @@ const _SHORTCUTS: Section[] = [
|
||||
class DialogShortcuts extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _opened = false;
|
||||
@state() private _open = false;
|
||||
|
||||
public async showDialog(): Promise<void> {
|
||||
this._opened = true;
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
public async closeDialog(): Promise<void> {
|
||||
this._opened = false;
|
||||
private _dialogClosed() {
|
||||
this._open = false;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
public async closeDialog() {
|
||||
this._open = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private _renderShortcut(
|
||||
shortcutKeys: ShortcutString[],
|
||||
descriptionKey: LocalizeKeys
|
||||
@@ -201,20 +209,11 @@ class DialogShortcuts extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._opened) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
hideActions
|
||||
@closed=${this.closeDialog}
|
||||
defaultAction="ignore"
|
||||
.heading=${createCloseHeading(
|
||||
this.hass,
|
||||
this.hass.localize("ui.dialogs.shortcuts.title")
|
||||
)}
|
||||
<ha-wa-dialog
|
||||
.open=${this._open}
|
||||
@closed=${this._dialogClosed}
|
||||
.headerTitle=${this.hass.localize("ui.dialogs.shortcuts.title")}
|
||||
>
|
||||
<div class="content">
|
||||
${_SHORTCUTS.map(
|
||||
@@ -237,7 +236,7 @@ class DialogShortcuts extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ha-alert>
|
||||
<ha-alert slot="footer">
|
||||
${this.hass.localize("ui.dialogs.shortcuts.enable_shortcuts_hint", {
|
||||
user_profile: html`<a href="/profile/general#shortcuts"
|
||||
>${this.hass.localize(
|
||||
@@ -246,25 +245,12 @@ class DialogShortcuts extends LitElement {
|
||||
>`,
|
||||
})}
|
||||
</ha-alert>
|
||||
</ha-dialog>
|
||||
</ha-wa-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = [
|
||||
haStyleDialog,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-z-index: 15;
|
||||
}
|
||||
|
||||
h3:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@@ -286,6 +272,10 @@ class DialogShortcuts extends LitElement {
|
||||
ha-svg-icon {
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
ha-alert a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -59,10 +59,28 @@ export const handleExternalMessage = (
|
||||
|
||||
if (msg.command === "restart") {
|
||||
hassMainEl.hass.connection.reconnect(true);
|
||||
bus.fireMessage({
|
||||
id: msg.id,
|
||||
type: "result",
|
||||
success: true,
|
||||
result: null,
|
||||
});
|
||||
} else if (msg.command === "navigate") {
|
||||
navigate(msg.payload.path, msg.payload.options);
|
||||
bus.fireMessage({
|
||||
id: msg.id,
|
||||
type: "result",
|
||||
success: true,
|
||||
result: null,
|
||||
});
|
||||
} else if (msg.command === "notifications/show") {
|
||||
fireEvent(hassMainEl, "hass-show-notifications");
|
||||
bus.fireMessage({
|
||||
id: msg.id,
|
||||
type: "result",
|
||||
success: true,
|
||||
result: null,
|
||||
});
|
||||
} else if (msg.command === "sidebar/toggle") {
|
||||
if (mainWindow.history.state?.open) {
|
||||
bus.fireMessage({
|
||||
@@ -74,6 +92,12 @@ export const handleExternalMessage = (
|
||||
return true;
|
||||
}
|
||||
fireEvent(hassMainEl, "hass-toggle-menu");
|
||||
bus.fireMessage({
|
||||
id: msg.id,
|
||||
type: "result",
|
||||
success: true,
|
||||
result: null,
|
||||
});
|
||||
} else if (msg.command === "sidebar/show") {
|
||||
if (mainWindow.history.state?.open) {
|
||||
bus.fireMessage({
|
||||
@@ -85,29 +109,56 @@ export const handleExternalMessage = (
|
||||
return true;
|
||||
}
|
||||
fireEvent(hassMainEl, "hass-toggle-menu", { open: true });
|
||||
bus.fireMessage({
|
||||
id: msg.id,
|
||||
type: "result",
|
||||
success: true,
|
||||
result: null,
|
||||
});
|
||||
} else if (msg.command === "automation/editor/show") {
|
||||
showAutomationEditor(msg.payload?.config);
|
||||
bus.fireMessage({
|
||||
id: msg.id,
|
||||
type: "result",
|
||||
success: true,
|
||||
result: null,
|
||||
});
|
||||
} else if (msg.command === "improv/discovered_device") {
|
||||
fireEvent(window, "improv-discovered-device", msg.payload);
|
||||
bus.fireMessage({
|
||||
id: msg.id,
|
||||
type: "result",
|
||||
success: true,
|
||||
result: null,
|
||||
});
|
||||
} else if (msg.command === "improv/device_setup_done") {
|
||||
fireEvent(window, "improv-device-setup-done");
|
||||
bus.fireMessage({
|
||||
id: msg.id,
|
||||
type: "result",
|
||||
success: true,
|
||||
result: null,
|
||||
});
|
||||
} else if (msg.command === "bar_code/scan_result") {
|
||||
barCodeListeners.forEach((listener) => listener(msg));
|
||||
bus.fireMessage({
|
||||
id: msg.id,
|
||||
type: "result",
|
||||
success: true,
|
||||
result: null,
|
||||
});
|
||||
} else if (msg.command === "bar_code/aborted") {
|
||||
barCodeListeners.forEach((listener) => listener(msg));
|
||||
} else if (msg.command === "kiosk_mode/set") {
|
||||
fireEvent(window, "hass-kiosk-mode", { enable: msg.payload.enable });
|
||||
bus.fireMessage({
|
||||
id: msg.id,
|
||||
type: "result",
|
||||
success: true,
|
||||
result: null,
|
||||
});
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
bus.fireMessage({
|
||||
id: msg.id,
|
||||
type: "result",
|
||||
success: true,
|
||||
result: null,
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
@@ -301,15 +301,6 @@ export interface EMIncomingMessageImprovDeviceSetupDone extends EMMessage {
|
||||
command: "improv/device_setup_done";
|
||||
}
|
||||
|
||||
export interface EMIncomingMessageKioskModeSet {
|
||||
id: number;
|
||||
type: "command";
|
||||
command: "kiosk_mode/set";
|
||||
payload: {
|
||||
enable: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export type EMIncomingMessageCommands =
|
||||
| EMIncomingMessageRestart
|
||||
| EMIncomingMessageNavigate
|
||||
@@ -320,8 +311,7 @@ export type EMIncomingMessageCommands =
|
||||
| EMIncomingMessageBarCodeScanResult
|
||||
| EMIncomingMessageBarCodeScanAborted
|
||||
| EMIncomingMessageImprovDeviceDiscovered
|
||||
| EMIncomingMessageImprovDeviceSetupDone
|
||||
| EMIncomingMessageKioskModeSet;
|
||||
| EMIncomingMessageImprovDeviceSetupDone;
|
||||
|
||||
type EMIncomingMessage =
|
||||
| EMMessageResultSuccess
|
||||
|
||||
@@ -282,7 +282,6 @@ export const provideHass = (
|
||||
dockedSidebar: "auto",
|
||||
vibrate: true,
|
||||
debugConnection: false,
|
||||
kioskMode: false,
|
||||
suspendWhenHidden: false,
|
||||
moreInfoEntityId: null as any,
|
||||
// @ts-ignore
|
||||
|
||||
@@ -23,8 +23,6 @@ export interface PageNavigation {
|
||||
core?: boolean;
|
||||
advancedOnly?: boolean;
|
||||
iconPath?: string;
|
||||
iconSecondaryPath?: string;
|
||||
iconViewBox?: string;
|
||||
description?: string;
|
||||
iconColor?: string;
|
||||
info?: any;
|
||||
|
||||
@@ -44,8 +44,7 @@ export class HomeAssistantMain extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const sidebarNarrow =
|
||||
this._sidebarNarrow || this._externalSidebar || this.hass.kioskMode;
|
||||
const sidebarNarrow = this._sidebarNarrow || this._externalSidebar;
|
||||
|
||||
const isPanelReady =
|
||||
this.hass.panels && this.hass.userData && this.hass.systemData;
|
||||
@@ -134,7 +133,7 @@ export class HomeAssistantMain extends LitElement {
|
||||
toggleAttribute(
|
||||
this,
|
||||
"modal",
|
||||
this._sidebarNarrow || this._externalSidebar || this.hass.kioskMode
|
||||
this._sidebarNarrow || this._externalSidebar
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ import "../../../components/ha-wa-dialog";
|
||||
import type {
|
||||
AreaRegistryEntry,
|
||||
AreaRegistryEntryMutableParams,
|
||||
} from "../../../data/area_registry";
|
||||
import { deleteAreaRegistryEntry } from "../../../data/area_registry";
|
||||
} from "../../../data/area/area_registry";
|
||||
import { deleteAreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import {
|
||||
SENSOR_DEVICE_CLASS_HUMIDITY,
|
||||
SENSOR_DEVICE_CLASS_TEMPERATURE,
|
||||
|
||||
@@ -14,16 +14,16 @@ import "../../../components/ha-button";
|
||||
import "../../../components/ha-dialog-footer";
|
||||
import "../../../components/ha-floor-icon";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-wa-dialog";
|
||||
import "../../../components/ha-md-list";
|
||||
import "../../../components/ha-md-list-item";
|
||||
import "../../../components/ha-sortable";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import "../../../components/ha-wa-dialog";
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import {
|
||||
reorderAreaRegistryEntries,
|
||||
updateAreaRegistryEntry,
|
||||
} from "../../../data/area_registry";
|
||||
} from "../../../data/area/area_registry";
|
||||
import { reorderFloorRegistryEntries } from "../../../data/floor_registry";
|
||||
import { haStyle, haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@@ -18,7 +18,7 @@ import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-textfield";
|
||||
import "../../../components/ha-wa-dialog";
|
||||
import { updateAreaRegistryEntry } from "../../../data/area_registry";
|
||||
import { updateAreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type {
|
||||
FloorRegistryEntry,
|
||||
FloorRegistryEntryMutableParams,
|
||||
|
||||
@@ -23,11 +23,11 @@ import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-list";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-tooltip";
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import {
|
||||
deleteAreaRegistryEntry,
|
||||
updateAreaRegistryEntry,
|
||||
} from "../../../data/area_registry";
|
||||
} from "../../../data/area/area_registry";
|
||||
import type { AutomationEntity } from "../../../data/automation";
|
||||
import { fullEntitiesContext } from "../../../data/context";
|
||||
import type { DeviceRegistryEntry } from "../../../data/device/device_registry";
|
||||
|
||||
@@ -31,12 +31,12 @@ import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-sortable";
|
||||
import type { HaSortableOptions } from "../../../components/ha-sortable";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import {
|
||||
createAreaRegistryEntry,
|
||||
reorderAreaRegistryEntries,
|
||||
updateAreaRegistryEntry,
|
||||
} from "../../../data/area_registry";
|
||||
} from "../../../data/area/area_registry";
|
||||
import type { FloorRegistryEntry } from "../../../data/floor_registry";
|
||||
import {
|
||||
createFloorRegistryEntry,
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
import "../../../layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import "../ha-config-section";
|
||||
import { configSections } from "../ha-panel-config";
|
||||
import {
|
||||
loadAreaRegistryDetailDialog,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type {
|
||||
AreaRegistryEntry,
|
||||
AreaRegistryEntryMutableParams,
|
||||
} from "../../../data/area_registry";
|
||||
} from "../../../data/area/area_registry";
|
||||
|
||||
export interface AreaRegistryDetailDialogParams {
|
||||
entry?: AreaRegistryEntry;
|
||||
|
||||
@@ -57,11 +57,11 @@ import {
|
||||
ACTION_COLLECTIONS,
|
||||
ACTION_ICONS,
|
||||
} from "../../../data/action";
|
||||
import type { FloorComboBoxItem } from "../../../data/area_floor_picker";
|
||||
import {
|
||||
getAreaDeviceLookup,
|
||||
getAreaEntityLookup,
|
||||
} from "../../../data/area_registry";
|
||||
} from "../../../data/area/area_registry";
|
||||
import type { FloorComboBoxItem } from "../../../data/area_floor_picker";
|
||||
import {
|
||||
DYNAMIC_PREFIX,
|
||||
getValueFromDynamic,
|
||||
|
||||
@@ -28,6 +28,10 @@ import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-section-title";
|
||||
import "../../../../components/ha-state-icon";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import {
|
||||
getAreaDeviceLookup,
|
||||
getAreaEntityLookup,
|
||||
} from "../../../../data/area/area_registry";
|
||||
import {
|
||||
getAreasNestedInFloors,
|
||||
type AreaFloorValue,
|
||||
@@ -35,10 +39,6 @@ import {
|
||||
type FloorNestedComboBoxItem,
|
||||
type UnassignedAreasFloorComboBoxItem,
|
||||
} from "../../../../data/area_floor_picker";
|
||||
import {
|
||||
getAreaDeviceLookup,
|
||||
getAreaEntityLookup,
|
||||
} from "../../../../data/area_registry";
|
||||
import {
|
||||
getConfigEntries,
|
||||
type ConfigEntry,
|
||||
|
||||
@@ -4,6 +4,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../../common/entity/compute_domain";
|
||||
import "../../../../../components/ha-checkbox";
|
||||
import "../../../../../components/ha-selector/ha-selector";
|
||||
import "../../../../../components/ha-settings-row";
|
||||
@@ -268,11 +269,8 @@ export class HaPlatformCondition extends LitElement {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const context: Record<string, any> = {};
|
||||
const context = {};
|
||||
for (const [context_key, data_key] of Object.entries(field.context)) {
|
||||
if (data_key === "target" && this.description?.target) {
|
||||
context.target_selector = this._targetSelector(this.description.target);
|
||||
}
|
||||
context[context_key] =
|
||||
data_key === "target"
|
||||
? this.condition.target
|
||||
@@ -380,7 +378,7 @@ export class HaPlatformCondition extends LitElement {
|
||||
return "";
|
||||
}
|
||||
return this.hass.localize(
|
||||
`component.${getConditionDomain(this.condition.condition)}.selector.${key}`
|
||||
`component.${computeDomain(this.condition.condition)}.selector.${key}`
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ import type { Entries, HomeAssistant, Route } from "../../../types";
|
||||
import { isMac } from "../../../util/is_mac";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
||||
import "../ha-config-section";
|
||||
import { showAutomationModeDialog } from "./automation-mode-dialog/show-dialog-automation-mode";
|
||||
import {
|
||||
type EntityRegistryUpdate,
|
||||
|
||||
@@ -66,7 +66,7 @@ import type { HaMdMenuItem } from "../../../components/ha-md-menu-item";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import { createAreaRegistryEntry } from "../../../data/area_registry";
|
||||
import { createAreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type { AutomationEntity } from "../../../data/automation";
|
||||
import {
|
||||
deleteAutomation,
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiAlert,
|
||||
mdiCodeBraces,
|
||||
mdiFormatListBulleted,
|
||||
mdiShape,
|
||||
} from "@mdi/js";
|
||||
import { mdiAlert, mdiFormatListBulleted, mdiShape } from "@mdi/js";
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, type nothing, type TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { ensureArray } from "../../../../common/array/ensure-array";
|
||||
import { transform } from "../../../../common/decorators/transform";
|
||||
import { isTemplate } from "../../../../common/string/has-template";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type { ConfigEntry } from "../../../../data/config_entries";
|
||||
import {
|
||||
@@ -173,16 +167,6 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the target is a template
|
||||
if (isTemplate(targetId)) {
|
||||
return this._renderTargetBadge(
|
||||
html`<ha-svg-icon .path=${mdiCodeBraces}></ha-svg-icon>`,
|
||||
this.localize(
|
||||
"ui.panel.config.automation.editor.target_summary.template"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const exists = this._checkTargetExists(targetType, targetId);
|
||||
if (!exists) {
|
||||
return this._renderTargetBadge(
|
||||
|
||||
@@ -4,6 +4,7 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../../common/entity/compute_domain";
|
||||
import "../../../../../components/ha-checkbox";
|
||||
import "../../../../../components/ha-selector/ha-selector";
|
||||
import "../../../../../components/ha-settings-row";
|
||||
@@ -304,11 +305,8 @@ export class HaPlatformTrigger extends LitElement {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const context: Record<string, any> = {};
|
||||
const context = {};
|
||||
for (const [context_key, data_key] of Object.entries(field.context)) {
|
||||
if (data_key === "target" && this.description?.target) {
|
||||
context.target_selector = this._targetSelector(this.description.target);
|
||||
}
|
||||
context[context_key] =
|
||||
data_key === "target"
|
||||
? this.trigger.target
|
||||
@@ -416,7 +414,7 @@ export class HaPlatformTrigger extends LitElement {
|
||||
return "";
|
||||
}
|
||||
return this.hass.localize(
|
||||
`component.${getTriggerDomain(this.trigger.trigger)}.selector.${key}`
|
||||
`component.${computeDomain(this.trigger.trigger)}.selector.${key}`
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ class DialogImportBlueprint extends LitElement {
|
||||
</div>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="secondaryAction"
|
||||
slot="primaryAction"
|
||||
@click=${this.closeDialog}
|
||||
.disabled=${this._saving}
|
||||
>
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} 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 { 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";
|
||||
@@ -33,12 +33,10 @@ import {
|
||||
checkForEntityUpdates,
|
||||
filterUpdateEntitiesWithInstall,
|
||||
} from "../../../data/update";
|
||||
import {
|
||||
QuickBarMode,
|
||||
showQuickBar,
|
||||
} from "../../../dialogs/quick-bar/show-dialog-quick-bar";
|
||||
import { showQuickBar } from "../../../dialogs/quick-bar/show-dialog-quick-bar";
|
||||
import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart";
|
||||
import { showShortcutsDialog } from "../../../dialogs/shortcuts/show-shortcuts-dialog";
|
||||
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
@@ -154,6 +152,8 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ attribute: false }) public cloudStatus?: CloudStatus;
|
||||
|
||||
@property({ attribute: false }) public showAdvanced = false;
|
||||
|
||||
@state() private _tip?: string;
|
||||
|
||||
@state() private _repairsIssues: { issues: RepairsIssue[]; total: number } = {
|
||||
@@ -161,24 +161,21 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
total: 0,
|
||||
};
|
||||
|
||||
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,
|
||||
]);
|
||||
private _pages = memoizeOne((cloudStatus, isCloudLoaded) => {
|
||||
const pages: PageNavigation[] = [];
|
||||
if (isCloudLoaded) {
|
||||
pages.push({
|
||||
component: "cloud",
|
||||
path: "/config/cloud",
|
||||
name: "Home Assistant Cloud",
|
||||
info: cloudStatus,
|
||||
iconPath: mdiCloudLock,
|
||||
iconColor: "#3B808E",
|
||||
translationKey: "cloud",
|
||||
});
|
||||
}
|
||||
return [...pages, ...configSections.dashboard];
|
||||
});
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
return [
|
||||
@@ -308,22 +305,18 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
: ""}
|
||||
</ha-card>`
|
||||
: ""}
|
||||
${this._pages(
|
||||
this.cloudStatus,
|
||||
isComponentLoaded(this.hass, "cloud")
|
||||
).map((categoryPages) =>
|
||||
categoryPages.length === 0
|
||||
? nothing
|
||||
: html`
|
||||
<ha-card outlined>
|
||||
<ha-config-navigation
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.pages=${categoryPages}
|
||||
></ha-config-navigation>
|
||||
</ha-card>
|
||||
`
|
||||
)}
|
||||
|
||||
<ha-card outlined>
|
||||
<ha-config-navigation
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.showAdvanced=${this.showAdvanced}
|
||||
.pages=${this._pages(
|
||||
this.cloudStatus,
|
||||
isComponentLoaded(this.hass, "cloud")
|
||||
)}
|
||||
></ha-config-navigation>
|
||||
</ha-card>
|
||||
<ha-tip .hass=${this.hass}>${this._tip}</ha-tip>
|
||||
</ha-config-section>
|
||||
</ha-top-app-bar-fixed>
|
||||
@@ -368,7 +361,6 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
};
|
||||
|
||||
showQuickBar(this, {
|
||||
mode: QuickBarMode.Command,
|
||||
hint: this.hass.enableShortcuts
|
||||
? this.hass.localize("ui.dialogs.quick-bar.key_c_tip", params)
|
||||
: undefined,
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { canShowPage } from "../../../common/config/can_show_page";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-next";
|
||||
import "../../../components/ha-navigation-list";
|
||||
import type { CloudStatus } from "../../../data/cloud";
|
||||
import { getConfigEntries } from "../../../data/config_entries";
|
||||
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@@ -18,29 +17,13 @@ class HaConfigNavigation extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public pages!: PageNavigation[];
|
||||
|
||||
@state() private _hasBluetoothConfigEntries = false;
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
getConfigEntries(this.hass, {
|
||||
domain: "bluetooth",
|
||||
}).then((bluetoothEntries) => {
|
||||
this._hasBluetoothConfigEntries = bluetoothEntries.length > 0;
|
||||
});
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const pages = this.pages
|
||||
.filter((page) => {
|
||||
if (page.path === "#external-app-configuration") {
|
||||
return this.hass.auth.external?.config.hasSettingsScreen;
|
||||
}
|
||||
// Only show Bluetooth page if there are Bluetooth config entries
|
||||
if (page.component === "bluetooth") {
|
||||
return this._hasBluetoothConfigEntries;
|
||||
}
|
||||
return canShowPage(this.hass, page);
|
||||
})
|
||||
.filter((page) =>
|
||||
page.path === "#external-app-configuration"
|
||||
? this.hass.auth.external?.config.hasSettingsScreen
|
||||
: canShowPage(this.hass, page)
|
||||
)
|
||||
.map((page) => ({
|
||||
...page,
|
||||
name:
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-md-divider";
|
||||
import "../../../components/ha-md-menu-item";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import { createAreaRegistryEntry } from "../../../data/area_registry";
|
||||
import { createAreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type { ConfigEntry, SubEntry } from "../../../data/config_entries";
|
||||
import { getSubEntries, sortConfigEntries } from "../../../data/config_entries";
|
||||
import { fullEntitiesContext } from "../../../data/context";
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
mdiAccount,
|
||||
mdiBackupRestore,
|
||||
mdiBadgeAccountHorizontal,
|
||||
mdiBluetooth,
|
||||
mdiCellphoneCog,
|
||||
mdiCog,
|
||||
mdiDatabase,
|
||||
@@ -30,8 +29,6 @@ import {
|
||||
mdiTools,
|
||||
mdiUpdate,
|
||||
mdiViewDashboard,
|
||||
mdiZigbee,
|
||||
mdiZWave,
|
||||
} from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
@@ -104,59 +101,6 @@ export const configSections: Record<string, PageNavigation[]> = {
|
||||
iconPath: mdiMicrophone,
|
||||
iconColor: "#3263C3",
|
||||
},
|
||||
],
|
||||
dashboard_2: [
|
||||
{
|
||||
path: "/config/zha",
|
||||
name: "Zigbee",
|
||||
iconPath: mdiZigbee,
|
||||
iconColor: "#E74011",
|
||||
component: "zha",
|
||||
translationKey: "zha",
|
||||
},
|
||||
{
|
||||
path: "/config/zwave_js",
|
||||
name: "Z-Wave",
|
||||
iconPath: mdiZWave,
|
||||
iconColor: "#153163",
|
||||
component: "zwave_js",
|
||||
translationKey: "zwave_js",
|
||||
},
|
||||
{
|
||||
path: "/knx",
|
||||
name: "KNX",
|
||||
iconPath:
|
||||
"M 3.9861338,14.261456 3.7267552,13.934877 6.3179131,11.306266 H 4.466374 l -2.6385205,2.68258 V 11.312882 H 0.00440574 L 0,17.679803 l 1.8278535,5.43e-4 v -1.818482 l 0.7225444,-0.732459 2.1373588,2.543782 2.1869324,-5.44e-4 M 24,17.680369 21.809238,17.669359 19.885559,15.375598 17.640262,17.68037 h -1.828407 l 3.236048,-3.302138 -2.574075,-3.067547 2.135161,0.0016 1.610309,1.87687 1.866403,-1.87687 h 1.828429 l -2.857742,2.87478 m -10.589867,-2.924898 2.829625,3.990552 -0.01489,-3.977887 1.811889,-0.0044 0.0011,6.357564 -2.093314,-5.44e-4 -2.922133,-3.947594 -0.0314,3.947594 H 8.2581097 V 11.261677 M 11.971203,6.3517488 c 0,0 2.800714,-0.093203 6.172001,1.0812045 3.462393,1.0898845 5.770926,3.4695627 5.770926,3.4695627 l -1.823898,-5.43e-4 C 22.088532,10.900273 20.577938,9.4271528 17.660223,8.5024618 15.139256,7.703366 12.723057,7.645835 12.111178,7.6449876 l -9.71e-4,0.0011 c 0,0 -0.0259,-6.4e-4 -0.07527,-9.714e-4 -0.04726,3.33e-4 -0.07201,9.714e-4 -0.07201,9.714e-4 v -0.00113 C 11.337007,7.6453728 8.8132091,7.7001736 6.2821829,8.5024618 3.3627914,9.4276738 1.8521646,10.901973 1.8521646,10.901973 l -1.82398708,5.43e-4 C 0.03128403,10.899322 2.339143,8.5221038 5.799224,7.4329533 9.170444,6.2585642 11.971203,6.3517488 11.971203,6.3517488 Z",
|
||||
iconColor: "#4EAA66",
|
||||
component: "knx",
|
||||
translationKey: "knx",
|
||||
},
|
||||
{
|
||||
path: "/config/thread",
|
||||
name: "Thread",
|
||||
iconPath:
|
||||
"m 17.126982,8.0730792 c 0,-0.7297242 -0.593746,-1.32357 -1.323637,-1.32357 -0.729454,0 -1.323199,0.5938458 -1.323199,1.32357 v 1.3234242 l 1.323199,1.458e-4 c 0.729891,0 1.323637,-0.5937006 1.323637,-1.32357 z M 11.999709,0 C 5.3829818,0 0,5.3838955 0,12.001455 0,18.574352 5.3105455,23.927406 11.865164,24 V 12.012075 l -3.9275642,-2.91e-4 c -1.1669814,0 -2.1169453,0.949979 -2.1169453,2.118323 0,1.16718 0.9499639,2.116868 2.1169453,2.116868 v 2.615717 c -2.6093089,0 -4.732218,-2.12327 -4.732218,-4.732585 0,-2.61048 2.1229091,-4.7343308 4.732218,-4.7343308 l 3.9275642,5.82e-4 v -1.323279 c 0,-2.172296 1.766691,-3.9395777 3.938181,-3.9395777 2.171928,0 3.9392,1.7672817 3.9392,3.9395777 0,2.1721498 -1.767272,3.9395768 -3.9392,3.9395768 l -1.323199,-1.45e-4 V 23.744102 C 19.911127,22.597726 24,17.768833 24,12.001455 24,5.3838955 18.616727,0 11.999709,0 Z",
|
||||
iconColor: "#ED7744",
|
||||
component: "thread",
|
||||
translationKey: "thread",
|
||||
},
|
||||
{
|
||||
path: "/config/bluetooth",
|
||||
name: "Bluetooth",
|
||||
iconPath: mdiBluetooth,
|
||||
iconColor: "#0082FC",
|
||||
component: "bluetooth",
|
||||
translationKey: "bluetooth",
|
||||
},
|
||||
{
|
||||
path: "/insteon",
|
||||
name: "Insteon",
|
||||
iconPath:
|
||||
"m 12.001571,6.3842473 h 0.02973 c 3.652189,0 6.767389,-2.29456 7.987462,-5.5177193 L 15.389382,0 Z m 0,0 h -0.02972 c -3.6522186,0 -6.7673314,-2.2918546 -7.9874477,-5.5177193 h -0.00271 L 8.6111273,0 Z M 6.3840436,11.999287 v -0.02972 c 0,-3.6524074 -2.2944727,-6.7675928 -5.51754469,-7.9877383 L 0,8.6114473 Z m 0,0 v 0.02964 c 0,3.652378 -2.2917818,6.767578 -5.51754469,7.987796 v 0.0026 L 0,15.389818 Z M 24,8.6114473 23.133527,3.9818327 v 0.00269 C 19.907636,5.2046836 17.616,8.3198691 17.616,11.972276 v 0.02966 0.02972 0.0027 c 0,3.65232 2.2944,6.76752 5.517527,7.987738 L 24,15.392436 17.616,12.001935 Z M 20.018618,23.133527 15.389091,24 11.99872,17.615709 h 0.02964 c 3.652218,0 6.767418,2.291927 7.987491,5.517818 z M 11.99872,17.615709 8.6082618,24 3.9788364,23.133527 C 5.1989527,19.9104 8.3140655,17.615709 11.966284,17.615709 h 0.0027 z",
|
||||
iconColor: "#E4002C",
|
||||
component: "insteon",
|
||||
translationKey: "insteon",
|
||||
},
|
||||
{
|
||||
path: "/config/tags",
|
||||
translationKey: "tags",
|
||||
@@ -164,8 +108,6 @@ export const configSections: Record<string, PageNavigation[]> = {
|
||||
iconColor: "#616161",
|
||||
component: "tag",
|
||||
},
|
||||
],
|
||||
dashboard_3: [
|
||||
{
|
||||
path: "/config/person",
|
||||
translationKey: "people",
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import { mdiCogOutline } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-button";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-list";
|
||||
import "../../../../../components/ha-list-item";
|
||||
|
||||
import "../../../../../components/ha-code-editor";
|
||||
import "../../../../../components/ha-formfield";
|
||||
import "../../../../../components/ha-switch";
|
||||
import "../../../../../components/ha-button";
|
||||
import { getConfigEntries } from "../../../../../data/config_entries";
|
||||
import { showOptionsFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-options-flow";
|
||||
import "../../../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import {
|
||||
subscribeBluetoothConnectionAllocations,
|
||||
subscribeBluetoothScannerState,
|
||||
subscribeBluetoothScannersDetails,
|
||||
} from "../../../../../data/bluetooth";
|
||||
import type {
|
||||
BluetoothAllocationsData,
|
||||
BluetoothScannerState,
|
||||
@@ -16,17 +23,10 @@ import type {
|
||||
HaScannerType,
|
||||
} from "../../../../../data/bluetooth";
|
||||
import {
|
||||
subscribeBluetoothConnectionAllocations,
|
||||
subscribeBluetoothScannerState,
|
||||
subscribeBluetoothScannersDetails,
|
||||
} from "../../../../../data/bluetooth";
|
||||
import type { ConfigEntry } from "../../../../../data/config_entries";
|
||||
import { getConfigEntries } from "../../../../../data/config_entries";
|
||||
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
|
||||
import { showOptionsFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-options-flow";
|
||||
import "../../../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
getValueInPercentage,
|
||||
roundWithOneDecimal,
|
||||
} from "../../../../../util/calculate";
|
||||
import "../../../../../components/ha-metric";
|
||||
|
||||
@customElement("bluetooth-config-dashboard")
|
||||
export class BluetoothConfigDashboard extends LitElement {
|
||||
@@ -34,14 +34,18 @@ export class BluetoothConfigDashboard extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _configEntries: ConfigEntry[] = [];
|
||||
|
||||
@state() private _connectionAllocationData: BluetoothAllocationsData[] = [];
|
||||
|
||||
@state() private _scannerStates: Record<string, BluetoothScannerState> = {};
|
||||
@state() private _connectionAllocationsError?: string;
|
||||
|
||||
@state() private _scannerState?: BluetoothScannerState;
|
||||
|
||||
@state() private _scannerDetails?: BluetoothScannersDetails;
|
||||
|
||||
private _configEntry = new URLSearchParams(window.location.search).get(
|
||||
"config_entry"
|
||||
);
|
||||
|
||||
private _unsubConnectionAllocations?: (() => Promise<void>) | undefined;
|
||||
|
||||
private _unsubScannerState?: (() => Promise<void>) | undefined;
|
||||
@@ -51,44 +55,41 @@ export class BluetoothConfigDashboard extends LitElement {
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.hass) {
|
||||
this._loadConfigEntries();
|
||||
this._subscribeBluetoothConnectionAllocations();
|
||||
this._subscribeBluetoothScannerState();
|
||||
this._subscribeScannerDetails();
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadConfigEntries(): Promise<void> {
|
||||
this._configEntries = await getConfigEntries(this.hass, {
|
||||
domain: "bluetooth",
|
||||
});
|
||||
}
|
||||
|
||||
private async _subscribeBluetoothConnectionAllocations(): Promise<void> {
|
||||
if (this._unsubConnectionAllocations) {
|
||||
if (this._unsubConnectionAllocations || !this._configEntry) {
|
||||
return;
|
||||
}
|
||||
this._unsubConnectionAllocations =
|
||||
await subscribeBluetoothConnectionAllocations(
|
||||
this.hass.connection,
|
||||
(data) => {
|
||||
this._connectionAllocationData = data;
|
||||
}
|
||||
);
|
||||
try {
|
||||
this._unsubConnectionAllocations =
|
||||
await subscribeBluetoothConnectionAllocations(
|
||||
this.hass.connection,
|
||||
(data) => {
|
||||
this._connectionAllocationData = data;
|
||||
},
|
||||
this._configEntry
|
||||
);
|
||||
} catch (err: any) {
|
||||
this._unsubConnectionAllocations = undefined;
|
||||
this._connectionAllocationsError = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
private async _subscribeBluetoothScannerState(): Promise<void> {
|
||||
if (this._unsubScannerState) {
|
||||
if (this._unsubScannerState || !this._configEntry) {
|
||||
return;
|
||||
}
|
||||
this._unsubScannerState = await subscribeBluetoothScannerState(
|
||||
this.hass.connection,
|
||||
(scannerState) => {
|
||||
this._scannerStates = {
|
||||
...this._scannerStates,
|
||||
[scannerState.source]: scannerState,
|
||||
};
|
||||
}
|
||||
this._scannerState = scannerState;
|
||||
},
|
||||
this._configEntry
|
||||
);
|
||||
}
|
||||
|
||||
@@ -121,19 +122,31 @@ export class BluetoothConfigDashboard extends LitElement {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
// Get scanner type to determine if options button should be shown
|
||||
const scannerDetails =
|
||||
this._scannerState && this._scannerDetails?.[this._scannerState.source];
|
||||
const scannerType: HaScannerType =
|
||||
scannerDetails?.scanner_type ?? "unknown";
|
||||
const isRemoteScanner = scannerType === "remote";
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
header=${this.hass.localize("ui.panel.config.bluetooth.title")}
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
>
|
||||
<hass-subpage .narrow=${this.narrow} .hass=${this.hass}>
|
||||
<div class="content">
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.settings_title"
|
||||
)}
|
||||
>
|
||||
<ha-list>${this._renderAdaptersList()}</ha-list>
|
||||
<div class="card-content">${this._renderScannerState()}</div>
|
||||
${!isRemoteScanner
|
||||
? html`<div class="card-actions">
|
||||
<ha-button @click=${this._openOptionFlow}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.option_flow"
|
||||
)}</ha-button
|
||||
>
|
||||
</div>`
|
||||
: nothing}
|
||||
</ha-card>
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
@@ -170,11 +183,7 @@ export class BluetoothConfigDashboard extends LitElement {
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.connection_slot_allocations_monitor_description"
|
||||
)}
|
||||
</p>
|
||||
${this._renderConnectionAllocations()}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button
|
||||
@@ -192,90 +201,13 @@ export class BluetoothConfigDashboard extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderAdaptersList() {
|
||||
if (this._configEntries.length === 0) {
|
||||
return html`<ha-list-item noninteractive>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.no_scanner_state_available"
|
||||
)}
|
||||
</ha-list-item>`;
|
||||
}
|
||||
|
||||
// Build source to device mapping (same as visualization)
|
||||
const sourceDevices: Record<string, DeviceRegistryEntry> = {};
|
||||
Object.values(this.hass.devices).forEach((device) => {
|
||||
const btConnection = device.connections.find(
|
||||
(connection) => connection[0] === "bluetooth"
|
||||
);
|
||||
if (btConnection) {
|
||||
sourceDevices[btConnection[1]] = device;
|
||||
}
|
||||
});
|
||||
|
||||
return this._configEntries.map((entry) => {
|
||||
// Find scanner by matching device's config_entries to this entry
|
||||
const scannerDetails = this._scannerDetails
|
||||
? Object.values(this._scannerDetails).find((d) => {
|
||||
const device = sourceDevices[d.source];
|
||||
return device?.config_entries.includes(entry.entry_id);
|
||||
})
|
||||
: undefined;
|
||||
const scannerState = scannerDetails
|
||||
? this._scannerStates[scannerDetails.source]
|
||||
: undefined;
|
||||
const scannerType: HaScannerType =
|
||||
scannerDetails?.scanner_type ?? "unknown";
|
||||
const isRemoteScanner = scannerType === "remote";
|
||||
const hasMismatch =
|
||||
scannerState &&
|
||||
scannerState.current_mode !== scannerState.requested_mode;
|
||||
|
||||
// Find allocation data for this scanner
|
||||
const allocations = scannerDetails
|
||||
? this._connectionAllocationData.find(
|
||||
(a) => a.source === scannerDetails.source
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const secondaryText = this._formatScannerModeText(scannerState);
|
||||
|
||||
return html`
|
||||
<ha-list-item twoline hasMeta noninteractive>
|
||||
<span>${entry.title}</span>
|
||||
<span slot="secondary">
|
||||
${secondaryText}${allocations
|
||||
? allocations.slots > 0
|
||||
? ` · ${allocations.slots - allocations.free}/${allocations.slots} ${this.hass.localize("ui.panel.config.bluetooth.active_connections")}`
|
||||
: ` · ${this.hass.localize("ui.panel.config.bluetooth.no_connection_slots")}`
|
||||
: nothing}
|
||||
</span>
|
||||
${!isRemoteScanner
|
||||
? html`<ha-icon-button
|
||||
slot="meta"
|
||||
.path=${mdiCogOutline}
|
||||
.entry=${entry}
|
||||
@click=${this._openOptionFlow}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.option_flow"
|
||||
)}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
</ha-list-item>
|
||||
${hasMismatch && scannerDetails
|
||||
? this._renderScannerMismatchWarning(
|
||||
entry.title,
|
||||
scannerState,
|
||||
scannerType
|
||||
)
|
||||
: nothing}
|
||||
`;
|
||||
});
|
||||
}
|
||||
private _getUsedAllocations = (used: number, total: number) =>
|
||||
roundWithOneDecimal(getValueInPercentage(used, 0, total));
|
||||
|
||||
private _renderScannerMismatchWarning(
|
||||
name: string,
|
||||
scannerState: BluetoothScannerState,
|
||||
scannerType: HaScannerType
|
||||
scannerType: HaScannerType,
|
||||
formatMode: (mode: string | null) => string
|
||||
) {
|
||||
const instructions: string[] = [];
|
||||
|
||||
@@ -306,9 +238,8 @@ export class BluetoothConfigDashboard extends LitElement {
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.scanner_mode_mismatch",
|
||||
{
|
||||
name: name,
|
||||
requested: this._formatMode(scannerState.requested_mode),
|
||||
current: this._formatMode(scannerState.current_mode),
|
||||
requested: formatMode(scannerState.requested_mode),
|
||||
current: formatMode(scannerState.current_mode),
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
@@ -318,59 +249,127 @@ export class BluetoothConfigDashboard extends LitElement {
|
||||
</ha-alert>`;
|
||||
}
|
||||
|
||||
private _formatMode(mode: string | null): string {
|
||||
switch (mode) {
|
||||
case null:
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.bluetooth.scanning_mode_none"
|
||||
);
|
||||
case "active":
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.bluetooth.scanning_mode_active"
|
||||
);
|
||||
case "passive":
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.bluetooth.scanning_mode_passive"
|
||||
);
|
||||
default:
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
|
||||
private _formatModeLabel(mode: string | null): string {
|
||||
switch (mode) {
|
||||
case null:
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.bluetooth.scanning_mode_none_label"
|
||||
);
|
||||
case "active":
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.bluetooth.scanning_mode_active_label"
|
||||
);
|
||||
case "passive":
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.bluetooth.scanning_mode_passive_label"
|
||||
);
|
||||
default:
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
|
||||
private _formatScannerModeText(
|
||||
scannerState: BluetoothScannerState | undefined
|
||||
): string {
|
||||
if (!scannerState) {
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.bluetooth.scanner_state_unknown"
|
||||
);
|
||||
private _renderScannerState() {
|
||||
if (!this._configEntry || !this._scannerState) {
|
||||
return html`<div>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.no_scanner_state_available"
|
||||
)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return this._formatModeLabel(scannerState.current_mode);
|
||||
const scannerState = this._scannerState;
|
||||
// Find the scanner details for this source
|
||||
const scannerDetails = this._scannerDetails?.[scannerState.source];
|
||||
const scannerType: HaScannerType =
|
||||
scannerDetails?.scanner_type ?? "unknown";
|
||||
|
||||
const formatMode = (mode: string | null) => {
|
||||
switch (mode) {
|
||||
case null:
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.bluetooth.scanning_mode_none"
|
||||
);
|
||||
case "active":
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.bluetooth.scanning_mode_active"
|
||||
);
|
||||
case "passive":
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.bluetooth.scanning_mode_passive"
|
||||
);
|
||||
default:
|
||||
return mode; // Fallback for unknown modes
|
||||
}
|
||||
};
|
||||
|
||||
return html`
|
||||
<div class="scanner-state">
|
||||
<div class="state-row">
|
||||
<span
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.current_scanning_mode"
|
||||
)}:</span
|
||||
>
|
||||
<span class="state-value"
|
||||
>${formatMode(scannerState.current_mode)}</span
|
||||
>
|
||||
</div>
|
||||
<div class="state-row">
|
||||
<span
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.requested_scanning_mode"
|
||||
)}:</span
|
||||
>
|
||||
<span class="state-value"
|
||||
>${formatMode(scannerState.requested_mode)}</span
|
||||
>
|
||||
</div>
|
||||
${scannerState.current_mode !== scannerState.requested_mode
|
||||
? this._renderScannerMismatchWarning(
|
||||
scannerState,
|
||||
scannerType,
|
||||
formatMode
|
||||
)
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _openOptionFlow(ev: Event) {
|
||||
const button = ev.currentTarget as HTMLElement & { entry: ConfigEntry };
|
||||
showOptionsFlowDialog(this, button.entry);
|
||||
private _renderConnectionAllocations() {
|
||||
if (this._connectionAllocationsError) {
|
||||
return html`<ha-alert alert-type="error"
|
||||
>${this._connectionAllocationsError}</ha-alert
|
||||
>`;
|
||||
}
|
||||
if (this._connectionAllocationData.length === 0) {
|
||||
return html`<div>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.no_connection_slot_allocations"
|
||||
)}
|
||||
</div>`;
|
||||
}
|
||||
const allocations = this._connectionAllocationData[0];
|
||||
const allocationsUsed = allocations.slots - allocations.free;
|
||||
const allocationsTotal = allocations.slots;
|
||||
if (allocationsTotal === 0) {
|
||||
return html`<div>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.no_active_connection_support"
|
||||
)}
|
||||
</div>`;
|
||||
}
|
||||
return html`
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.connection_slot_allocations_monitor_details",
|
||||
{ slots: allocationsTotal }
|
||||
)}
|
||||
</p>
|
||||
<ha-metric
|
||||
.heading=${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.used_connection_slot_allocations"
|
||||
)}
|
||||
.value=${this._getUsedAllocations(allocationsUsed, allocationsTotal)}
|
||||
.tooltip=${allocations.allocated.length > 0
|
||||
? `${allocationsUsed}/${allocationsTotal} (${allocations.allocated.join(", ")})`
|
||||
: `${allocationsUsed}/${allocationsTotal}`}
|
||||
></ha-metric>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _openOptionFlow() {
|
||||
const configEntryId = this._configEntry;
|
||||
if (!configEntryId) {
|
||||
return;
|
||||
}
|
||||
const configEntries = await getConfigEntries(this.hass, {
|
||||
domain: "bluetooth",
|
||||
});
|
||||
const configEntry = configEntries.find(
|
||||
(entry) => entry.entry_id === configEntryId
|
||||
);
|
||||
showOptionsFlowDialog(this, configEntry!);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -395,9 +394,17 @@ export class BluetoothConfigDashboard extends LitElement {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
ha-list-item {
|
||||
--mdc-list-item-meta-display: flex;
|
||||
--mdc-list-item-meta-size: 48px;
|
||||
.scanner-state {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.state-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.state-value {
|
||||
font-weight: 500;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -46,54 +46,44 @@ export class MatterConfigDashboard extends LitElement {
|
||||
href="/config/thread"
|
||||
slot="toolbar-icon"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.thread_panel"
|
||||
)}</ha-button
|
||||
Visit Thread Panel</ha-button
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
<div class="content">
|
||||
<ha-card header="Matter">
|
||||
<ha-alert alert-type="warning"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.experimental_note"
|
||||
)}</ha-alert
|
||||
>Matter is still in the early phase of development, it is not
|
||||
meant to be used in production. This panel is for development
|
||||
only.</ha-alert
|
||||
>
|
||||
<div class="card-content">
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
${this.hass.localize("ui.panel.config.matter.panel.add_devices")}
|
||||
You can add Matter devices by commissing them if they are not
|
||||
setup yet, or share them from another controller and enter the
|
||||
share code.
|
||||
</div>
|
||||
<div class="card-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
|
||||
>Commission device with mobile app</ha-button
|
||||
>`
|
||||
: ""}
|
||||
<ha-button appearance="plain" @click=${this._commission}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.commission_device"
|
||||
)}</ha-button
|
||||
>Commission device</ha-button
|
||||
>
|
||||
<ha-button appearance="plain" @click=${this._acceptSharedDevice}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.add_shared_device"
|
||||
)}</ha-button
|
||||
>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
|
||||
>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
|
||||
>Set Thread Credentials</ha-button
|
||||
>
|
||||
</div>
|
||||
</ha-card>
|
||||
@@ -124,31 +114,19 @@ export class MatterConfigDashboard extends LitElement {
|
||||
private async _setWifi(): Promise<void> {
|
||||
this._error = undefined;
|
||||
const networkName = await showPromptDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.matter.panel.prompts.network_name.title"
|
||||
),
|
||||
inputLabel: this.hass.localize(
|
||||
"ui.panel.config.matter.panel.prompts.network_name.input_label"
|
||||
),
|
||||
title: "Network name",
|
||||
inputLabel: "Network name",
|
||||
inputType: "string",
|
||||
confirmText: this.hass.localize(
|
||||
"ui.panel.config.matter.panel.prompts.network_name.confirm"
|
||||
),
|
||||
confirmText: "Continue",
|
||||
});
|
||||
if (!networkName) {
|
||||
return;
|
||||
}
|
||||
const psk = await showPromptDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.matter.panel.prompts.passcode.title"
|
||||
),
|
||||
inputLabel: this.hass.localize(
|
||||
"ui.panel.config.matter.panel.prompts.passcode.input_label"
|
||||
),
|
||||
title: "Passcode",
|
||||
inputLabel: "Code",
|
||||
inputType: "password",
|
||||
confirmText: this.hass.localize(
|
||||
"ui.panel.config.matter.panel.prompts.passcode.confirm"
|
||||
),
|
||||
confirmText: "Set Wifi",
|
||||
});
|
||||
if (!psk) {
|
||||
return;
|
||||
@@ -162,16 +140,10 @@ export class MatterConfigDashboard extends LitElement {
|
||||
|
||||
private async _commission(): Promise<void> {
|
||||
const code = await showPromptDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.matter.panel.prompts.commission_device.title"
|
||||
),
|
||||
inputLabel: this.hass.localize(
|
||||
"ui.panel.config.matter.panel.prompts.commission_device.input_label"
|
||||
),
|
||||
title: "Commission device",
|
||||
inputLabel: "Code",
|
||||
inputType: "string",
|
||||
confirmText: this.hass.localize(
|
||||
"ui.panel.config.matter.panel.prompts.commission_device.confirm"
|
||||
),
|
||||
confirmText: "Commission",
|
||||
});
|
||||
if (!code) {
|
||||
return;
|
||||
@@ -188,16 +160,10 @@ export class MatterConfigDashboard extends LitElement {
|
||||
|
||||
private async _acceptSharedDevice(): Promise<void> {
|
||||
const code = await showPromptDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.matter.panel.prompts.add_shared_device.title"
|
||||
),
|
||||
inputLabel: this.hass.localize(
|
||||
"ui.panel.config.matter.panel.prompts.add_shared_device.input_label"
|
||||
),
|
||||
title: "Add shared device",
|
||||
inputLabel: "Pin",
|
||||
inputType: "number",
|
||||
confirmText: this.hass.localize(
|
||||
"ui.panel.config.matter.panel.prompts.add_shared_device.confirm"
|
||||
),
|
||||
confirmText: "Accept device",
|
||||
});
|
||||
if (!code) {
|
||||
return;
|
||||
@@ -214,16 +180,10 @@ export class MatterConfigDashboard extends LitElement {
|
||||
|
||||
private async _setThread(): Promise<void> {
|
||||
const code = await showPromptDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.matter.panel.prompts.set_thread.title"
|
||||
),
|
||||
inputLabel: this.hass.localize(
|
||||
"ui.panel.config.matter.panel.prompts.set_thread.input_label"
|
||||
),
|
||||
title: "Set Thread operation",
|
||||
inputLabel: "Dataset",
|
||||
inputType: "string",
|
||||
confirmText: this.hass.localize(
|
||||
"ui.panel.config.matter.panel.prompts.set_thread.confirm"
|
||||
),
|
||||
confirmText: "Set Thread",
|
||||
});
|
||||
if (!code) {
|
||||
return;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import type { RouterOptions } from "../../../../../layouts/hass-router-page";
|
||||
import { HassRouterPage } from "../../../../../layouts/hass-router-page";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
@@ -11,6 +12,10 @@ class ZHAConfigDashboardRouter extends HassRouterPage {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
private _configEntry = new URLSearchParams(window.location.search).get(
|
||||
"config_entry"
|
||||
);
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
defaultPage: "dashboard",
|
||||
showLoading: true,
|
||||
@@ -47,6 +52,7 @@ class ZHAConfigDashboardRouter extends HassRouterPage {
|
||||
el.hass = this.hass;
|
||||
el.isWide = this.isWide;
|
||||
el.narrow = this.narrow;
|
||||
el.configEntryId = this._configEntry;
|
||||
if (this._currentPage === "group") {
|
||||
el.groupId = this.routeTail.path.substr(1);
|
||||
} else if (this._currentPage === "device") {
|
||||
@@ -54,6 +60,17 @@ class ZHAConfigDashboardRouter extends HassRouterPage {
|
||||
} else if (this._currentPage === "visualization") {
|
||||
el.zoomedDeviceIdFromURL = this.routeTail.path.substr(1);
|
||||
}
|
||||
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
if (this._configEntry && !searchParams.has("config_entry")) {
|
||||
searchParams.append("config_entry", this._configEntry);
|
||||
navigate(
|
||||
`${this.routeTail.prefix}${
|
||||
this.routeTail.path
|
||||
}?${searchParams.toString()}`,
|
||||
{ replace: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ class ZHAConfigDashboard extends LitElement {
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@state() private _configEntry?: ConfigEntry;
|
||||
@property({ attribute: false }) public configEntryId?: string;
|
||||
|
||||
@state() private _configuration?: ZHAConfiguration;
|
||||
|
||||
@@ -95,7 +95,6 @@ class ZHAConfigDashboard extends LitElement {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (this.hass) {
|
||||
this.hass.loadBackendTranslation("config_panel", "zha", false);
|
||||
this._fetchConfigEntry();
|
||||
this._fetchConfiguration();
|
||||
this._fetchSettings();
|
||||
this._fetchDevicesAndUpdateStatus();
|
||||
@@ -111,6 +110,7 @@ class ZHAConfigDashboard extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.tabs=${zhaTabs}
|
||||
back-path="/config/integrations"
|
||||
has-fab
|
||||
>
|
||||
<div class="container">
|
||||
@@ -151,26 +151,28 @@ class ZHAConfigDashboard extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this._configEntry?.entry_id}`}
|
||||
appearance="plain"
|
||||
size="small"
|
||||
>
|
||||
${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}`}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.caption"
|
||||
)}</ha-button
|
||||
>
|
||||
</div>
|
||||
${this.configEntryId
|
||||
? html`<div class="card-actions">
|
||||
<ha-button
|
||||
href=${`/config/devices/dashboard?historyBack=1&config_entry=${this.configEntryId}`}
|
||||
appearance="plain"
|
||||
size="small"
|
||||
>
|
||||
${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.configEntryId}`}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.entities.caption"
|
||||
)}</ha-button
|
||||
>
|
||||
</div>`
|
||||
: ""}
|
||||
</ha-card>
|
||||
<ha-card
|
||||
class="network-settings"
|
||||
@@ -319,15 +321,6 @@ class ZHAConfigDashboard extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _fetchConfigEntry(): Promise<void> {
|
||||
const configEntries = await getConfigEntries(this.hass, {
|
||||
domain: "zha",
|
||||
});
|
||||
if (configEntries.length) {
|
||||
this._configEntry = configEntries[0];
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchConfiguration(): Promise<void> {
|
||||
this._configuration = await fetchZHAConfiguration(this.hass!);
|
||||
}
|
||||
@@ -406,11 +399,20 @@ class ZHAConfigDashboard extends LitElement {
|
||||
fileDownload(backupJSON, `${basename}.json`);
|
||||
}
|
||||
|
||||
private _openOptionFlow() {
|
||||
if (!this._configEntry) {
|
||||
private async _openOptionFlow() {
|
||||
if (!this.configEntryId) {
|
||||
return;
|
||||
}
|
||||
showOptionsFlowDialog(this, this._configEntry);
|
||||
|
||||
const configEntries: ConfigEntry[] = await getConfigEntries(this.hass, {
|
||||
domain: "zha",
|
||||
});
|
||||
|
||||
const configEntry = configEntries.find(
|
||||
(entry) => entry.entry_id === this.configEntryId
|
||||
);
|
||||
|
||||
showOptionsFlowDialog(this, configEntry!);
|
||||
}
|
||||
|
||||
private _dataChanged(ev) {
|
||||
|
||||
@@ -302,11 +302,10 @@ class DialogZWaveJSUpdateFirmwareNode extends LitElement {
|
||||
</div>
|
||||
${this._updateFinishedMessage!.success
|
||||
? html`<p>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.zwave_js.update_firmware.finished_status.done${localizationKeySuffix}`
|
||||
)}
|
||||
</p>
|
||||
${closeButton}`
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.zwave_js.update_firmware.finished_status.done${localizationKeySuffix}`
|
||||
)}
|
||||
</p>`
|
||||
: html`<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.update_firmware.finished_status.try_again"
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../../components/ha-card";
|
||||
import "../../../../../components/ha-icon-next";
|
||||
import "../../../../../components/ha-list";
|
||||
import "../../../../../components/ha-list-item";
|
||||
import "../../../../../layouts/hass-loading-screen";
|
||||
import "../../../../../layouts/hass-subpage";
|
||||
import type { ConfigEntry } from "../../../../../data/config_entries";
|
||||
import { getConfigEntries } from "../../../../../data/config_entries";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import { caseInsensitiveStringCompare } from "../../../../../common/string/compare";
|
||||
|
||||
@customElement("zwave_js-config-entry-picker")
|
||||
class ZWaveJSConfigEntryPicker extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _configEntries?: ConfigEntry[];
|
||||
|
||||
protected async firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
await this._fetchConfigEntries();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._configEntries) {
|
||||
return html`<hass-loading-screen></hass-loading-screen>`;
|
||||
}
|
||||
|
||||
if (this._configEntries.length === 0) {
|
||||
return html`
|
||||
<hass-subpage header="Z-Wave" .narrow=${this.narrow} .hass=${this.hass}>
|
||||
<div class="content">
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.picker.no_entries"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
</ha-card>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<hass-subpage header="Z-Wave" .narrow=${this.narrow} .hass=${this.hass}>
|
||||
<div class="content">
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.picker.title"
|
||||
)}
|
||||
>
|
||||
<ha-list>
|
||||
${this._configEntries.map(
|
||||
(entry) => html`
|
||||
<a
|
||||
href="/config/zwave_js/dashboard?config_entry=${entry.entry_id}"
|
||||
>
|
||||
<ha-list-item hasMeta>
|
||||
<span>${entry.title}</span>
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
</a>
|
||||
`
|
||||
)}
|
||||
</ha-list>
|
||||
</ha-card>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private async _fetchConfigEntries() {
|
||||
const entries = await getConfigEntries(this.hass, {
|
||||
domain: "zwave_js",
|
||||
});
|
||||
this._configEntries = entries.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a.title, b.title)
|
||||
);
|
||||
if (this._configEntries.length === 1) {
|
||||
navigate(
|
||||
`/config/zwave_js/dashboard?config_entry=${this._configEntries[0].entry_id}`,
|
||||
{ replace: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
css`
|
||||
.content {
|
||||
padding: 24px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-size: 20px;
|
||||
font-weight: 500;
|
||||
padding: 16px;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
ha-list {
|
||||
--md-list-item-leading-space: var(--ha-space-4);
|
||||
--md-list-item-trailing-space: var(--ha-space-4);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"zwave_js-config-entry-picker": ZWaveJSConfigEntryPicker;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@ import { customElement, property } from "lit/decorators";
|
||||
import type { RouterOptions } from "../../../../../layouts/hass-router-page";
|
||||
import { HassRouterPage } from "../../../../../layouts/hass-router-page";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
|
||||
import { getConfigEntries } from "../../../../../data/config_entries";
|
||||
|
||||
export const configTabs: PageNavigation[] = [
|
||||
{
|
||||
@@ -31,36 +33,14 @@ class ZWaveJSConfigRouter extends HassRouterPage {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
private _configEntry: string | null = null;
|
||||
private _configEntry = new URLSearchParams(window.location.search).get(
|
||||
"config_entry"
|
||||
);
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
defaultPage: "picker",
|
||||
defaultPage: "dashboard",
|
||||
showLoading: true,
|
||||
// Make sure that we have a config entry in the URL before rendering other pages
|
||||
beforeRender: (page) => {
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
if (searchParams.has("config_entry")) {
|
||||
this._configEntry = searchParams.get("config_entry");
|
||||
} else if (page === "picker") {
|
||||
this._configEntry = null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if ((!page || page === "picker") && this._configEntry) {
|
||||
return "dashboard";
|
||||
}
|
||||
|
||||
if ((!page || page !== "picker") && !this._configEntry) {
|
||||
return "picker";
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
routes: {
|
||||
picker: {
|
||||
tag: "zwave_js-config-entry-picker",
|
||||
load: () => import("./zwave_js-config-entry-picker"),
|
||||
},
|
||||
dashboard: {
|
||||
tag: "zwave_js-config-dashboard",
|
||||
load: () => import("./zwave_js-config-dashboard"),
|
||||
@@ -90,6 +70,7 @@ class ZWaveJSConfigRouter extends HassRouterPage {
|
||||
load: () => import("./zwave_js-network-visualization"),
|
||||
},
|
||||
},
|
||||
initialLoad: () => this._fetchConfigEntries(),
|
||||
};
|
||||
|
||||
protected updatePageEl(el): void {
|
||||
@@ -98,6 +79,29 @@ class ZWaveJSConfigRouter extends HassRouterPage {
|
||||
el.isWide = this.isWide;
|
||||
el.narrow = this.narrow;
|
||||
el.configEntryId = this._configEntry;
|
||||
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
if (this._configEntry && !searchParams.has("config_entry")) {
|
||||
searchParams.append("config_entry", this._configEntry);
|
||||
navigate(
|
||||
`${this.routeTail.prefix}${
|
||||
this.routeTail.path
|
||||
}?${searchParams.toString()}`,
|
||||
{ replace: true }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchConfigEntries() {
|
||||
if (this._configEntry) {
|
||||
return;
|
||||
}
|
||||
const entries = await getConfigEntries(this.hass, {
|
||||
domain: "zwave_js",
|
||||
});
|
||||
if (entries.length) {
|
||||
this._configEntry = entries[0].entry_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ import "../../../components/ha-state-icon";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import { createAreaRegistryEntry } from "../../../data/area_registry";
|
||||
import { createAreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type { CategoryRegistryEntry } from "../../../data/category_registry";
|
||||
import {
|
||||
createCategoryRegistryEntry,
|
||||
|
||||
@@ -61,7 +61,7 @@ import "../../../components/ha-md-menu-item";
|
||||
import "../../../components/ha-sub-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import { createAreaRegistryEntry } from "../../../data/area_registry";
|
||||
import { createAreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import type { CategoryRegistryEntry } from "../../../data/category_registry";
|
||||
import {
|
||||
createCategoryRegistryEntry,
|
||||
|
||||
@@ -25,8 +25,6 @@ class HaPanelDevEvent extends LitElement {
|
||||
|
||||
@state() private _isValid = true;
|
||||
|
||||
@state() private _selectedEventType = "";
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div
|
||||
@@ -91,10 +89,7 @@ class HaPanelDevEvent extends LitElement {
|
||||
</div>
|
||||
</ha-card>
|
||||
|
||||
<event-subscribe-card
|
||||
.hass=${this.hass}
|
||||
.selectedEventType=${this._selectedEventType}
|
||||
></event-subscribe-card>
|
||||
<event-subscribe-card .hass=${this.hass}></event-subscribe-card>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -114,7 +109,6 @@ class HaPanelDevEvent extends LitElement {
|
||||
|
||||
private _eventSelected(ev) {
|
||||
this._eventType = ev.detail.eventType;
|
||||
this._selectedEventType = ev.detail.eventType;
|
||||
}
|
||||
|
||||
private _eventTypeChanged(ev) {
|
||||
|
||||
@@ -15,8 +15,6 @@ import type { HomeAssistant } from "../../../types";
|
||||
class EventSubscribeCard extends LitElement {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public selectedEventType = "";
|
||||
|
||||
@state() private _eventType = "";
|
||||
|
||||
@state() private _subscribed?: () => void;
|
||||
@@ -38,18 +36,6 @@ class EventSubscribeCard extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: Map<string, any>) {
|
||||
super.willUpdate(changedProperties);
|
||||
|
||||
if (
|
||||
changedProperties.has("selectedEventType") &&
|
||||
this.selectedEventType &&
|
||||
!this._subscribed
|
||||
) {
|
||||
this._eventType = this.selectedEventType;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-card
|
||||
|
||||
@@ -16,7 +16,7 @@ import "../../../components/ha-control-button";
|
||||
import "../../../components/ha-control-button-group";
|
||||
import "../../../components/ha-domain-icon";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { AreaRegistryEntry } from "../../../data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../data/area/area_registry";
|
||||
import { forwardHaptic } from "../../../data/haptics";
|
||||
import { computeCssVariable } from "../../../resources/css-variables";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
|
||||
@@ -30,33 +30,35 @@ import {
|
||||
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";
|
||||
|
||||
export function getSuggestedMax(period: StatisticPeriod, end: Date): number {
|
||||
export function getSuggestedMax(
|
||||
dayDifference: number,
|
||||
end: Date,
|
||||
detailedDailyData = false
|
||||
): number {
|
||||
let suggestedMax = new Date(end);
|
||||
|
||||
if (period === "5minute") {
|
||||
return suggestedMax.getTime();
|
||||
}
|
||||
suggestedMax.setMinutes(0, 0, 0);
|
||||
if (period === "hour") {
|
||||
return suggestedMax.getTime();
|
||||
}
|
||||
// Sometimes around DST we get a time of 0:59 instead of 23:59 as expected.
|
||||
// Correct for this when showing days/months so we don't get an extra day.
|
||||
if (suggestedMax.getHours() === 0) {
|
||||
if (dayDifference > 2 && suggestedMax.getHours() === 0) {
|
||||
suggestedMax = subHours(suggestedMax, 1);
|
||||
}
|
||||
suggestedMax.setHours(0);
|
||||
if (period === "day" || period === "week") {
|
||||
return suggestedMax.getTime();
|
||||
|
||||
if (!detailedDailyData) {
|
||||
suggestedMax.setMinutes(0, 0, 0);
|
||||
}
|
||||
if (dayDifference > 35) {
|
||||
suggestedMax.setDate(1);
|
||||
}
|
||||
if (dayDifference > 2) {
|
||||
suggestedMax.setHours(0);
|
||||
}
|
||||
// period === month
|
||||
suggestedMax.setDate(1);
|
||||
return suggestedMax.getTime();
|
||||
}
|
||||
|
||||
export function getSuggestedPeriod(dayDifference: number): StatisticPeriod {
|
||||
export function getSuggestedPeriod(
|
||||
dayDifference: number
|
||||
): "month" | "day" | "hour" {
|
||||
return dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour";
|
||||
}
|
||||
|
||||
@@ -94,10 +96,7 @@ export function getCommonOptions(
|
||||
xAxis: {
|
||||
type: "time",
|
||||
min: start,
|
||||
max: getSuggestedMax(
|
||||
detailedDailyData ? "5minute" : getSuggestedPeriod(dayDifference),
|
||||
end
|
||||
),
|
||||
max: getSuggestedMax(dayDifference, end, detailedDailyData),
|
||||
},
|
||||
yAxis: {
|
||||
type: "value",
|
||||
|
||||
@@ -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>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -196,10 +196,6 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("hass") || changedProps.has("_config")) {
|
||||
this._computeNames();
|
||||
}
|
||||
|
||||
if (!this._config || !changedProps.has("_config")) {
|
||||
return;
|
||||
}
|
||||
@@ -229,6 +225,10 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProps.has("hass")) {
|
||||
this._computeNames();
|
||||
}
|
||||
|
||||
if (
|
||||
changedProps.has("_config") &&
|
||||
oldConfig?.entities !== this._config.entities
|
||||
@@ -334,7 +334,10 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
|
||||
.maxYAxis=${this._config.max_y_axis}
|
||||
.startTime=${this._energyStart}
|
||||
.endTime=${this._energyEnd && this._energyStart
|
||||
? getSuggestedMax(this._period!, this._energyEnd)
|
||||
? getSuggestedMax(
|
||||
differenceInDays(this._energyEnd, this._energyStart),
|
||||
this._energyEnd
|
||||
)
|
||||
: undefined}
|
||||
.fitYData=${this._config.fit_y_data || false}
|
||||
.hideLegend=${this._config.hide_legend || false}
|
||||
|
||||
@@ -242,7 +242,6 @@ export class HuiClockCardEditor
|
||||
selector: {
|
||||
select: {
|
||||
mode: "dropdown",
|
||||
picker: true,
|
||||
options: [
|
||||
[
|
||||
"auto",
|
||||
|
||||
@@ -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! });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ import "../../components/ha-svg-icon";
|
||||
import "../../components/ha-tab-group";
|
||||
import "../../components/ha-tab-group-tab";
|
||||
import "../../components/ha-tooltip";
|
||||
import { createAreaRegistryEntry } from "../../data/area_registry";
|
||||
import { createAreaRegistryEntry } from "../../data/area/area_registry";
|
||||
import type { LovelacePanelConfig } from "../../data/lovelace";
|
||||
import type {
|
||||
LovelaceConfig,
|
||||
@@ -72,10 +72,7 @@ import {
|
||||
showConfirmationDialog,
|
||||
} from "../../dialogs/generic/show-dialog-box";
|
||||
import { showMoreInfoDialog } from "../../dialogs/more-info/show-ha-more-info-dialog";
|
||||
import {
|
||||
QuickBarMode,
|
||||
showQuickBar,
|
||||
} from "../../dialogs/quick-bar/show-dialog-quick-bar";
|
||||
import { showQuickBar } from "../../dialogs/quick-bar/show-dialog-quick-bar";
|
||||
import { showShortcutsDialog } from "../../dialogs/shortcuts/show-shortcuts-dialog";
|
||||
import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
@@ -263,8 +260,7 @@ class HUIRoot extends LitElement {
|
||||
{
|
||||
icon: mdiPlus,
|
||||
key: "ui.panel.lovelace.menu.add",
|
||||
visible:
|
||||
!this._editMode && this.hass.user?.is_admin && !this.hass.kioskMode,
|
||||
visible: !this._editMode && this.hass.user?.is_admin,
|
||||
overflow: this.narrow,
|
||||
subItems: [
|
||||
{
|
||||
@@ -299,13 +295,11 @@ class HUIRoot extends LitElement {
|
||||
},
|
||||
{
|
||||
icon: mdiMagnify,
|
||||
key: "ui.panel.lovelace.menu.search_entities",
|
||||
key: "ui.common.search",
|
||||
buttonAction: this._showQuickBar,
|
||||
overflowAction: this._handleShowQuickBar,
|
||||
visible: !this._editMode && !this.hass.kioskMode,
|
||||
visible: !this._editMode,
|
||||
overflow: this.narrow,
|
||||
suffix:
|
||||
this.hass.enableShortcuts && !isMobileClient ? "(E)" : undefined,
|
||||
},
|
||||
{
|
||||
icon: mdiCommentProcessingOutline,
|
||||
@@ -350,8 +344,7 @@ class HUIRoot extends LitElement {
|
||||
visible:
|
||||
!this._editMode &&
|
||||
this.hass!.user?.is_admin &&
|
||||
!this.hass!.config.recovery_mode &&
|
||||
!this.hass.kioskMode,
|
||||
!this.hass!.config.recovery_mode,
|
||||
overflow: true,
|
||||
overflow_can_promote: true,
|
||||
},
|
||||
@@ -905,7 +898,6 @@ class HUIRoot extends LitElement {
|
||||
};
|
||||
|
||||
showQuickBar(this, {
|
||||
mode: QuickBarMode.Entity,
|
||||
hint: this.hass.enableShortcuts
|
||||
? this.hass.localize("ui.tips.key_e_tip", params)
|
||||
: undefined,
|
||||
|
||||
@@ -13,11 +13,12 @@ import "../../../../../components/ha-entities-display-editor";
|
||||
import "../../../../../components/ha-icon";
|
||||
import "../../../../../components/ha-icon-button";
|
||||
import "../../../../../components/ha-icon-button-prev";
|
||||
import type { DisplayItem } from "../../../../../components/ha-items-display-editor";
|
||||
import "../../../../../components/ha-svg-icon";
|
||||
import {
|
||||
updateAreaRegistryEntry,
|
||||
type AreaRegistryEntry,
|
||||
} from "../../../../../data/area_registry";
|
||||
} from "../../../../../data/area/area_registry";
|
||||
import {
|
||||
haCardSizeLarge,
|
||||
haCardSizeSmall,
|
||||
@@ -33,7 +34,6 @@ import {
|
||||
AREA_STRATEGY_GROUPS,
|
||||
getAreaGroupedEntities,
|
||||
} from "../helpers/areas-strategy-helper";
|
||||
import type { DisplayItem } from "../../../../../components/ha-items-display-editor";
|
||||
|
||||
@customElement("hui-areas-dashboard-strategy-editor")
|
||||
export class HuiAreasDashboardStrategyEditor
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { EntityFilterFunc } from "../../../../../common/entity/entity_filte
|
||||
import { generateEntityFilter } from "../../../../../common/entity/entity_filter";
|
||||
import { stripPrefixFromEntityName } from "../../../../../common/entity/strip_prefix_from_entity_name";
|
||||
import { orderCompare } from "../../../../../common/string/compare";
|
||||
import type { AreaRegistryEntry } from "../../../../../data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../../../data/area/area_registry";
|
||||
import type { FloorRegistryEntry } from "../../../../../data/floor_registry";
|
||||
import type { LovelaceCardConfig } from "../../../../../data/lovelace/config/card";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
generateEntityFilter,
|
||||
} from "../../../../common/entity/entity_filter";
|
||||
import { floorDefaultIcon } from "../../../../components/ha-floor-icon";
|
||||
import type { AreaRegistryEntry } from "../../../../data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../../data/area/area_registry";
|
||||
import { getEnergyPreferences } from "../../../../data/energy";
|
||||
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import type {
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { computeStateName } from "../common/entity/compute_state_name";
|
||||
import { promiseTimeout } from "../common/util/promise-timeout";
|
||||
import { subscribeAreaRegistry } from "../data/area_registry";
|
||||
import { subscribeAreaRegistry } from "../data/area/area_registry";
|
||||
import { broadcastConnectionStatus } from "../data/connection-status";
|
||||
import { subscribeDeviceRegistry } from "../data/device/device_registry";
|
||||
import {
|
||||
@@ -77,7 +77,6 @@ export const connectionMixin = <T extends Constructor<HassBaseEl>>(
|
||||
resources: null as any,
|
||||
localize: () => "",
|
||||
translationMetadata,
|
||||
kioskMode: false,
|
||||
dockedSidebar: "docked",
|
||||
vibrate: true,
|
||||
debugConnection: __DEV__,
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
|
||||
import { canOverrideAlphanumericInput } from "../common/dom/can-override-input";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import type { QuickBarParams } from "../dialogs/quick-bar/show-dialog-quick-bar";
|
||||
import {
|
||||
QuickBarMode,
|
||||
showQuickBar,
|
||||
import { ShortcutManager } from "../common/keyboard/shortcuts";
|
||||
import { extractSearchParamsObject } from "../common/url/search-params";
|
||||
import type {
|
||||
QuickBarParams,
|
||||
QuickBarSection,
|
||||
} from "../dialogs/quick-bar/show-dialog-quick-bar";
|
||||
import { showQuickBar } from "../dialogs/quick-bar/show-dialog-quick-bar";
|
||||
import { showShortcutsDialog } from "../dialogs/shortcuts/show-shortcuts-dialog";
|
||||
import { showVoiceCommandDialog } from "../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
|
||||
import type { Redirects } from "../panels/my/ha-panel-my";
|
||||
import type { Constructor, HomeAssistant } from "../types";
|
||||
import { storeState } from "../util/ha-pref-storage";
|
||||
import { showToast } from "../util/toast";
|
||||
import type { HassElement } from "./hass-element";
|
||||
import { ShortcutManager } from "../common/keyboard/shortcuts";
|
||||
import { extractSearchParamsObject } from "../common/url/search-params";
|
||||
import { showVoiceCommandDialog } from "../dialogs/voice-command-dialog/show-ha-voice-command-dialog";
|
||||
import { canOverrideAlphanumericInput } from "../common/dom/can-override-input";
|
||||
import { showShortcutsDialog } from "../dialogs/shortcuts/show-shortcuts-dialog";
|
||||
import type { Redirects } from "../panels/my/ha-panel-my";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
@@ -39,13 +39,13 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
|
||||
mainWindow.addEventListener("hass-quick-bar-trigger", (ev) => {
|
||||
switch (ev.detail.key) {
|
||||
case "e":
|
||||
this._showQuickBar(ev.detail);
|
||||
this._showQuickBar(ev.detail, "entity");
|
||||
break;
|
||||
case "c":
|
||||
this._showQuickBar(ev.detail, QuickBarMode.Command);
|
||||
this._showQuickBar(ev.detail, "command");
|
||||
break;
|
||||
case "d":
|
||||
this._showQuickBar(ev.detail, QuickBarMode.Device);
|
||||
this._showQuickBar(ev.detail, "device");
|
||||
break;
|
||||
case "m":
|
||||
this._createMyLink(ev.detail);
|
||||
@@ -65,19 +65,21 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
|
||||
const shortcutManager = new ShortcutManager();
|
||||
shortcutManager.add({
|
||||
// Those are for latin keyboards that have e, c, m keys
|
||||
e: { handler: (ev) => this._showQuickBar(ev) },
|
||||
c: { handler: (ev) => this._showQuickBar(ev, QuickBarMode.Command) },
|
||||
e: { handler: (ev) => this._showQuickBar(ev, "entity") },
|
||||
c: { handler: (ev) => this._showQuickBar(ev, "command") },
|
||||
m: { handler: (ev) => this._createMyLink(ev) },
|
||||
a: { handler: (ev) => this._showVoiceCommandDialog(ev) },
|
||||
d: { handler: (ev) => this._showQuickBar(ev, QuickBarMode.Device) },
|
||||
d: { handler: (ev) => this._showQuickBar(ev, "device") },
|
||||
"$mod+k": { handler: (ev) => this._showQuickBar(ev) },
|
||||
// Workaround see https://github.com/jamiebuilds/tinykeys/issues/130
|
||||
"Shift+?": { handler: (ev) => this._showShortcutDialog(ev) },
|
||||
// Those are fallbacks for non-latin keyboards that don't have e, c, m keys (qwerty-based shortcuts)
|
||||
KeyE: { handler: (ev) => this._showQuickBar(ev) },
|
||||
KeyC: { handler: (ev) => this._showQuickBar(ev, QuickBarMode.Command) },
|
||||
KeyE: { handler: (ev) => this._showQuickBar(ev, "entity") },
|
||||
KeyC: { handler: (ev) => this._showQuickBar(ev, "command") },
|
||||
KeyM: { handler: (ev) => this._createMyLink(ev) },
|
||||
KeyA: { handler: (ev) => this._showVoiceCommandDialog(ev) },
|
||||
KeyD: { handler: (ev) => this._showQuickBar(ev, QuickBarMode.Device) },
|
||||
KeyD: { handler: (ev) => this._showQuickBar(ev, "device") },
|
||||
"$mod+KeyK": { handler: (ev) => this._showQuickBar(ev) },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -102,10 +104,7 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
|
||||
showVoiceCommandDialog(this, this.hass!, { pipeline_id: "last_used" });
|
||||
}
|
||||
|
||||
private _showQuickBar(
|
||||
e: KeyboardEvent,
|
||||
mode: QuickBarMode = QuickBarMode.Entity
|
||||
) {
|
||||
private _showQuickBar(e: KeyboardEvent, mode?: QuickBarSection) {
|
||||
if (!this._canShowQuickBar(e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -11,12 +11,10 @@ declare global {
|
||||
// for fire event
|
||||
interface HASSDomEvents {
|
||||
"hass-dock-sidebar": DockSidebarParams;
|
||||
"hass-kiosk-mode": { enable: boolean };
|
||||
}
|
||||
// for add event listener
|
||||
interface HTMLElementEventMap {
|
||||
"hass-dock-sidebar": HASSDomEvent<DockSidebarParams>;
|
||||
"hass-kiosk-mode": HASSDomEvent<HASSDomEvents["hass-kiosk-mode"]>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,8 +26,5 @@ export default <T extends Constructor<HassBaseEl>>(superClass: T) =>
|
||||
this._updateHass({ dockedSidebar: ev.detail.dock });
|
||||
storeState(this.hass!);
|
||||
});
|
||||
window.addEventListener("hass-kiosk-mode", (ev) => {
|
||||
this._updateHass({ kioskMode: ev.detail.enable });
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -137,9 +137,9 @@
|
||||
},
|
||||
"counter": {
|
||||
"actions": {
|
||||
"increment": "Increment",
|
||||
"decrement": "Decrement",
|
||||
"reset": "Reset"
|
||||
"increment": "increment",
|
||||
"decrement": "decrement",
|
||||
"reset": "reset"
|
||||
}
|
||||
},
|
||||
"cover": {
|
||||
@@ -672,8 +672,8 @@
|
||||
"device_missing": "No related device"
|
||||
},
|
||||
"add": "Add",
|
||||
"search": "Search or enter custom name",
|
||||
"custom_name": "Custom name"
|
||||
"custom_name": "Custom name",
|
||||
"no_match": "No entities found"
|
||||
},
|
||||
"entity-attribute-picker": {
|
||||
"attribute": "Attribute",
|
||||
@@ -685,7 +685,7 @@
|
||||
},
|
||||
"entity-state-content-picker": {
|
||||
"add": "Add",
|
||||
"custom_attribute": "Custom attribute"
|
||||
"custom_state": "Custom state"
|
||||
}
|
||||
},
|
||||
"target-picker": {
|
||||
@@ -772,6 +772,9 @@
|
||||
"no_match": "No languages found for {term}",
|
||||
"no_languages": "No languages available"
|
||||
},
|
||||
"icon-picker": {
|
||||
"no_match": "No matching icons found"
|
||||
},
|
||||
"tts-picker": {
|
||||
"tts": "Text-to-speech",
|
||||
"none": "None"
|
||||
@@ -1335,6 +1338,8 @@
|
||||
"text": "Home Assistant is running in safe mode, custom integrations and frontend modules are not available. Restart Home Assistant to exit safe mode."
|
||||
},
|
||||
"quick-bar": {
|
||||
"commands_title": "Commands",
|
||||
"navigate_title": "Navigate",
|
||||
"commands": {
|
||||
"reload": {
|
||||
"all": "[%key:ui::panel::developer-tools::tabs::yaml::section::reloading::all%]",
|
||||
@@ -2145,6 +2150,7 @@
|
||||
"title": "Searching",
|
||||
"on_any_page": "On any page",
|
||||
"on_pages_with_tables": "On pages with tables",
|
||||
"search": "Search Home Assistant",
|
||||
"search_command": "search command",
|
||||
"search_entities": "search entities",
|
||||
"search_devices": "search devices",
|
||||
@@ -2330,27 +2336,6 @@
|
||||
},
|
||||
"cloud": {
|
||||
"secondary": "Loading..."
|
||||
},
|
||||
"zwave_js": {
|
||||
"secondary": "Sub-GHz mesh protocol"
|
||||
},
|
||||
"zha": {
|
||||
"secondary": "Low-power mesh network"
|
||||
},
|
||||
"matter": {
|
||||
"secondary": "Cross-vendor smart home standard"
|
||||
},
|
||||
"thread": {
|
||||
"secondary": "Mesh network often used for Matter devices"
|
||||
},
|
||||
"bluetooth": {
|
||||
"secondary": "Local device connectivity"
|
||||
},
|
||||
"knx": {
|
||||
"secondary": "Building automation standard"
|
||||
},
|
||||
"insteon": {
|
||||
"secondary": "Dual-mesh home automation"
|
||||
}
|
||||
},
|
||||
"common": {
|
||||
@@ -4087,7 +4072,7 @@
|
||||
"new_automation_setup_failed_title": "New {type} setup timed out",
|
||||
"new_automation_setup_failed_text": "Your new {type} was saved, but waiting for it to set up has timed out. This could be due to errors parsing your configuration.yaml, please check the configuration in developer tools. Your {type} will not be visible until this is corrected and {types} are reloaded. Changes to area, category, or labels were not saved and must be reapplied.",
|
||||
"new_automation_setup_keep_waiting": "You may continue to wait for a response from the server, in case it is just taking an unusually long time to process this {type}.",
|
||||
"new_automation_setup_timedout_success": "The server has responded and this has now set up successfully. You may now close this dialog.",
|
||||
"new_automation_setup_timedout_success": "The server has responded and this has now setup successfully. You may now close this dialog.",
|
||||
"item_pasted": "{item} pasted",
|
||||
"ctrl": "Ctrl",
|
||||
"del": "Del",
|
||||
@@ -4116,8 +4101,7 @@
|
||||
"targets": "{count} {count, plural,\n one {target}\n other {targets}\n}",
|
||||
"invalid": "Invalid target",
|
||||
"all_entities": "All entities",
|
||||
"none_entities": "No entities",
|
||||
"template": "Template"
|
||||
"none_entities": "No entities"
|
||||
},
|
||||
"triggers": {
|
||||
"name": "Triggers",
|
||||
@@ -6011,31 +5995,26 @@
|
||||
},
|
||||
"bluetooth": {
|
||||
"title": "Bluetooth",
|
||||
"settings_title": "Bluetooth adapters",
|
||||
"settings_title": "Bluetooth settings",
|
||||
"option_flow": "Configure Bluetooth options",
|
||||
"advertisement_monitor": "Advertisement monitor",
|
||||
"advertisement_monitor_details": "The advertisement monitor listens for Bluetooth advertisements and displays the data in a structured format.",
|
||||
"connection_slot_allocations_monitor": "Connection slot allocations monitor",
|
||||
"connection_slot_allocations_monitor_description": "The connection slot allocations monitor displays the (GATT) connection slot allocations for each adapter. Each remote Bluetooth device that requires an active connection will use one connection slot while the Bluetooth device is connecting or connected.",
|
||||
"connection_slot_allocations_monitor_details": "The connection slot allocations monitor displays the (GATT) connection slot allocations for the adapter. This adapter supports up to {slots} simultaneous connections. Each remote Bluetooth device that requires an active connection will use one connection slot while the Bluetooth device is connecting or connected.",
|
||||
"connection_monitor": "Connection monitor",
|
||||
"visualization": "Visualization",
|
||||
"used_connection_slot_allocations": "Used connection slot allocations",
|
||||
"no_connections": "No active connections",
|
||||
"active_connections": "connections",
|
||||
"no_advertisements_found": "No matching Bluetooth advertisements found",
|
||||
"no_connection_slot_allocations": "No connection slot allocations information available",
|
||||
"no_connection_slots": "No connection slots",
|
||||
"no_active_connection_support": "This adapter does not support making active (GATT) connections.",
|
||||
"no_scanner_state_available": "No scanner state available",
|
||||
"scanner_state_unknown": "State unknown",
|
||||
"current_scanning_mode": "Current scanning mode",
|
||||
"requested_scanning_mode": "Requested scanning mode",
|
||||
"scanning_mode_none": "none",
|
||||
"scanning_mode_active": "active",
|
||||
"scanning_mode_passive": "passive",
|
||||
"scanning_mode_active_label": "Active scanning",
|
||||
"scanning_mode_passive_label": "Passive scanning",
|
||||
"scanning_mode_none_label": "No scanning",
|
||||
"scanner_mode_mismatch": "{name} requested {requested} mode but is operating in {current} mode. The scanner is in a bad state and needs to be power cycled.",
|
||||
"scanner_mode_mismatch": "Scanner requested {requested} mode but is operating in {current} mode. The scanner is in a bad state and needs to be power cycled.",
|
||||
"scanner_mode_mismatch_remote": "For proxies: reboot the device",
|
||||
"scanner_mode_mismatch_usb": "For USB adapters: unplug and plug back in",
|
||||
"scanner_mode_mismatch_uart": "For UART/onboard adapters: power down the system completely and power it back up",
|
||||
@@ -6256,7 +6235,7 @@
|
||||
"new_channel": "New channel",
|
||||
"change_channel": "Change channel",
|
||||
"migration_warning": "Zigbee channel migration is an experimental feature and relies on devices on your network to support it. Device support for this feature varies and only a portion of your network may end up migrating! It may take up to an hour for changes to propagate to all devices.",
|
||||
"description": "Change your Zigbee channel only after you have eliminated all other sources of 2.4GHz interference by using a USB extension cable and moving your coordinator away from USB 3.0 devices and ports, SSDs, 2.4GHz Wi-Fi networks on the same channel, motherboards, and so on.",
|
||||
"description": "Change your Zigbee channel only after you have eliminated all other sources of 2.4GHz interference by using a USB extension cable and moving your coordinator away from USB 3.0 devices and ports, SSDs, 2.4GHz WiFi networks on the same channel, motherboards, and so on.",
|
||||
"smart_explanation": "It is recommended to use the \"Smart\" option once your environment is optimized as opposed to manually choosing a channel, as it picks the best channel for you after scanning all Zigbee channels. This does not configure ZHA to automatically change channels in the future, it only changes the channel a single time.",
|
||||
"channel_has_been_changed": "Network channel has been changed",
|
||||
"devices_will_rejoin": "Devices will re-join the network over time. This may take a few minutes.",
|
||||
@@ -6830,50 +6809,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"picker": {
|
||||
"title": "Select Z-Wave network",
|
||||
"no_entries": "No Z-Wave networks configured. Set up the Z-Wave JS integration first."
|
||||
}
|
||||
},
|
||||
"matter": {
|
||||
"panel": {
|
||||
"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",
|
||||
"set_wifi_credentials": "Set Wi-Fi credentials",
|
||||
"set_thread_credentials": "Set Thread credentials",
|
||||
"prompts": {
|
||||
"network_name": {
|
||||
"title": "Network name",
|
||||
"input_label": "Network name",
|
||||
"confirm": "Continue"
|
||||
},
|
||||
"passcode": {
|
||||
"title": "Passcode",
|
||||
"input_label": "Code",
|
||||
"confirm": "Set Wi-Fi"
|
||||
},
|
||||
"commission_device": {
|
||||
"title": "Commission device",
|
||||
"input_label": "Code",
|
||||
"confirm": "Commission"
|
||||
},
|
||||
"add_shared_device": {
|
||||
"title": "Add shared device",
|
||||
"input_label": "PIN",
|
||||
"confirm": "Accept device"
|
||||
},
|
||||
"set_thread": {
|
||||
"title": "Set Thread operation",
|
||||
"input_label": "Dataset",
|
||||
"confirm": "Set Thread"
|
||||
}
|
||||
}
|
||||
},
|
||||
"network_type": {
|
||||
"thread": "Thread",
|
||||
"wifi": "Wi-Fi",
|
||||
@@ -7348,7 +7286,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"
|
||||
|
||||
@@ -14,7 +14,7 @@ import type {
|
||||
EntityNameOptions,
|
||||
} from "./common/entity/compute_entity_name_display";
|
||||
import type { LocalizeFunc } from "./common/translations/localize";
|
||||
import type { AreaRegistryEntry } from "./data/area_registry";
|
||||
import type { AreaRegistryEntry } from "./data/area/area_registry";
|
||||
import type { DeviceRegistryEntry } from "./data/device/device_registry";
|
||||
import type { EntityRegistryDisplayEntry } from "./data/entity/entity_registry";
|
||||
import type { FloorRegistryEntry } from "./data/floor_registry";
|
||||
@@ -250,7 +250,6 @@ export interface HomeAssistant {
|
||||
enableShortcuts: boolean;
|
||||
vibrate: boolean;
|
||||
debugConnection: boolean;
|
||||
kioskMode: boolean;
|
||||
dockedSidebar: "docked" | "always_hidden" | "auto";
|
||||
moreInfoEntityId: string | null;
|
||||
user?: CurrentUser;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { computeAreaName } from "../../../src/common/entity/compute_area_name";
|
||||
import type { AreaRegistryEntry } from "../../../src/data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../src/data/area/area_registry";
|
||||
|
||||
describe("computeAreaName", () => {
|
||||
it("returns the trimmed name if present", () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area_registry";
|
||||
import type { AreaRegistryEntry } from "../../../../src/data/area/area_registry";
|
||||
import type { DeviceRegistryEntry } from "../../../../src/data/device/device_registry";
|
||||
import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
|
||||
Reference in New Issue
Block a user