mirror of
https://github.com/home-assistant/frontend.git
synced 2026-01-10 01:07:32 +00:00
Compare commits
43 Commits
rc
...
bluetooth_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c492e84e58 | ||
|
|
e39ab6dfe4 | ||
|
|
76f43676d5 | ||
|
|
b84a51235d | ||
|
|
602d6a2337 | ||
|
|
6e614cd3f2 | ||
|
|
6793edd68b | ||
|
|
ad6e3267c3 | ||
|
|
f941117ca4 | ||
|
|
aef0bf03e3 | ||
|
|
f22f6b74db | ||
|
|
913c4ae24e | ||
|
|
4b7b5fa21a | ||
|
|
bf6887541b | ||
|
|
26da9f3a37 | ||
|
|
d48520efdf | ||
|
|
d462356122 | ||
|
|
9a5cdb0a99 | ||
|
|
eaf012d5ff | ||
|
|
19934dad72 | ||
|
|
6194f73442 | ||
|
|
dbc880fe35 | ||
|
|
be4e46a3c6 | ||
|
|
2fce89a689 | ||
|
|
81d21b0907 | ||
|
|
65381b1dc5 | ||
|
|
7cbede2f6e | ||
|
|
0a13dddaea | ||
|
|
662be980e8 | ||
|
|
209abf466d | ||
|
|
db9a3bd562 | ||
|
|
36ecaa6610 | ||
|
|
4f46d0f4a3 | ||
|
|
42ad47649d | ||
|
|
c62ee6e692 | ||
|
|
b38c8d7d5f | ||
|
|
83bcc39d5f | ||
|
|
8d317d1e2c | ||
|
|
9acad2e83c | ||
|
|
9099c5a92c | ||
|
|
60c4d60d66 | ||
|
|
e8a4cde643 | ||
|
|
148eab31b6 |
4
.github/copilot-instructions.md
vendored
4
.github/copilot-instructions.md
vendored
@@ -22,11 +22,13 @@ 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
|
||||
yarn lint:types # TypeScript compiler (run WITHOUT file arguments)
|
||||
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@c45aaa919ef85620af54242a241ac17a8fa35983 # v3.2.1
|
||||
uses: relative-ci/agent-action@3c681926017930047fc03acaa35cd6a44efcbfc3 # v3.2.2
|
||||
with:
|
||||
key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
|
||||
token: ${{ github.token }}
|
||||
|
||||
@@ -187,5 +187,11 @@ export default tseslint.config(
|
||||
],
|
||||
"no-use-before-define": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/util/recorder-worklet.js"],
|
||||
languageOptions: {
|
||||
globals: globals.audioWorklet,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
40
package.json
40
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.7",
|
||||
"@codemirror/view": "6.39.8",
|
||||
"@date-fns/tz": "1.4.1",
|
||||
"@egjs/hammerjs": "2.0.17",
|
||||
"@formatjs/intl-datetimeformat": "7.1.0",
|
||||
"@formatjs/intl-displaynames": "7.1.0",
|
||||
"@formatjs/intl-durationformat": "0.9.0",
|
||||
"@formatjs/intl-getcanonicallocales": "3.1.0",
|
||||
"@formatjs/intl-listformat": "8.1.0",
|
||||
"@formatjs/intl-locale": "5.1.0",
|
||||
"@formatjs/intl-numberformat": "9.1.0",
|
||||
"@formatjs/intl-pluralrules": "6.1.0",
|
||||
"@formatjs/intl-relativetimeformat": "12.1.0",
|
||||
"@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",
|
||||
"@fullcalendar/core": "6.1.20",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"@fullcalendar/interaction": "6.1.20",
|
||||
@@ -112,13 +112,13 @@
|
||||
"hls.js": "1.6.15",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "11.0.7",
|
||||
"intl-messageformat": "11.0.8",
|
||||
"js-yaml": "4.1.1",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
|
||||
"leaflet.markercluster": "1.5.3",
|
||||
"lit": "3.3.2",
|
||||
"lit-html": "3.3.2",
|
||||
"lit": "3.3.1",
|
||||
"lit-html": "3.3.1",
|
||||
"luxon": "3.7.2",
|
||||
"marked": "17.0.1",
|
||||
"memoize-one": "6.0.0",
|
||||
@@ -156,8 +156,8 @@
|
||||
"@octokit/plugin-retry": "8.0.3",
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.4.0",
|
||||
"@rspack/core": "1.6.8",
|
||||
"@rspack/dev-server": "1.1.4",
|
||||
"@rspack/core": "1.7.0",
|
||||
"@rspack/dev-server": "1.1.5",
|
||||
"@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.3.0",
|
||||
"jsdom": "27.4.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.50.1",
|
||||
"typescript-eslint": "8.51.0",
|
||||
"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.2",
|
||||
"lit-html": "3.3.2",
|
||||
"lit": "3.3.1",
|
||||
"lit-html": "3.3.1",
|
||||
"clean-css": "5.3.3",
|
||||
"@lit/reactive-element": "2.1.2",
|
||||
"@fullcalendar/daygrid": "6.1.20",
|
||||
"globals": "16.5.0",
|
||||
"globals": "17.0.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"
|
||||
|
||||
@@ -18,10 +18,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 rowRenderer: RenderItemFunction<PickerComboBoxItem> = (item) => html`
|
||||
@@ -184,18 +181,17 @@ 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
|
||||
@@ -316,10 +312,7 @@ export class HaEntityNamePicker extends LitElement {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _getFilteredItems = (
|
||||
searchString?: string,
|
||||
_section?: string
|
||||
): PickerComboBoxItem[] => {
|
||||
private _getFilteredItems = (): PickerComboBoxItem[] => {
|
||||
const items = this._getItems(this.entityId);
|
||||
const currentItem =
|
||||
this._editIndex != null ? this._items[this._editIndex] : undefined;
|
||||
@@ -336,49 +329,27 @@ export class HaEntityNamePicker extends LitElement {
|
||||
);
|
||||
|
||||
// When editing an existing text item, include it in the base items
|
||||
if (currentItem?.type === "text" && currentItem.text && !searchString) {
|
||||
if (currentItem?.type === "text" && currentItem.text) {
|
||||
filteredItems.push(this._customNameOption(currentItem.text));
|
||||
}
|
||||
|
||||
return filteredItems;
|
||||
};
|
||||
|
||||
private _getAdditionalItems = (
|
||||
searchString?: string
|
||||
private _searchFn = (
|
||||
searchString: string,
|
||||
filteredItems: PickerComboBoxItem[]
|
||||
): 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;
|
||||
|
||||
// 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
|
||||
);
|
||||
// Remove custom name option if search string is present to avoid duplicates
|
||||
if (searchString && currentId) {
|
||||
return filteredItems.filter((item) => item.id !== currentId);
|
||||
}
|
||||
return filteredItems;
|
||||
};
|
||||
|
||||
@@ -19,7 +19,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 HIDDEN_ATTRIBUTES = [
|
||||
@@ -199,11 +202,7 @@ export class HaStateContentPicker extends LitElement {
|
||||
.value=${this._getPickerValue()}
|
||||
.getItems=${this._getFilteredItems}
|
||||
.getAdditionalItems=${this._getAdditionalItems}
|
||||
.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"
|
||||
)}
|
||||
.searchFn=${this._searchFn}
|
||||
@value-changed=${this._pickerValueChanged}
|
||||
>
|
||||
<div slot="field" class="container">
|
||||
@@ -328,7 +327,7 @@ export class HaStateContentPicker extends LitElement {
|
||||
(text: string): PickerComboBoxItem => ({
|
||||
id: text,
|
||||
primary: this.hass.localize(
|
||||
"ui.components.entity.entity-state-content-picker.custom_state"
|
||||
"ui.components.entity.entity-state-content-picker.custom_attribute"
|
||||
),
|
||||
secondary: `"${text}"`,
|
||||
search_labels: {
|
||||
@@ -340,10 +339,7 @@ export class HaStateContentPicker extends LitElement {
|
||||
})
|
||||
);
|
||||
|
||||
private _getFilteredItems = (
|
||||
searchString?: string,
|
||||
_section?: string
|
||||
): PickerComboBoxItem[] => {
|
||||
private _getFilteredItems = (): PickerComboBoxItem[] => {
|
||||
const stateObj = this.entityId
|
||||
? this.hass.states[this.entityId]
|
||||
: undefined;
|
||||
@@ -358,11 +354,7 @@ 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) &&
|
||||
!searchString
|
||||
) {
|
||||
if (currentValue && !items.find((item) => item.id === currentValue)) {
|
||||
filteredItems.push(this._customValueOption(currentValue));
|
||||
}
|
||||
|
||||
@@ -372,33 +364,34 @@ 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);
|
||||
const existingItem = items.find((item) => item.id === searchString);
|
||||
|
||||
// Only return custom value option if it doesn't match an existing item
|
||||
if (!existingItem) {
|
||||
// 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) {
|
||||
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,6 +7,7 @@ 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 {
|
||||
@@ -108,6 +109,12 @@ 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;
|
||||
@@ -125,6 +132,7 @@ 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"
|
||||
|
||||
@@ -141,6 +141,7 @@ export class HaStatisticPicker extends LitElement {
|
||||
|
||||
private async _getStatisticIds() {
|
||||
this.statisticIds = await getStatisticIds(this.hass, this.statisticTypes);
|
||||
this._picker?.requestUpdate();
|
||||
}
|
||||
|
||||
private _getItems = () =>
|
||||
@@ -177,9 +178,9 @@ export class HaStatisticPicker extends LitElement {
|
||||
entitiesOnly?: boolean,
|
||||
excludeStatistics?: string[],
|
||||
value?: string
|
||||
): StatisticComboBoxItem[] => {
|
||||
): StatisticComboBoxItem[] | undefined => {
|
||||
if (!statisticIds) {
|
||||
return [];
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (includeStatisticsUnitOfMeasurement) {
|
||||
|
||||
@@ -39,7 +39,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
public getItems!: (
|
||||
searchString?: string,
|
||||
section?: string
|
||||
) => (PickerComboBoxItem | string)[];
|
||||
) => (PickerComboBoxItem | string)[] | undefined;
|
||||
|
||||
@property({ attribute: false, type: Array })
|
||||
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
|
||||
|
||||
@@ -124,9 +124,6 @@ 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}
|
||||
>
|
||||
@@ -173,20 +170,6 @@ 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);
|
||||
|
||||
@@ -44,6 +44,7 @@ class HaNavigationList extends LitElement {
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${page.iconPath}
|
||||
.secondaryPath=${page.iconSecondaryPath}
|
||||
.viewBox=${page.iconViewBox}
|
||||
></ha-svg-icon>
|
||||
</div>
|
||||
|
||||
@@ -55,6 +55,7 @@ export interface PickerComboBoxItem {
|
||||
}
|
||||
|
||||
export const NO_ITEMS_AVAILABLE_ID = "___no_items_available___";
|
||||
const PADDING_ID = "___padding___";
|
||||
|
||||
const DEFAULT_ROW_RENDERER: RenderItemFunction<PickerComboBoxItem> = (
|
||||
item
|
||||
@@ -108,7 +109,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
public getItems!: (
|
||||
searchString?: string,
|
||||
section?: string
|
||||
) => (PickerComboBoxItem | string)[];
|
||||
) => PickerComboBoxItem[] | undefined;
|
||||
|
||||
@property({ attribute: false, type: Array })
|
||||
public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
|
||||
@@ -150,7 +151,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
@query("ha-textfield") private _searchFieldElement?: HaTextField;
|
||||
|
||||
@state() private _items: (PickerComboBoxItem | string)[] = [];
|
||||
@state() private _items: PickerComboBoxItem[] = [];
|
||||
|
||||
protected get scrollableElement(): HTMLElement | null {
|
||||
return this._virtualizerElement as HTMLElement | null;
|
||||
@@ -160,7 +161,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
@state() private _valuePinned = true;
|
||||
|
||||
private _allItems: (PickerComboBoxItem | string)[] = [];
|
||||
private _allItems: PickerComboBoxItem[] = [];
|
||||
|
||||
private _selectedItemIndex = -1;
|
||||
|
||||
@@ -278,8 +279,8 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
this._sectionTitle = this.sectionTitleFunction({
|
||||
firstIndex: ev.first,
|
||||
lastIndex: ev.last,
|
||||
firstItem: firstItem as PickerComboBoxItem | string,
|
||||
secondItem: secondItem as PickerComboBoxItem | string,
|
||||
firstItem: firstItem as PickerComboBoxItem,
|
||||
secondItem: secondItem as PickerComboBoxItem,
|
||||
itemsCount: this._virtualizerElement.items.length,
|
||||
});
|
||||
}
|
||||
@@ -294,7 +295,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
this.getAdditionalItems?.(searchString) || [];
|
||||
|
||||
private _getItems = () => {
|
||||
let items = [...this.getItems(this._search, this.selectedSection)];
|
||||
let items = [...(this.getItems(this._search, this.selectedSection) || [])];
|
||||
|
||||
if (!this.sections?.length) {
|
||||
items = items.sort((entityA, entityB) => {
|
||||
@@ -323,28 +324,28 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
if (!items.length) {
|
||||
items.push(NO_ITEMS_AVAILABLE_ID);
|
||||
if (!items.length && !this.allowCustomValue) {
|
||||
items.push({ id: NO_ITEMS_AVAILABLE_ID, primary: "" });
|
||||
}
|
||||
|
||||
const additionalItems = this._getAdditionalItems();
|
||||
items.push(...additionalItems);
|
||||
|
||||
if (this.mode === "dialog") {
|
||||
items.push("padding"); // padding for safe area inset
|
||||
items.push({ id: PADDING_ID, primary: "" }); // padding for safe area inset
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
private _renderItem = (item: PickerComboBoxItem | string, index: number) => {
|
||||
private _renderItem = (item: PickerComboBoxItem, index: number) => {
|
||||
if (!item) {
|
||||
return nothing;
|
||||
}
|
||||
if (item === "padding") {
|
||||
if (item.id === PADDING_ID) {
|
||||
return html`<div class="bottom-padding"></div>`;
|
||||
}
|
||||
if (item === NO_ITEMS_AVAILABLE_ID) {
|
||||
if (item.id === NO_ITEMS_AVAILABLE_ID) {
|
||||
return html`
|
||||
<div class="combo-box-row">
|
||||
<ha-combo-box-item type="text" compact>
|
||||
@@ -419,21 +420,18 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = this._fuseIndex(
|
||||
this._allItems as PickerComboBoxItem[],
|
||||
this.searchKeys
|
||||
);
|
||||
const index = this._fuseIndex(this._allItems, this.searchKeys);
|
||||
|
||||
let filteredItems = multiTermSortedSearch<PickerComboBoxItem>(
|
||||
this._allItems as PickerComboBoxItem[],
|
||||
this._allItems,
|
||||
searchString,
|
||||
this.searchKeys || DEFAULT_SEARCH_KEYS,
|
||||
(item) => item.id,
|
||||
index
|
||||
) as (PickerComboBoxItem | string)[];
|
||||
);
|
||||
|
||||
if (!filteredItems.length) {
|
||||
filteredItems.push(NO_ITEMS_AVAILABLE_ID);
|
||||
if (!filteredItems.length && !this.allowCustomValue) {
|
||||
filteredItems.push({ id: NO_ITEMS_AVAILABLE_ID, primary: "" });
|
||||
}
|
||||
|
||||
const additionalItems = this._getAdditionalItems(searchString);
|
||||
@@ -442,8 +440,8 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
if (this.searchFn) {
|
||||
filteredItems = this.searchFn(
|
||||
searchString,
|
||||
filteredItems as PickerComboBoxItem[],
|
||||
this._allItems as PickerComboBoxItem[]
|
||||
filteredItems,
|
||||
this._allItems
|
||||
);
|
||||
}
|
||||
|
||||
@@ -459,7 +457,7 @@ export class HaPickerComboBox extends ScrollableFadeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
this._items = filteredItems as PickerComboBoxItem[];
|
||||
this._items = filteredItems;
|
||||
}
|
||||
|
||||
this._selectedItemIndex = -1;
|
||||
|
||||
@@ -54,7 +54,8 @@ export class HaChooseSelector extends LitElement {
|
||||
size="small"
|
||||
.buttons=${this._toggleButtons(
|
||||
this.selector.choose.choices,
|
||||
this.selector.choose.translation_key
|
||||
this.selector.choose.translation_key,
|
||||
this.hass.localize
|
||||
)}
|
||||
.active=${this._activeChoice}
|
||||
@value-changed=${this._choiceChanged}
|
||||
@@ -72,7 +73,11 @@ export class HaChooseSelector extends LitElement {
|
||||
}
|
||||
|
||||
private _toggleButtons = memoizeOne(
|
||||
(choices: ChooseSelector["choose"]["choices"], translationKey?: string) =>
|
||||
(
|
||||
choices: ChooseSelector["choose"]["choices"],
|
||||
translationKey?: string,
|
||||
_localize?: HomeAssistant["localize"]
|
||||
) =>
|
||||
Object.keys(choices).map((choice) => ({
|
||||
label:
|
||||
this.localizeValue && translationKey
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { StateSelector } from "../../data/selector";
|
||||
import { extractFromTarget } from "../../data/target";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
resolveEntityIDs,
|
||||
type StateSelector,
|
||||
type TargetSelector,
|
||||
} from "../../data/selector";
|
||||
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) {
|
||||
@@ -28,16 +33,33 @@ 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?.filter_target,
|
||||
this.context?.target_selector
|
||||
).then((entityIds) => {
|
||||
this._entityIds = entityIds;
|
||||
});
|
||||
@@ -45,6 +67,9 @@ 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
|
||||
@@ -52,7 +77,7 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
.entityId=${this._entityIds}
|
||||
.attribute=${this.selector.state?.attribute ||
|
||||
this.context?.filter_attribute}
|
||||
.extraOptions=${this.selector.state?.extra_options}
|
||||
.extraOptions=${extraOptions}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
@@ -69,7 +94,7 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
.entityId=${this._entityIds}
|
||||
.attribute=${this.selector.state?.attribute ||
|
||||
this.context?.filter_attribute}
|
||||
.extraOptions=${this.selector.state?.extra_options}
|
||||
.extraOptions=${extraOptions}
|
||||
.value=${this.value}
|
||||
.label=${this.label}
|
||||
.helper=${this.helper}
|
||||
@@ -84,7 +109,8 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
private async _resolveEntityIds(
|
||||
selectorEntityId: string | string[] | undefined,
|
||||
contextFilterEntity: string | string[] | undefined,
|
||||
contextFilterTarget: HassServiceTarget | undefined
|
||||
contextFilterTarget: HassServiceTarget | undefined,
|
||||
contextTargetSelector: TargetSelector | undefined
|
||||
): Promise<string | string[] | undefined> {
|
||||
if (selectorEntityId !== undefined) {
|
||||
return selectorEntityId;
|
||||
@@ -93,8 +119,14 @@ export class HaSelectorState extends SubscribeMixin(LitElement) {
|
||||
return contextFilterEntity;
|
||||
}
|
||||
if (contextFilterTarget !== undefined) {
|
||||
const result = await extractFromTarget(this.hass, contextFilterTarget);
|
||||
return result.referenced_entities;
|
||||
return resolveEntityIDs(
|
||||
this.hass,
|
||||
contextFilterTarget,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas,
|
||||
contextTargetSelector
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ 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";
|
||||
|
||||
@@ -185,21 +184,22 @@ export class HaWaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
await this.updateComplete;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
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;
|
||||
}
|
||||
// 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;
|
||||
// }
|
||||
(this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
|
||||
});
|
||||
};
|
||||
@@ -271,6 +271,7 @@ 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(
|
||||
|
||||
@@ -16,6 +16,8 @@ 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 {
|
||||
@@ -174,7 +176,7 @@ export const fetchStatistics = (
|
||||
startTime: Date,
|
||||
endTime?: Date,
|
||||
statistic_ids?: string[],
|
||||
period: "5minute" | "hour" | "day" | "week" | "month" = "hour",
|
||||
period: StatisticPeriod = "hour",
|
||||
units?: StatisticsUnitConfiguration,
|
||||
types?: StatisticsTypes
|
||||
) =>
|
||||
|
||||
@@ -929,13 +929,13 @@ export const resolveEntityIDs = (
|
||||
targetPickerValue: HassServiceTarget,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"]
|
||||
areas: HomeAssistant["areas"],
|
||||
targetSelector: TargetSelector = { target: {} }
|
||||
): 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));
|
||||
|
||||
@@ -28,6 +28,7 @@ window.loadES5Adapter = () => {
|
||||
};
|
||||
|
||||
let panelEl: HTMLElement | undefined;
|
||||
let initialized = false;
|
||||
|
||||
function setProperties(properties) {
|
||||
if (!panelEl) {
|
||||
@@ -128,13 +129,23 @@ function initialize(
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener(
|
||||
"DOMContentLoaded",
|
||||
() => window.parent.customPanel!.registerIframe(initialize, setProperties),
|
||||
{ once: true }
|
||||
);
|
||||
function handleReady() {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
window.parent.customPanel!.registerIframe(initialize, setProperties);
|
||||
}
|
||||
|
||||
window.addEventListener("unload", () => {
|
||||
// Initial load
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", handleReady, { once: true });
|
||||
} else {
|
||||
handleReady();
|
||||
}
|
||||
|
||||
window.addEventListener("pageshow", handleReady);
|
||||
|
||||
window.addEventListener("pagehide", () => {
|
||||
initialized = false;
|
||||
// allow disconnected callback to fire
|
||||
while (document.body.lastChild) {
|
||||
document.body.removeChild(document.body.lastChild);
|
||||
|
||||
@@ -282,6 +282,7 @@ export const provideHass = (
|
||||
dockedSidebar: "auto",
|
||||
vibrate: true,
|
||||
debugConnection: false,
|
||||
kioskMode: false,
|
||||
suspendWhenHidden: false,
|
||||
moreInfoEntityId: null as any,
|
||||
// @ts-ignore
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface PageNavigation {
|
||||
core?: boolean;
|
||||
advancedOnly?: boolean;
|
||||
iconPath?: string;
|
||||
iconSecondaryPath?: string;
|
||||
iconViewBox?: string;
|
||||
description?: string;
|
||||
iconColor?: string;
|
||||
|
||||
@@ -50,7 +50,6 @@ 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,
|
||||
|
||||
@@ -4,7 +4,6 @@ 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";
|
||||
@@ -269,8 +268,11 @@ export class HaPlatformCondition extends LitElement {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const context = {};
|
||||
const context: Record<string, any> = {};
|
||||
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
|
||||
@@ -378,7 +380,7 @@ export class HaPlatformCondition extends LitElement {
|
||||
return "";
|
||||
}
|
||||
return this.hass.localize(
|
||||
`component.${computeDomain(this.condition.condition)}.selector.${key}`
|
||||
`component.${getConditionDomain(this.condition.condition)}.selector.${key}`
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -82,7 +82,6 @@ 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,
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiAlert, mdiFormatListBulleted, mdiShape } from "@mdi/js";
|
||||
import {
|
||||
mdiAlert,
|
||||
mdiCodeBraces,
|
||||
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 {
|
||||
@@ -167,6 +173,16 @@ 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,7 +4,6 @@ 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";
|
||||
@@ -305,8 +304,11 @@ export class HaPlatformTrigger extends LitElement {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const context = {};
|
||||
const context: Record<string, any> = {};
|
||||
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
|
||||
@@ -414,7 +416,7 @@ export class HaPlatformTrigger extends LitElement {
|
||||
return "";
|
||||
}
|
||||
return this.hass.localize(
|
||||
`component.${computeDomain(this.trigger.trigger)}.selector.${key}`
|
||||
`component.${getTriggerDomain(this.trigger.trigger)}.selector.${key}`
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -163,7 +163,7 @@ class DialogImportBlueprint extends LitElement {
|
||||
</div>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
slot="primaryAction"
|
||||
slot="secondaryAction"
|
||||
@click=${this.closeDialog}
|
||||
.disabled=${this._saving}
|
||||
>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } 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";
|
||||
|
||||
@@ -17,13 +18,29 @@ 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) =>
|
||||
page.path === "#external-app-configuration"
|
||||
? this.hass.auth.external?.config.hasSettingsScreen
|
||||
: canShowPage(this.hass, page)
|
||||
)
|
||||
.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);
|
||||
})
|
||||
.map((page) => ({
|
||||
...page,
|
||||
name:
|
||||
|
||||
@@ -5,8 +5,9 @@ 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";
|
||||
@@ -19,6 +20,8 @@ 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;
|
||||
@@ -42,10 +45,15 @@ 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 });
|
||||
@@ -57,10 +65,12 @@ class DialogDeviceRegistryDetail extends LitElement {
|
||||
}
|
||||
const device = this._params.device;
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this.closeDialog}
|
||||
.heading=${computeDeviceNameDisplay(device, this.hass)}
|
||||
<ha-wa-dialog
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${computeDeviceNameDisplay(device, this.hass)}
|
||||
prevent-scrim-close
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<div>
|
||||
${this._error
|
||||
@@ -68,6 +78,7 @@ class DialogDeviceRegistryDetail extends LitElement {
|
||||
: ""}
|
||||
<div class="form">
|
||||
<ha-textfield
|
||||
autofocus
|
||||
.value=${this._nameByUser}
|
||||
@input=${this._nameChanged}
|
||||
.label=${this.hass.localize(
|
||||
@@ -75,7 +86,6 @@ class DialogDeviceRegistryDetail extends LitElement {
|
||||
)}
|
||||
.placeholder=${device.name || ""}
|
||||
.disabled=${this._submitting}
|
||||
dialogInitialFocus
|
||||
></ha-textfield>
|
||||
<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
@@ -131,22 +141,25 @@ class DialogDeviceRegistryDetail extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
mdiScrewdriver,
|
||||
mdiScriptText,
|
||||
mdiShape,
|
||||
mdiLan,
|
||||
mdiSofa,
|
||||
mdiTools,
|
||||
mdiUpdate,
|
||||
@@ -107,15 +106,6 @@ export const configSections: Record<string, PageNavigation[]> = {
|
||||
},
|
||||
],
|
||||
dashboard_2: [
|
||||
{
|
||||
path: "/config/matter",
|
||||
name: "Matter",
|
||||
iconPath:
|
||||
"M7.228375 6.41685c0.98855 0.80195 2.16365 1.3412 3.416275 1.56765V1.30093l1.3612 -0.7854275 1.360125 0.7854275V7.9845c1.252875 -0.226675 2.4283 -0.765875 3.41735 -1.56765l2.471225 1.4293c-4.019075 3.976275 -10.490025 3.976275 -14.5091 0l2.482925 -1.4293Zm3.00335 17.067575c1.43325 -5.47035 -1.8052 -11.074775 -7.2604 -12.564675v2.859675c1.189125 0.455 2.244125 1.202875 3.0672 2.174275L0.25 19.2955v1.5719l1.3611925 0.781175L7.39865 18.3068c0.430175 1.19825 0.550625 2.48575 0.35015 3.743l2.482925 1.434625ZM21.034 10.91975c-5.452225 1.4932 -8.6871 7.09635 -7.254025 12.564675l2.47655 -1.43035c-0.200025 -1.257275 -0.079575 -2.544675 0.35015 -3.743025l5.7832 3.337525L23.75 20.86315V19.2955L17.961475 15.9537c0.8233 -0.97115 1.878225 -1.718975 3.0672 -2.174275l0.005325 -2.859675Z",
|
||||
iconColor: "#2458B3",
|
||||
component: "matter",
|
||||
translationKey: "matter",
|
||||
},
|
||||
{
|
||||
path: "/config/zha",
|
||||
name: "Zigbee",
|
||||
@@ -135,7 +125,8 @@ export const configSections: Record<string, PageNavigation[]> = {
|
||||
{
|
||||
path: "/knx",
|
||||
name: "KNX",
|
||||
iconPath: mdiLan,
|
||||
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",
|
||||
@@ -144,8 +135,7 @@ export const configSections: Record<string, PageNavigation[]> = {
|
||||
path: "/config/thread",
|
||||
name: "Thread",
|
||||
iconPath:
|
||||
"M82.498,0C37.008,0,0,37.008,0,82.496c0,45.181,36.51,81.977,81.573,82.476V82.569l-27.002-0.002 c-8.023,0-14.554,6.53-14.554,14.561c0,8.023,6.531,14.551,14.554,14.551v17.98c-17.939,0-32.534-14.595-32.534-32.531 c0-17.944,14.595-32.543,32.534-32.543l27.002,0.004v-9.096c0-14.932,12.146-27.08,27.075-27.08 c14.932,0,27.082,12.148,27.082,27.08c0,14.931-12.15,27.08-27.082,27.08l-9.097-0.001v80.641 C136.889,155.333,165,122.14,165,82.496C165,37.008,127.99,0,82.498,0z",
|
||||
iconViewBox: "0 0 165 165",
|
||||
"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",
|
||||
@@ -161,7 +151,8 @@ export const configSections: Record<string, PageNavigation[]> = {
|
||||
{
|
||||
path: "/insteon",
|
||||
name: "Insteon",
|
||||
iconPath: mdiLan,
|
||||
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",
|
||||
|
||||
@@ -23,23 +23,12 @@ import {
|
||||
subscribeBluetoothScannersDetails,
|
||||
} from "../../../../../data/bluetooth";
|
||||
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
|
||||
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
|
||||
import "../../../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import { bluetoothTabs } from "./bluetooth-config-dashboard";
|
||||
import { showBluetoothDeviceInfoDialog } from "./show-dialog-bluetooth-device-info";
|
||||
|
||||
export const bluetoothAdvertisementMonitorTabs: PageNavigation[] = [
|
||||
{
|
||||
translationKey: "ui.panel.config.bluetooth.advertisement_monitor",
|
||||
path: "advertisement-monitor",
|
||||
},
|
||||
{
|
||||
translationKey: "ui.panel.config.bluetooth.visualization",
|
||||
path: "visualization",
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("bluetooth-advertisement-monitor")
|
||||
export class BluetoothAdvertisementMonitorPanel extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -232,7 +221,7 @@ export class BluetoothAdvertisementMonitorPanel extends LitElement {
|
||||
@collapsed-changed=${this._handleCollapseChanged}
|
||||
filter=${this.address || ""}
|
||||
clickable
|
||||
.tabs=${bluetoothAdvertisementMonitorTabs}
|
||||
.tabs=${bluetoothTabs}
|
||||
></hass-tabs-subpage-data-table>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../../components/ha-card";
|
||||
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";
|
||||
mdiBroadcast,
|
||||
mdiCogOutline,
|
||||
mdiLan,
|
||||
mdiLinkVariant,
|
||||
mdiNetwork,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, 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 type {
|
||||
BluetoothAllocationsData,
|
||||
BluetoothScannerState,
|
||||
@@ -23,29 +21,60 @@ import type {
|
||||
HaScannerType,
|
||||
} from "../../../../../data/bluetooth";
|
||||
import {
|
||||
getValueInPercentage,
|
||||
roundWithOneDecimal,
|
||||
} from "../../../../../util/calculate";
|
||||
import "../../../../../components/ha-metric";
|
||||
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-tabs-subpage";
|
||||
import type { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
|
||||
export const bluetoothTabs: PageNavigation[] = [
|
||||
{
|
||||
translationKey: "ui.panel.config.bluetooth.tabs.overview",
|
||||
path: `/config/bluetooth/dashboard`,
|
||||
iconPath: mdiNetwork,
|
||||
},
|
||||
{
|
||||
translationKey: "ui.panel.config.bluetooth.tabs.advertisements",
|
||||
path: `/config/bluetooth/advertisement-monitor`,
|
||||
iconPath: mdiBroadcast,
|
||||
},
|
||||
{
|
||||
translationKey: "ui.panel.config.bluetooth.tabs.visualization",
|
||||
path: `/config/bluetooth/visualization`,
|
||||
iconPath: mdiLan,
|
||||
},
|
||||
{
|
||||
translationKey: "ui.panel.config.bluetooth.tabs.connections",
|
||||
path: `/config/bluetooth/connection-monitor`,
|
||||
iconPath: mdiLinkVariant,
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("bluetooth-config-dashboard")
|
||||
export class BluetoothConfigDashboard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@state() private _configEntries: ConfigEntry[] = [];
|
||||
|
||||
@state() private _connectionAllocationData: BluetoothAllocationsData[] = [];
|
||||
|
||||
@state() private _connectionAllocationsError?: string;
|
||||
|
||||
@state() private _scannerState?: BluetoothScannerState;
|
||||
@state() private _scannerStates: Record<string, 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;
|
||||
@@ -55,41 +84,44 @@ 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 || !this._configEntry) {
|
||||
if (this._unsubConnectionAllocations) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this._unsubConnectionAllocations =
|
||||
await subscribeBluetoothConnectionAllocations(
|
||||
this.hass.connection,
|
||||
(data) => {
|
||||
this._connectionAllocationData = data;
|
||||
},
|
||||
this._configEntry
|
||||
);
|
||||
} catch (err: any) {
|
||||
this._unsubConnectionAllocations = undefined;
|
||||
this._connectionAllocationsError = err.message;
|
||||
}
|
||||
this._unsubConnectionAllocations =
|
||||
await subscribeBluetoothConnectionAllocations(
|
||||
this.hass.connection,
|
||||
(data) => {
|
||||
this._connectionAllocationData = data;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private async _subscribeBluetoothScannerState(): Promise<void> {
|
||||
if (this._unsubScannerState || !this._configEntry) {
|
||||
if (this._unsubScannerState) {
|
||||
return;
|
||||
}
|
||||
this._unsubScannerState = await subscribeBluetoothScannerState(
|
||||
this.hass.connection,
|
||||
(scannerState) => {
|
||||
this._scannerState = scannerState;
|
||||
},
|
||||
this._configEntry
|
||||
this._scannerStates = {
|
||||
...this._scannerStates,
|
||||
[scannerState.source]: scannerState,
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
@@ -122,92 +154,111 @@ 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 .narrow=${this.narrow} .hass=${this.hass}>
|
||||
<hass-tabs-subpage
|
||||
header=${this.hass.localize("ui.panel.config.bluetooth.title")}
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
.route=${this.route}
|
||||
.tabs=${bluetoothTabs}
|
||||
>
|
||||
<div class="content">
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.settings_title"
|
||||
)}
|
||||
>
|
||||
<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(
|
||||
"ui.panel.config.bluetooth.advertisement_monitor"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.advertisement_monitor_details"
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button
|
||||
href="/config/bluetooth/advertisement-monitor"
|
||||
appearance="plain"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.advertisement_monitor"
|
||||
)}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
href="/config/bluetooth/visualization"
|
||||
appearance="plain"
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.bluetooth.visualization")}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
<ha-card
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.connection_slot_allocations_monitor"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
${this._renderConnectionAllocations()}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button
|
||||
href="/config/bluetooth/connection-monitor"
|
||||
appearance="plain"
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.connection_monitor"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
<ha-list>${this._renderAdaptersList()}</ha-list>
|
||||
</ha-card>
|
||||
</div>
|
||||
</hass-subpage>
|
||||
</hass-tabs-subpage>
|
||||
`;
|
||||
}
|
||||
|
||||
private _getUsedAllocations = (used: number, total: number) =>
|
||||
roundWithOneDecimal(getValueInPercentage(used, 0, total));
|
||||
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 _renderScannerMismatchWarning(
|
||||
name: string,
|
||||
scannerState: BluetoothScannerState,
|
||||
scannerType: HaScannerType,
|
||||
formatMode: (mode: string | null) => string
|
||||
scannerType: HaScannerType
|
||||
) {
|
||||
const instructions: string[] = [];
|
||||
|
||||
@@ -238,8 +289,9 @@ export class BluetoothConfigDashboard extends LitElement {
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.scanner_mode_mismatch",
|
||||
{
|
||||
requested: formatMode(scannerState.requested_mode),
|
||||
current: formatMode(scannerState.current_mode),
|
||||
name: name,
|
||||
requested: this._formatMode(scannerState.requested_mode),
|
||||
current: this._formatMode(scannerState.current_mode),
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
@@ -249,127 +301,59 @@ export class BluetoothConfigDashboard extends LitElement {
|
||||
</ha-alert>`;
|
||||
}
|
||||
|
||||
private _renderScannerState() {
|
||||
if (!this._configEntry || !this._scannerState) {
|
||||
return html`<div>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.bluetooth.no_scanner_state_available"
|
||||
)}
|
||||
</div>`;
|
||||
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;
|
||||
}
|
||||
|
||||
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 _renderConnectionAllocations() {
|
||||
if (this._connectionAllocationsError) {
|
||||
return html`<ha-alert alert-type="error"
|
||||
>${this._connectionAllocationsError}</ha-alert
|
||||
>`;
|
||||
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;
|
||||
}
|
||||
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;
|
||||
private _formatScannerModeText(
|
||||
scannerState: BluetoothScannerState | undefined
|
||||
): string {
|
||||
if (!scannerState) {
|
||||
return this.hass.localize(
|
||||
"ui.panel.config.bluetooth.scanner_state_unknown"
|
||||
);
|
||||
}
|
||||
const configEntries = await getConfigEntries(this.hass, {
|
||||
domain: "bluetooth",
|
||||
});
|
||||
const configEntry = configEntries.find(
|
||||
(entry) => entry.entry_id === configEntryId
|
||||
);
|
||||
showOptionsFlowDialog(this, configEntry!);
|
||||
|
||||
return this._formatModeLabel(scannerState.current_mode);
|
||||
}
|
||||
|
||||
private _openOptionFlow(ev: Event) {
|
||||
const button = ev.currentTarget as HTMLElement & { entry: ConfigEntry };
|
||||
showOptionsFlowDialog(this, button.entry);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -394,17 +378,9 @@ export class BluetoothConfigDashboard extends LitElement {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.scanner-state {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.state-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.state-value {
|
||||
font-weight: 500;
|
||||
ha-list-item {
|
||||
--mdc-list-item-meta-display: flex;
|
||||
--mdc-list-item-meta-size: 48px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -24,6 +24,7 @@ import type { DeviceRegistryEntry } from "../../../../../data/device/device_regi
|
||||
import "../../../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import { bluetoothTabs } from "./bluetooth-config-dashboard";
|
||||
|
||||
@customElement("bluetooth-connection-monitor")
|
||||
export class BluetoothConnectionMonitorPanel extends LitElement {
|
||||
@@ -214,6 +215,7 @@ export class BluetoothConnectionMonitorPanel extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.tabs=${bluetoothTabs}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.data=${this._dataWithNamedSourceAndIds(this._data)}
|
||||
.initialGroupColumn=${this._activeGrouping}
|
||||
|
||||
@@ -26,9 +26,9 @@ import {
|
||||
subscribeBluetoothScannersDetails,
|
||||
} from "../../../../../data/bluetooth";
|
||||
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
|
||||
import "../../../../../layouts/hass-subpage";
|
||||
import "../../../../../layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant, Route } from "../../../../../types";
|
||||
import { bluetoothAdvertisementMonitorTabs } from "./bluetooth-advertisement-monitor";
|
||||
import { bluetoothTabs } from "./bluetooth-config-dashboard";
|
||||
|
||||
const UPDATE_THROTTLE_TIME = 10000;
|
||||
|
||||
@@ -123,8 +123,7 @@ export class BluetoothNetworkVisualization extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
header=${this.hass.localize("ui.panel.config.bluetooth.visualization")}
|
||||
.tabs=${bluetoothAdvertisementMonitorTabs}
|
||||
.tabs=${bluetoothTabs}
|
||||
>
|
||||
<ha-network-graph
|
||||
.hass=${this.hass}
|
||||
|
||||
@@ -46,44 +46,54 @@ export class MatterConfigDashboard extends LitElement {
|
||||
href="/config/thread"
|
||||
slot="toolbar-icon"
|
||||
>
|
||||
Visit Thread Panel</ha-button
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.thread_panel"
|
||||
)}</ha-button
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
<div class="content">
|
||||
<ha-card header="Matter">
|
||||
<ha-alert alert-type="warning"
|
||||
>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
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.experimental_note"
|
||||
)}</ha-alert
|
||||
>
|
||||
<div class="card-content">
|
||||
${this._error
|
||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||
: ""}
|
||||
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.
|
||||
${this.hass.localize("ui.panel.config.matter.panel.add_devices")}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
${canCommissionMatterExternal(this.hass)
|
||||
? html`<ha-button
|
||||
appearance="plain"
|
||||
@click=${this._startMobileCommissioning}
|
||||
>Commission device with mobile app</ha-button
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.mobile_app_commisioning"
|
||||
)}</ha-button
|
||||
>`
|
||||
: ""}
|
||||
<ha-button appearance="plain" @click=${this._commission}
|
||||
>Commission device</ha-button
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.commission_device"
|
||||
)}</ha-button
|
||||
>
|
||||
<ha-button appearance="plain" @click=${this._acceptSharedDevice}
|
||||
>Add shared device</ha-button
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.add_shared_device"
|
||||
)}</ha-button
|
||||
>
|
||||
<ha-button appearance="plain" @click=${this._setWifi}
|
||||
>Set WiFi Credentials</ha-button
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.set_wifi_credentials"
|
||||
)}</ha-button
|
||||
>
|
||||
<ha-button appearance="plain" @click=${this._setThread}
|
||||
>Set Thread Credentials</ha-button
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.matter.panel.set_thread_credentials"
|
||||
)}</ha-button
|
||||
>
|
||||
</div>
|
||||
</ha-card>
|
||||
@@ -114,19 +124,31 @@ export class MatterConfigDashboard extends LitElement {
|
||||
private async _setWifi(): Promise<void> {
|
||||
this._error = undefined;
|
||||
const networkName = await showPromptDialog(this, {
|
||||
title: "Network name",
|
||||
inputLabel: "Network name",
|
||||
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"
|
||||
),
|
||||
inputType: "string",
|
||||
confirmText: "Continue",
|
||||
confirmText: this.hass.localize(
|
||||
"ui.panel.config.matter.panel.prompts.network_name.confirm"
|
||||
),
|
||||
});
|
||||
if (!networkName) {
|
||||
return;
|
||||
}
|
||||
const psk = await showPromptDialog(this, {
|
||||
title: "Passcode",
|
||||
inputLabel: "Code",
|
||||
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"
|
||||
),
|
||||
inputType: "password",
|
||||
confirmText: "Set Wifi",
|
||||
confirmText: this.hass.localize(
|
||||
"ui.panel.config.matter.panel.prompts.passcode.confirm"
|
||||
),
|
||||
});
|
||||
if (!psk) {
|
||||
return;
|
||||
@@ -140,10 +162,16 @@ export class MatterConfigDashboard extends LitElement {
|
||||
|
||||
private async _commission(): Promise<void> {
|
||||
const code = await showPromptDialog(this, {
|
||||
title: "Commission device",
|
||||
inputLabel: "Code",
|
||||
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"
|
||||
),
|
||||
inputType: "string",
|
||||
confirmText: "Commission",
|
||||
confirmText: this.hass.localize(
|
||||
"ui.panel.config.matter.panel.prompts.commission_device.confirm"
|
||||
),
|
||||
});
|
||||
if (!code) {
|
||||
return;
|
||||
@@ -160,10 +188,16 @@ export class MatterConfigDashboard extends LitElement {
|
||||
|
||||
private async _acceptSharedDevice(): Promise<void> {
|
||||
const code = await showPromptDialog(this, {
|
||||
title: "Add shared device",
|
||||
inputLabel: "Pin",
|
||||
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"
|
||||
),
|
||||
inputType: "number",
|
||||
confirmText: "Accept device",
|
||||
confirmText: this.hass.localize(
|
||||
"ui.panel.config.matter.panel.prompts.add_shared_device.confirm"
|
||||
),
|
||||
});
|
||||
if (!code) {
|
||||
return;
|
||||
@@ -180,10 +214,16 @@ export class MatterConfigDashboard extends LitElement {
|
||||
|
||||
private async _setThread(): Promise<void> {
|
||||
const code = await showPromptDialog(this, {
|
||||
title: "Set Thread operation",
|
||||
inputLabel: "Dataset",
|
||||
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"
|
||||
),
|
||||
inputType: "string",
|
||||
confirmText: "Set Thread",
|
||||
confirmText: this.hass.localize(
|
||||
"ui.panel.config.matter.panel.prompts.set_thread.confirm"
|
||||
),
|
||||
});
|
||||
if (!code) {
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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";
|
||||
@@ -12,10 +11,6 @@ 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,
|
||||
@@ -52,7 +47,6 @@ 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") {
|
||||
@@ -60,17 +54,6 @@ 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;
|
||||
|
||||
@property({ attribute: false }) public configEntryId?: string;
|
||||
@state() private _configEntry?: ConfigEntry;
|
||||
|
||||
@state() private _configuration?: ZHAConfiguration;
|
||||
|
||||
@@ -95,6 +95,7 @@ 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();
|
||||
@@ -110,7 +111,6 @@ class ZHAConfigDashboard extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.tabs=${zhaTabs}
|
||||
back-path="/config/integrations"
|
||||
has-fab
|
||||
>
|
||||
<div class="container">
|
||||
@@ -151,28 +151,26 @@ class ZHAConfigDashboard extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
</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>`
|
||||
: ""}
|
||||
<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>
|
||||
</ha-card>
|
||||
<ha-card
|
||||
class="network-settings"
|
||||
@@ -321,6 +319,15 @@ 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!);
|
||||
}
|
||||
@@ -399,20 +406,11 @@ class ZHAConfigDashboard extends LitElement {
|
||||
fileDownload(backupJSON, `${basename}.json`);
|
||||
}
|
||||
|
||||
private async _openOptionFlow() {
|
||||
if (!this.configEntryId) {
|
||||
private _openOptionFlow() {
|
||||
if (!this._configEntry) {
|
||||
return;
|
||||
}
|
||||
|
||||
const configEntries: ConfigEntry[] = await getConfigEntries(this.hass, {
|
||||
domain: "zha",
|
||||
});
|
||||
|
||||
const configEntry = configEntries.find(
|
||||
(entry) => entry.entry_id === this.configEntryId
|
||||
);
|
||||
|
||||
showOptionsFlowDialog(this, configEntry!);
|
||||
showOptionsFlowDialog(this, this._configEntry);
|
||||
}
|
||||
|
||||
private _dataChanged(ev) {
|
||||
|
||||
@@ -302,10 +302,11 @@ 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>`
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.zwave_js.update_firmware.finished_status.done${localizationKeySuffix}`
|
||||
)}
|
||||
</p>
|
||||
${closeButton}`
|
||||
: html`<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.update_firmware.finished_status.try_again"
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
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,9 +3,7 @@ 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[] = [
|
||||
{
|
||||
@@ -33,14 +31,36 @@ class ZWaveJSConfigRouter extends HassRouterPage {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
private _configEntry = new URLSearchParams(window.location.search).get(
|
||||
"config_entry"
|
||||
);
|
||||
private _configEntry: string | null = null;
|
||||
|
||||
protected routerOptions: RouterOptions = {
|
||||
defaultPage: "dashboard",
|
||||
defaultPage: "picker",
|
||||
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"),
|
||||
@@ -70,7 +90,6 @@ class ZWaveJSConfigRouter extends HassRouterPage {
|
||||
load: () => import("./zwave_js-network-visualization"),
|
||||
},
|
||||
},
|
||||
initialLoad: () => this._fetchConfigEntries(),
|
||||
};
|
||||
|
||||
protected updatePageEl(el): void {
|
||||
@@ -79,29 +98,6 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,35 +30,33 @@ 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(
|
||||
dayDifference: number,
|
||||
end: Date,
|
||||
detailedDailyData = false
|
||||
): number {
|
||||
export function getSuggestedMax(period: StatisticPeriod, end: Date): 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 (dayDifference > 2 && suggestedMax.getHours() === 0) {
|
||||
if (suggestedMax.getHours() === 0) {
|
||||
suggestedMax = subHours(suggestedMax, 1);
|
||||
}
|
||||
|
||||
if (!detailedDailyData) {
|
||||
suggestedMax.setMinutes(0, 0, 0);
|
||||
}
|
||||
if (dayDifference > 35) {
|
||||
suggestedMax.setDate(1);
|
||||
}
|
||||
if (dayDifference > 2) {
|
||||
suggestedMax.setHours(0);
|
||||
suggestedMax.setHours(0);
|
||||
if (period === "day" || period === "week") {
|
||||
return suggestedMax.getTime();
|
||||
}
|
||||
// period === month
|
||||
suggestedMax.setDate(1);
|
||||
return suggestedMax.getTime();
|
||||
}
|
||||
|
||||
export function getSuggestedPeriod(
|
||||
dayDifference: number
|
||||
): "month" | "day" | "hour" {
|
||||
export function getSuggestedPeriod(dayDifference: number): StatisticPeriod {
|
||||
return dayDifference > 35 ? "month" : dayDifference > 2 ? "day" : "hour";
|
||||
}
|
||||
|
||||
@@ -96,7 +94,10 @@ export function getCommonOptions(
|
||||
xAxis: {
|
||||
type: "time",
|
||||
min: start,
|
||||
max: getSuggestedMax(dayDifference, end, detailedDailyData),
|
||||
max: getSuggestedMax(
|
||||
detailedDailyData ? "5minute" : getSuggestedPeriod(dayDifference),
|
||||
end
|
||||
),
|
||||
},
|
||||
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`;
|
||||
)} kWh ${params.percent ? `(${params.percent} %)` : ""}`;
|
||||
return `${title}${params.marker} ${params.seriesName}: <div style="direction:ltr; display: inline;">${value}</div>`;
|
||||
}
|
||||
|
||||
|
||||
@@ -196,6 +196,10 @@ 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;
|
||||
}
|
||||
@@ -225,10 +229,6 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
|
||||
}
|
||||
}
|
||||
|
||||
if (changedProps.has("hass")) {
|
||||
this._computeNames();
|
||||
}
|
||||
|
||||
if (
|
||||
changedProps.has("_config") &&
|
||||
oldConfig?.entities !== this._config.entities
|
||||
@@ -334,10 +334,7 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard {
|
||||
.maxYAxis=${this._config.max_y_axis}
|
||||
.startTime=${this._energyStart}
|
||||
.endTime=${this._energyEnd && this._energyStart
|
||||
? getSuggestedMax(
|
||||
differenceInDays(this._energyEnd, this._energyStart),
|
||||
this._energyEnd
|
||||
)
|
||||
? getSuggestedMax(this._period!, this._energyEnd)
|
||||
: undefined}
|
||||
.fitYData=${this._config.fit_y_data || false}
|
||||
.hideLegend=${this._config.hide_legend || false}
|
||||
|
||||
@@ -28,11 +28,16 @@ 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 type { EntityConfig } from "../../entity-rows/types";
|
||||
import "../hui-sub-element-editor";
|
||||
import type {
|
||||
EditDetailElementEvent,
|
||||
SubElementEditorConfig,
|
||||
EntitiesEditorEvent,
|
||||
} from "../types";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
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([
|
||||
@@ -76,13 +81,20 @@ 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 _configEntities?: EntityConfig[];
|
||||
@state() private _subElementEditorConfig?: SubElementEditorConfig;
|
||||
|
||||
@state() private _configEntities?: MapEntityConfig[];
|
||||
|
||||
@state() private _possibleGeoSources?: { value: string; label?: string }[];
|
||||
|
||||
@@ -150,7 +162,7 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor {
|
||||
|
||||
this._config = config;
|
||||
this._configEntities = config.entities
|
||||
? processEditorEntities(config.entities)
|
||||
? (processEditorEntities(config.entities) as MapEntityConfig[])
|
||||
: [];
|
||||
}
|
||||
|
||||
@@ -167,6 +179,19 @@ 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}
|
||||
@@ -180,7 +205,9 @@ 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>
|
||||
@@ -203,6 +230,36 @@ 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: {
|
||||
@@ -229,7 +286,9 @@ 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 || []);
|
||||
this._configEntities = processEditorEntities(
|
||||
this._config.entities || []
|
||||
) as MapEntityConfig[];
|
||||
fireEvent(this, "config-changed", { config: this._config! });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,7 +263,8 @@ class HUIRoot extends LitElement {
|
||||
{
|
||||
icon: mdiPlus,
|
||||
key: "ui.panel.lovelace.menu.add",
|
||||
visible: !this._editMode && this.hass.user?.is_admin,
|
||||
visible:
|
||||
!this._editMode && this.hass.user?.is_admin && !this.hass.kioskMode,
|
||||
overflow: this.narrow,
|
||||
subItems: [
|
||||
{
|
||||
@@ -301,7 +302,7 @@ class HUIRoot extends LitElement {
|
||||
key: "ui.panel.lovelace.menu.search_entities",
|
||||
buttonAction: this._showQuickBar,
|
||||
overflowAction: this._handleShowQuickBar,
|
||||
visible: !this._editMode,
|
||||
visible: !this._editMode && !this.hass.kioskMode,
|
||||
overflow: this.narrow,
|
||||
suffix:
|
||||
this.hass.enableShortcuts && !isMobileClient ? "(E)" : undefined,
|
||||
@@ -349,7 +350,8 @@ class HUIRoot extends LitElement {
|
||||
visible:
|
||||
!this._editMode &&
|
||||
this.hass!.user?.is_admin &&
|
||||
!this.hass!.config.recovery_mode,
|
||||
!this.hass!.config.recovery_mode &&
|
||||
!this.hass.kioskMode,
|
||||
overflow: true,
|
||||
overflow_can_promote: true,
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
"custom_name": "Custom name",
|
||||
"no_match": "No entities found"
|
||||
"search": "Search or enter custom name",
|
||||
"custom_name": "Custom name"
|
||||
},
|
||||
"entity-attribute-picker": {
|
||||
"attribute": "Attribute",
|
||||
@@ -685,7 +685,7 @@
|
||||
},
|
||||
"entity-state-content-picker": {
|
||||
"add": "Add",
|
||||
"custom_state": "Custom state"
|
||||
"custom_attribute": "Custom attribute"
|
||||
}
|
||||
},
|
||||
"target-picker": {
|
||||
@@ -772,9 +772,6 @@
|
||||
"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"
|
||||
@@ -4090,7 +4087,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 setup successfully. You may now close this dialog.",
|
||||
"new_automation_setup_timedout_success": "The server has responded and this has now set up successfully. You may now close this dialog.",
|
||||
"item_pasted": "{item} pasted",
|
||||
"ctrl": "Ctrl",
|
||||
"del": "Del",
|
||||
@@ -4119,7 +4116,8 @@
|
||||
"targets": "{count} {count, plural,\n one {target}\n other {targets}\n}",
|
||||
"invalid": "Invalid target",
|
||||
"all_entities": "All entities",
|
||||
"none_entities": "No entities"
|
||||
"none_entities": "No entities",
|
||||
"template": "Template"
|
||||
},
|
||||
"triggers": {
|
||||
"name": "Triggers",
|
||||
@@ -6013,26 +6011,28 @@
|
||||
},
|
||||
"bluetooth": {
|
||||
"title": "Bluetooth",
|
||||
"settings_title": "Bluetooth settings",
|
||||
"tabs": {
|
||||
"overview": "Overview",
|
||||
"advertisements": "Advertisements",
|
||||
"visualization": "Visualization",
|
||||
"connections": "Connections"
|
||||
},
|
||||
"settings_title": "Bluetooth adapters",
|
||||
"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_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_active_connection_support": "This adapter does not support making active (GATT) connections.",
|
||||
"no_connection_slots": "No connection slots",
|
||||
"no_scanner_state_available": "No scanner state available",
|
||||
"current_scanning_mode": "Current scanning mode",
|
||||
"requested_scanning_mode": "Requested scanning mode",
|
||||
"scanner_state_unknown": "State unknown",
|
||||
"scanning_mode_none": "none",
|
||||
"scanning_mode_active": "active",
|
||||
"scanning_mode_passive": "passive",
|
||||
"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.",
|
||||
"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_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",
|
||||
@@ -6050,7 +6050,6 @@
|
||||
"service_uuids": "Service UUIDs",
|
||||
"copy_to_clipboard": "[%key:ui::panel::config::automation::editor::copy_to_clipboard%]",
|
||||
"area": "Area",
|
||||
"core": "Home Assistant",
|
||||
"scanners": "Scanners",
|
||||
"known_devices": "Known devices",
|
||||
"unknown_devices": "Unknown devices"
|
||||
@@ -6253,7 +6252,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 WiFi 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 Wi-Fi 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.",
|
||||
@@ -6827,9 +6826,50 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -7304,7 +7344,7 @@
|
||||
"energy_usage_graph": {
|
||||
"total_consumed": "Total consumed {num} kWh",
|
||||
"total_returned": "Total returned {num} kWh",
|
||||
"total_usage": "{num} kWh used",
|
||||
"total_usage": "+{num} kWh",
|
||||
"combined_from_grid": "Combined from grid",
|
||||
"consumed_solar": "Consumed solar",
|
||||
"consumed_battery": "Consumed battery"
|
||||
|
||||
Reference in New Issue
Block a user