mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-02 22:41:47 +00:00
Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 153290593b | |||
| 189477a1d4 | |||
| 8cd9a5adf6 | |||
| 4d3437b491 | |||
| ceb51714be | |||
| d2c868f904 | |||
| 9f34de5de6 | |||
| 6c9452aa5a | |||
| 2cf79853aa | |||
| 6152812138 | |||
| 5540a6c1ff | |||
| e04297f2bd | |||
| e89f76bbbb | |||
| 319ba3940e | |||
| b9920065a2 | |||
| 3bb5201d41 | |||
| a0648b85ff | |||
| 54f901c7c9 | |||
| 2483a917f8 | |||
| d9cae08f53 | |||
| 106b35d6cf | |||
| f12d305688 | |||
| d2326b4f62 | |||
| ea9424053a |
@@ -57,7 +57,9 @@ gulp.task("gather-gallery-pages", async function gatherPages() {
|
||||
if (descriptionContent === "") {
|
||||
hasDescription = false;
|
||||
} else {
|
||||
descriptionContent = marked(descriptionContent).replace(/`/g, "\\`");
|
||||
descriptionContent = marked(descriptionContent)
|
||||
.replace(/\\/g, "\\\\")
|
||||
.replace(/`/g, "\\`");
|
||||
fs.mkdirSync(path.resolve(galleryBuild, category), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.resolve(galleryBuild, `${pageId}-description.ts`),
|
||||
|
||||
@@ -13,7 +13,7 @@ Our dialogs are based on the latest version of Material Design. Please note that
|
||||
|
||||
- Dialogs have a max width of 560px. Alert and confirmation dialogs have a fixed width of 320px. If you need more width, consider a dedicated page instead.
|
||||
- The close X-icon is on the top left, on all screen sizes. Except for alert and confirmation dialogs, they only have buttons and no X-icon. This is different compared to the Material guidelines.
|
||||
- Dialogs can't be closed with ESC or clicked outside of the dialog when there is a form that the user needs to fill out. Instead it will animate "no" by a little shake.
|
||||
- Dialogs can't be closed with ESC or clicked outside of the dialog when there is a form that the user **has made changes to**. Instead it will animate "no" by a little shake.
|
||||
- Extra icon buttons are on the top right, for example help, settings and expand dialog. More than 2 icon buttons, they will be in an overflow menu.
|
||||
- The submit button is grouped with a cancel button at the bottom right, on all screen sizes. Fullscreen mobile dialogs have them sticky at the bottom.
|
||||
- Keep the labels short, for example `Save`, `Delete`, `Enable`.
|
||||
|
||||
+6
-6
@@ -70,8 +70,8 @@
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@swc/helpers": "0.5.23",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "4.0.5",
|
||||
"@tsparticles/preset-links": "4.0.5",
|
||||
"@tsparticles/engine": "4.1.0",
|
||||
"@tsparticles/preset-links": "4.1.0",
|
||||
"@vibrant/color": "4.0.4",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
@@ -82,7 +82,7 @@
|
||||
"core-js": "3.49.0",
|
||||
"cropperjs": "1.6.2",
|
||||
"culori": "4.0.2",
|
||||
"date-fns": "4.3.0",
|
||||
"date-fns": "4.4.0",
|
||||
"deep-clone-simple": "1.1.1",
|
||||
"deep-freeze": "0.0.1",
|
||||
"dialog-polyfill": "0.5.6",
|
||||
@@ -138,7 +138,7 @@
|
||||
"@octokit/rest": "22.0.1",
|
||||
"@rsdoctor/rspack-plugin": "1.5.12",
|
||||
"@rspack/core": "2.0.5",
|
||||
"@rspack/dev-server": "2.0.1",
|
||||
"@rspack/dev-server": "2.0.3",
|
||||
"@types/chromecast-caf-receiver": "6.0.26",
|
||||
"@types/chromecast-caf-sender": "1.0.11",
|
||||
"@types/color-name": "2.0.0",
|
||||
@@ -158,7 +158,7 @@
|
||||
"babel-plugin-template-html-minifier": "4.1.0",
|
||||
"browserslist-useragent-regexp": "4.1.4",
|
||||
"del": "8.0.1",
|
||||
"eslint": "10.4.0",
|
||||
"eslint": "10.4.1",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-webpack": "0.13.11",
|
||||
"eslint-plugin-import-x": "4.16.2",
|
||||
@@ -180,7 +180,7 @@
|
||||
"jsdom": "29.1.1",
|
||||
"jszip": "3.10.1",
|
||||
"license-checker-rseidelsohn": "5.0.1",
|
||||
"lint-staged": "17.0.5",
|
||||
"lint-staged": "17.0.6",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.18.1",
|
||||
|
||||
@@ -17,6 +17,19 @@ export interface NavigateOptions {
|
||||
// max time to wait for dialogs to close before navigating
|
||||
const DIALOG_WAIT_TIMEOUT = 500;
|
||||
|
||||
/**
|
||||
* Stash a destination URL in the current history entry's state. If the page
|
||||
* is refreshed while a dialog is open, urlSyncMixin will navigate to this URL
|
||||
* on load instead of cleaning up the stale dialog state by going back.
|
||||
* The current URL is not changed.
|
||||
*/
|
||||
export const setRefreshUrl = (path: string) => {
|
||||
mainWindow.history.replaceState(
|
||||
{ ...mainWindow.history.state, refreshUrl: path },
|
||||
""
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Ensures all dialogs are closed before navigation.
|
||||
* Returns true if navigation can proceed, false if a dialog refused to close.
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import type { PickerComboBoxItem } from "../../components/ha-picker-combo-box";
|
||||
import type { RelatedResult } from "../../data/search";
|
||||
|
||||
export interface RelatedIdSets {
|
||||
areas: Set<string>;
|
||||
devices: Set<string>;
|
||||
entities: Set<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a set of related IDs for a given related result.
|
||||
* @param related - The related result to build the sets from.
|
||||
* @returns The related ID sets.
|
||||
*/
|
||||
export const buildRelatedIdSets = (related?: RelatedResult): RelatedIdSets => ({
|
||||
areas: new Set(related?.area || []),
|
||||
devices: new Set(related?.device || []),
|
||||
entities: new Set(related?.entity || []),
|
||||
});
|
||||
|
||||
/**
|
||||
* Stable partition sort: related items float to the top,
|
||||
* preserving relative order (e.g. Fuse score) within each group.
|
||||
* @param items - The items to sort.
|
||||
* @returns The sorted items.
|
||||
*/
|
||||
export const sortRelatedFirst = (
|
||||
items: PickerComboBoxItem[]
|
||||
): PickerComboBoxItem[] =>
|
||||
[...items].sort((a, b) => {
|
||||
const aRelated = Boolean(a.isRelated);
|
||||
const bRelated = Boolean(b.isRelated);
|
||||
if (aRelated === bRelated) {
|
||||
return 0;
|
||||
}
|
||||
return aRelated ? -1 : 1;
|
||||
});
|
||||
@@ -107,17 +107,15 @@ export class HaDevicePicker extends LitElement {
|
||||
excludeDevices?: string[],
|
||||
value?: string
|
||||
) =>
|
||||
getDevices(
|
||||
this.hass,
|
||||
configEntryLookup,
|
||||
getDevices(this.hass, configEntryLookup, {
|
||||
includeDomains,
|
||||
excludeDomains,
|
||||
includeDeviceClasses,
|
||||
deviceFilter,
|
||||
entityFilter,
|
||||
excludeDevices,
|
||||
value
|
||||
)
|
||||
value,
|
||||
})
|
||||
);
|
||||
|
||||
protected firstUpdated(_changedProperties: PropertyValues<this>): void {
|
||||
|
||||
@@ -309,7 +309,29 @@ export class HaEntityPicker extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _getEntitiesMemoized = memoizeOne(getEntities);
|
||||
private _getEntitiesMemoized = memoizeOne(
|
||||
(
|
||||
hass: HomeAssistant,
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
includeDeviceClasses?: string[],
|
||||
includeUnitOfMeasurement?: string[],
|
||||
includeEntities?: string[],
|
||||
excludeEntities?: string[],
|
||||
value?: string
|
||||
) =>
|
||||
getEntities(hass, {
|
||||
includeDomains,
|
||||
excludeDomains,
|
||||
entityFilter,
|
||||
includeDeviceClasses,
|
||||
includeUnitOfMeasurement,
|
||||
includeEntities,
|
||||
excludeEntities,
|
||||
value,
|
||||
})
|
||||
);
|
||||
|
||||
private _getItems = () => {
|
||||
const items = this._getEntitiesMemoized(
|
||||
|
||||
@@ -12,6 +12,7 @@ import { areaComboBoxKeys, getAreas } from "../data/area/area_picker";
|
||||
import { createAreaRegistryEntry } from "../data/area/area_registry";
|
||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { showAreaRegistryDetailDialog } from "../panels/config/areas/show-dialog-area-registry-detail";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../data/entity/entity";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
|
||||
import "./ha-combo-box-item";
|
||||
@@ -104,7 +105,29 @@ export class HaAreaPicker extends LitElement {
|
||||
await this._picker?.open();
|
||||
}
|
||||
|
||||
private _getAreasMemoized = memoizeOne(getAreas);
|
||||
private _getAreasMemoized = memoizeOne(
|
||||
(
|
||||
haAreas: HomeAssistant["areas"],
|
||||
haFloors: HomeAssistant["floors"],
|
||||
haDevices: HomeAssistant["devices"],
|
||||
haEntities: HomeAssistant["entities"],
|
||||
haStates: HomeAssistant["states"],
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
includeDeviceClasses?: string[],
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
excludeAreas?: string[]
|
||||
) =>
|
||||
getAreas(haAreas, haFloors, haDevices, haEntities, haStates, {
|
||||
includeDomains,
|
||||
excludeDomains,
|
||||
includeDeviceClasses,
|
||||
deviceFilter,
|
||||
entityFilter,
|
||||
excludeAreas,
|
||||
})
|
||||
);
|
||||
|
||||
// Recompute value renderer when the areas change
|
||||
private _computeValueRenderer = memoizeOne(
|
||||
|
||||
@@ -20,8 +20,8 @@ import {
|
||||
subscribeCategoryRegistry,
|
||||
updateCategoryRegistryEntry,
|
||||
} from "../data/category_registry";
|
||||
import { showConfirmationDialog } from "../dialogs/generic/show-dialog-box";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { confirmDeleteCategory } from "../panels/config/category/confirm-delete-category";
|
||||
import { showCategoryRegistryDetailDialog } from "../panels/config/category/show-dialog-category-registry-detail";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant } from "../types";
|
||||
@@ -199,17 +199,7 @@ export class HaFilterCategories extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private async _deleteCategory(id: string) {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.category.editor.confirm_delete"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.category.editor.confirm_delete_text"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
});
|
||||
if (!confirm) {
|
||||
if (!(await confirmDeleteCategory(this, this.hass))) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
|
||||
@@ -77,7 +77,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
|
||||
| "bottom-start"
|
||||
| "bottom-end"
|
||||
| "left-start"
|
||||
| "left-end" = "bottom-start";
|
||||
| "left-end" = "bottom";
|
||||
|
||||
/** If set picker shows an add button instead of textbox when value isn't set */
|
||||
@property({ attribute: "add-button-label" }) public addButtonLabel?: string;
|
||||
|
||||
@@ -121,7 +121,6 @@ export class HaIconPicker extends LitElement {
|
||||
.label=${this.label}
|
||||
.value=${this._value}
|
||||
.searchFn=${this._filterIcons}
|
||||
popover-placement="bottom-start"
|
||||
@value-changed=${this._valueChanged}
|
||||
>
|
||||
<slot name="start"></slot>
|
||||
|
||||
@@ -152,7 +152,6 @@ export class HaLanguagePicker extends LitElement {
|
||||
<ha-generic-picker
|
||||
.hass=${this.hass}
|
||||
.autofocus=${this.autofocus}
|
||||
popover-placement="bottom-end"
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
.emptyLabel=${this.hass?.localize(
|
||||
"ui.components.language-picker.no_languages"
|
||||
|
||||
@@ -130,11 +130,56 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
private _newTarget?: TargetItem;
|
||||
|
||||
private _getDevicesMemoized = memoizeOne(getDevices);
|
||||
private _getDevicesMemoized = memoizeOne(
|
||||
(
|
||||
hass: HomeAssistant,
|
||||
configEntryLookup: Record<string, ConfigEntry>,
|
||||
includeDomains?: string[],
|
||||
includeDeviceClasses?: string[],
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
excludeDevices?: string[],
|
||||
value?: string,
|
||||
idPrefix?: string
|
||||
) =>
|
||||
getDevices(hass, configEntryLookup, {
|
||||
includeDomains,
|
||||
includeDeviceClasses,
|
||||
deviceFilter,
|
||||
entityFilter,
|
||||
excludeDevices,
|
||||
value,
|
||||
idPrefix,
|
||||
})
|
||||
);
|
||||
|
||||
private _getLabelsMemoized = memoizeOne(getLabels);
|
||||
|
||||
private _getEntitiesMemoized = memoizeOne(getEntities);
|
||||
private _getEntitiesMemoized = memoizeOne(
|
||||
(
|
||||
hass: HomeAssistant,
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
includeDeviceClasses?: string[],
|
||||
includeUnitOfMeasurement?: string[],
|
||||
includeEntities?: string[],
|
||||
excludeEntities?: string[],
|
||||
value?: string,
|
||||
idPrefix?: string
|
||||
) =>
|
||||
getEntities(hass, {
|
||||
includeDomains,
|
||||
excludeDomains,
|
||||
entityFilter,
|
||||
includeDeviceClasses,
|
||||
includeUnitOfMeasurement,
|
||||
includeEntities,
|
||||
excludeEntities,
|
||||
value,
|
||||
idPrefix,
|
||||
})
|
||||
);
|
||||
|
||||
private _getAreasAndFloorsMemoized = memoizeOne(getAreasAndFloors);
|
||||
|
||||
@@ -919,7 +964,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
this.hass,
|
||||
configEntryLookup,
|
||||
includeDomains,
|
||||
undefined,
|
||||
includeDeviceClasses,
|
||||
deviceFilter,
|
||||
entityFilter,
|
||||
|
||||
@@ -82,7 +82,6 @@ export class HaThemePicker extends LitElement {
|
||||
.disabled=${this.disabled}
|
||||
.required=${this.required}
|
||||
@value-changed=${this._changed}
|
||||
popover-placement="bottom"
|
||||
></ha-generic-picker>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -15,20 +15,33 @@ import {
|
||||
import type { HaEntityPickerEntityFilterFunc } from "../entity/entity";
|
||||
import type { EntityRegistryDisplayEntry } from "../entity/entity_registry";
|
||||
|
||||
export interface GetAreasOptions {
|
||||
includeDomains?: string[];
|
||||
excludeDomains?: string[];
|
||||
includeDeviceClasses?: string[];
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||
excludeAreas?: string[];
|
||||
idPrefix?: string;
|
||||
}
|
||||
|
||||
export const getAreas = (
|
||||
haAreas: HomeAssistant["areas"],
|
||||
haFloors: HomeAssistant["floors"],
|
||||
haDevices: HomeAssistant["devices"],
|
||||
haEntities: HomeAssistant["entities"],
|
||||
haStates: HomeAssistant["states"],
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
includeDeviceClasses?: string[],
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
excludeAreas?: string[],
|
||||
idPrefix = ""
|
||||
options?: GetAreasOptions
|
||||
): PickerComboBoxItem[] => {
|
||||
const {
|
||||
includeDomains,
|
||||
excludeDomains,
|
||||
includeDeviceClasses,
|
||||
deviceFilter,
|
||||
entityFilter,
|
||||
excludeAreas,
|
||||
idPrefix = "",
|
||||
} = options ?? {};
|
||||
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||
let inputEntities: EntityRegistryDisplayEntry[] | undefined;
|
||||
|
||||
@@ -485,6 +485,17 @@ export const migrateAutomationTrigger = (
|
||||
}
|
||||
delete trigger.platform;
|
||||
}
|
||||
|
||||
if ("options" in trigger) {
|
||||
if (trigger.options && "behavior" in trigger.options) {
|
||||
if (trigger.options.behavior === "any") {
|
||||
trigger.options.behavior = "each";
|
||||
} else if (trigger.options.behavior === "last") {
|
||||
trigger.options.behavior = "all";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return trigger;
|
||||
};
|
||||
|
||||
|
||||
@@ -4,8 +4,9 @@ import type { Store } from "home-assistant-js-websocket/dist/store";
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import type { RegistryEntry } from "./registry";
|
||||
|
||||
export interface CategoryRegistryEntry {
|
||||
export interface CategoryRegistryEntry extends RegistryEntry {
|
||||
category_id: string;
|
||||
name: string;
|
||||
icon: string | null;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createContext } from "@lit/context";
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
HomeAssistant,
|
||||
HomeAssistantApi,
|
||||
@@ -10,10 +11,12 @@ import type {
|
||||
HomeAssistantRegistries,
|
||||
HomeAssistantUI,
|
||||
} from "../../types";
|
||||
import type { RelatedIdSets } from "../../common/search/related-context";
|
||||
import type { ConfigEntry } from "../config_entries";
|
||||
import type { EntityRegistryEntry } from "../entity/entity_registry";
|
||||
import type { DomainManifestLookup } from "../integration";
|
||||
import type { LabelRegistryEntry } from "../label/label_registry";
|
||||
import type { ItemType } from "../search";
|
||||
|
||||
/**
|
||||
* Entity, device, area, and floor registries
|
||||
@@ -94,6 +97,11 @@ export const areasContext = createContext<HomeAssistant["areas"]>("areas");
|
||||
*/
|
||||
export const floorsContext = createContext<HomeAssistant["floors"]>("floors");
|
||||
|
||||
/**
|
||||
* Whether the main Home Assistant viewport is using the narrow layout.
|
||||
*/
|
||||
export const narrowViewportContext = createContext<boolean>("narrowViewport");
|
||||
|
||||
// #region lazy-contexts
|
||||
|
||||
/**
|
||||
@@ -162,3 +170,30 @@ export const panelsContext = createContext<HomeAssistant["panels"]>("panels");
|
||||
export const authContext = createContext<HomeAssistant["auth"]>("auth");
|
||||
|
||||
// #endregion deprecated-contexts
|
||||
|
||||
// #region related-context
|
||||
|
||||
export interface RelatedContextItem {
|
||||
itemType: ItemType;
|
||||
itemId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolved related entities/devices/areas for the current page context.
|
||||
* Set by `RelatedContextProvider` when a page fires `hass-related-context`.
|
||||
* Cleared on navigation.
|
||||
*/
|
||||
export const relatedContext = createContext<RelatedIdSets | undefined>(
|
||||
"related"
|
||||
);
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"hass-related-context": RelatedContextItem | undefined;
|
||||
}
|
||||
interface HTMLElementEventMap {
|
||||
"hass-related-context": HASSDomEvent<RelatedContextItem | undefined>;
|
||||
}
|
||||
}
|
||||
|
||||
// #endregion related-context
|
||||
|
||||
@@ -32,6 +32,17 @@ export interface DeviceAreaLabel {
|
||||
viaDeviceAreaName?: string;
|
||||
}
|
||||
|
||||
export interface GetDevicesOptions {
|
||||
includeDomains?: string[];
|
||||
excludeDomains?: string[];
|
||||
includeDeviceClasses?: string[];
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc;
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||
excludeDevices?: string[];
|
||||
value?: string;
|
||||
idPrefix?: string;
|
||||
}
|
||||
|
||||
export const computeDeviceAreaLabel = (
|
||||
device: DeviceRegistryEntry,
|
||||
areas: HomeAssistant["areas"],
|
||||
@@ -96,15 +107,19 @@ export const deviceComboBoxKeys: FuseWeightedKey[] = [
|
||||
export const getDevices = (
|
||||
hass: HomeAssistant,
|
||||
configEntryLookup: Record<string, ConfigEntry>,
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
includeDeviceClasses?: string[],
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
excludeDevices?: string[],
|
||||
value?: string,
|
||||
idPrefix = ""
|
||||
options?: GetDevicesOptions
|
||||
): DevicePickerItem[] => {
|
||||
const {
|
||||
includeDomains,
|
||||
excludeDomains,
|
||||
includeDeviceClasses,
|
||||
deviceFilter,
|
||||
entityFilter,
|
||||
excludeDevices,
|
||||
value,
|
||||
idPrefix = "",
|
||||
} = options ?? {};
|
||||
|
||||
const devices = Object.values(hass.devices);
|
||||
const entities = Object.values(hass.entities);
|
||||
|
||||
|
||||
+16
-1
@@ -222,6 +222,12 @@ export interface EnergyPreferences {
|
||||
device_consumption_water: DeviceConsumptionEnergyPreference[];
|
||||
}
|
||||
|
||||
export const EMPTY_PREFERENCES: EnergyPreferences = {
|
||||
energy_sources: [],
|
||||
device_consumption: [],
|
||||
device_consumption_water: [],
|
||||
};
|
||||
|
||||
export interface EnergyInfo {
|
||||
cost_sensors: Record<string, string>;
|
||||
solar_forecast_domains: string[];
|
||||
@@ -802,7 +808,16 @@ export const getEnergyDataCollection = (
|
||||
if (!collection.prefs) {
|
||||
// This will raise if not found.
|
||||
// Detect by checking `e.code === "not_found"
|
||||
collection.prefs = await getEnergyPreferences(hass);
|
||||
try {
|
||||
collection.prefs = await getEnergyPreferences(hass);
|
||||
} catch (err: any) {
|
||||
if (err.code === "not_found") {
|
||||
return {
|
||||
prefs: EMPTY_PREFERENCES,
|
||||
} as EnergyData;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
scheduleHourlyRefresh(collection);
|
||||
|
||||
@@ -41,18 +41,34 @@ export const entityComboBoxKeys: FuseWeightedKey[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export interface GetEntitiesOptions {
|
||||
includeDomains?: string[];
|
||||
excludeDomains?: string[];
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc;
|
||||
includeDeviceClasses?: string[];
|
||||
includeUnitOfMeasurement?: string[];
|
||||
includeEntities?: string[];
|
||||
excludeEntities?: string[];
|
||||
value?: string;
|
||||
idPrefix?: string;
|
||||
}
|
||||
|
||||
export const getEntities = (
|
||||
hass: HomeAssistant,
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
includeDeviceClasses?: string[],
|
||||
includeUnitOfMeasurement?: string[],
|
||||
includeEntities?: string[],
|
||||
excludeEntities?: string[],
|
||||
value?: string,
|
||||
idPrefix = ""
|
||||
options?: GetEntitiesOptions
|
||||
): EntityComboBoxItem[] => {
|
||||
const {
|
||||
includeDomains,
|
||||
excludeDomains,
|
||||
entityFilter,
|
||||
includeDeviceClasses,
|
||||
includeUnitOfMeasurement,
|
||||
includeEntities,
|
||||
excludeEntities,
|
||||
value,
|
||||
idPrefix = "",
|
||||
} = options ?? {};
|
||||
|
||||
let items: EntityComboBoxItem[];
|
||||
|
||||
let entityIds = Object.keys(hass.states);
|
||||
|
||||
+2
-2
@@ -36,11 +36,11 @@ export type ItemType =
|
||||
| "script_blueprint";
|
||||
|
||||
export const findRelated = (
|
||||
hass: HomeAssistant,
|
||||
hass: Pick<HomeAssistant, "callWS">,
|
||||
itemType: ItemType,
|
||||
itemId: string
|
||||
): Promise<RelatedResult> =>
|
||||
hass.callWS({
|
||||
hass.callWS<RelatedResult>({
|
||||
type: "search/related",
|
||||
item_type: itemType,
|
||||
item_id: itemId,
|
||||
|
||||
@@ -87,6 +87,19 @@ const HOME_ASSISTANT_CORE_TITLE = "Home Assistant Core";
|
||||
const HOME_ASSISTANT_SUPERVISOR_TITLE = "Home Assistant Supervisor";
|
||||
const HOME_ASSISTANT_OS_TITLE = "Home Assistant Operating System";
|
||||
|
||||
// The hassio integration sets these as hard-coded `_attr_title` on the Core,
|
||||
// Operating System, and Supervisor update entities. They are not translated,
|
||||
// so a title comparison is the reliable way to identify them without depending
|
||||
// on the (lazily-fetched) entity sources.
|
||||
export const isSystemUpdate = (entity: UpdateEntity): boolean => {
|
||||
const title = entity.attributes.title || "";
|
||||
return (
|
||||
title === HOME_ASSISTANT_CORE_TITLE ||
|
||||
title === HOME_ASSISTANT_OS_TITLE ||
|
||||
title === HOME_ASSISTANT_SUPERVISOR_TITLE
|
||||
);
|
||||
};
|
||||
|
||||
export const filterUpdateEntities = (
|
||||
entities: HassEntities,
|
||||
language?: string
|
||||
@@ -133,6 +146,11 @@ export const filterUpdateEntitiesParameterized = (
|
||||
return updateCanInstall(entity, showSkipped);
|
||||
});
|
||||
|
||||
export const installUpdates = (hass: HomeAssistant, entityIds: string[]) =>
|
||||
hass.callService("update", "install", {
|
||||
entity_id: entityIds,
|
||||
});
|
||||
|
||||
export const checkForEntityUpdates = async (
|
||||
element: HTMLElement,
|
||||
hass: HomeAssistant
|
||||
|
||||
@@ -1,26 +1,37 @@
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import type { LocalizeKeys } from "../../common/translations/localize";
|
||||
import { createSearchParam } from "../../common/url/search-params";
|
||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import { SCENE_IGNORED_DOMAINS, type SceneEntities } from "../../data/scene";
|
||||
import type { SingleHassServiceTarget } from "../../data/target";
|
||||
import {
|
||||
ADD_AUTOMATION_ELEMENT_AREA_TARGET_PARAM,
|
||||
ADD_AUTOMATION_ELEMENT_DEVICE_TARGET_PARAM,
|
||||
ADD_AUTOMATION_ELEMENT_QUERY_PARAM,
|
||||
ADD_AUTOMATION_ELEMENT_ENTITY_TARGET_PARAM,
|
||||
ADD_AUTOMATION_ELEMENT_QUERY_PARAM,
|
||||
} from "../../panels/config/automation/show-add-automation-element-dialog";
|
||||
import type { HomeAssistant, TranslationDict } from "../../types";
|
||||
|
||||
/** Add to action keys are the keys of the translation dictionary for the add to actions. */
|
||||
export type AddToActionKey =
|
||||
TranslationDict["ui"]["dialogs"]["more_info_control"]["add_to"]["actions"] extends infer Actions
|
||||
? keyof Actions
|
||||
: never;
|
||||
/** Add to action keys are the keys of the translation dictionary for the add to action options. */
|
||||
type AddToActionOptions =
|
||||
TranslationDict["ui"]["dialogs"]["more_info_control"]["add_to"]["action_options"];
|
||||
|
||||
export type AddToActionKey = Extract<keyof AddToActionOptions, string>;
|
||||
|
||||
export type AddToAutomationScriptActionKey = Exclude<AddToActionKey, "scene">;
|
||||
|
||||
/** Fully-qualified localize key for an add to action option label. */
|
||||
type AddToActionOptionLabelKey = LocalizeKeys &
|
||||
`ui.dialogs.more_info_control.add_to.action_options.${AddToActionKey}`;
|
||||
|
||||
interface BaseEntityAddToAction {
|
||||
/** Whether the action is enabled and can be selected. */
|
||||
enabled: boolean;
|
||||
/** Translated name of the action */
|
||||
name: string;
|
||||
/** Translated label of the action option */
|
||||
name?: string;
|
||||
/** Fully-qualified localize key for the action option label */
|
||||
nameKey?: AddToActionOptionLabelKey;
|
||||
/** Optional translated description of the action */
|
||||
description?: string;
|
||||
/** MDI icon name (e.g., "mdi:car") */
|
||||
@@ -31,7 +42,7 @@ export interface DefaultEntityAddToAction extends BaseEntityAddToAction {
|
||||
/** Type of action handled in the frontend */
|
||||
type: "default";
|
||||
/** Stable key used to resolve the action handler */
|
||||
key: AddToActionKey;
|
||||
key: AddToAutomationScriptActionKey;
|
||||
}
|
||||
|
||||
export interface ExternalEntityAddToAction extends BaseEntityAddToAction {
|
||||
@@ -48,11 +59,11 @@ export type EntityAddToAction =
|
||||
export type EntityAddToActions = EntityAddToAction[];
|
||||
|
||||
interface ActionDefinition {
|
||||
translation_key: AddToActionKey;
|
||||
translation_key: AddToAutomationScriptActionKey;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_ACTION_DEFS: ActionDefinition[] = [
|
||||
const DEFAULT_ACTION_DEFS: ActionDefinition[] = [
|
||||
{
|
||||
translation_key: "automation_trigger",
|
||||
icon: "mdi:robot-outline",
|
||||
@@ -71,33 +82,49 @@ export const DEFAULT_ACTION_DEFS: ActionDefinition[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const getDefaultAddToActions = (
|
||||
states: HomeAssistant["states"],
|
||||
localize: LocalizeFunc,
|
||||
formatEntityName: HomeAssistant["formatEntityName"],
|
||||
entityId: string
|
||||
): EntityAddToActions =>
|
||||
export const getDefaultAddToActions = (): EntityAddToActions =>
|
||||
DEFAULT_ACTION_DEFS.map(
|
||||
(def: ActionDefinition): EntityAddToAction => ({
|
||||
type: "default",
|
||||
key: def.translation_key,
|
||||
enabled: true,
|
||||
name: localize(
|
||||
`ui.dialogs.more_info_control.add_to.actions.${def.translation_key}`,
|
||||
{
|
||||
target:
|
||||
states[entityId] !== undefined
|
||||
? formatEntityName(states[entityId], undefined)
|
||||
: entityId,
|
||||
}
|
||||
),
|
||||
nameKey: `ui.dialogs.more_info_control.add_to.action_options.${def.translation_key}`,
|
||||
icon: def.icon,
|
||||
})
|
||||
);
|
||||
|
||||
export const createAddToSceneEntities = (
|
||||
entityIds: string[]
|
||||
): SceneEntities => {
|
||||
const entities: SceneEntities = {};
|
||||
for (const entityId of entityIds) {
|
||||
entities[entityId] = "";
|
||||
}
|
||||
return entities;
|
||||
};
|
||||
|
||||
export const filterAddToSceneEntityIds = (
|
||||
entityIds: string[],
|
||||
entityRegistry: readonly EntityRegistryEntry[],
|
||||
states: HomeAssistant["states"]
|
||||
): string[] => {
|
||||
const entityIdSet = new Set(entityIds);
|
||||
|
||||
return entityRegistry
|
||||
.filter((entry) => entityIdSet.has(entry.entity_id))
|
||||
.filter(
|
||||
(entry) =>
|
||||
!entry.entity_category &&
|
||||
!entry.hidden_by &&
|
||||
!SCENE_IGNORED_DOMAINS.includes(computeDomain(entry.entity_id)) &&
|
||||
states[entry.entity_id]
|
||||
)
|
||||
.map((entry) => entry.entity_id);
|
||||
};
|
||||
|
||||
/** Handler for adding a target to an automation/script. */
|
||||
export function addToActionHandler(
|
||||
key: AddToActionKey,
|
||||
key: AddToAutomationScriptActionKey,
|
||||
target: SingleHassServiceTarget
|
||||
): Promise<boolean> {
|
||||
const searchParams: Record<string, string> = {};
|
||||
@@ -0,0 +1,211 @@
|
||||
import { mdiPlus } from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type {
|
||||
HASSDomCurrentTargetEvent,
|
||||
HASSDomEvent,
|
||||
} from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type {
|
||||
LocalizeFunc,
|
||||
LocalizeKeys,
|
||||
} from "../../common/translations/localize";
|
||||
import "../../components/ha-icon";
|
||||
import "../../components/ha-svg-icon";
|
||||
import type { HaListItemButton } from "../../components/item/ha-list-item-button";
|
||||
import "../../components/item/ha-list-item-button";
|
||||
import "../../components/list/ha-list-base";
|
||||
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
|
||||
|
||||
export interface AddToActionListItem {
|
||||
name?: string;
|
||||
nameKey?: LocalizeKeys;
|
||||
description?: string;
|
||||
descriptionKey?: LocalizeKeys;
|
||||
icon?: string;
|
||||
iconPath?: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface AddToActionListSection<
|
||||
Item extends AddToActionListItem = AddToActionListItem,
|
||||
> {
|
||||
title?: string;
|
||||
titleKey?: LocalizeKeys;
|
||||
actions: readonly Item[];
|
||||
empty?: string;
|
||||
emptyKey?: LocalizeKeys;
|
||||
}
|
||||
|
||||
export interface AddToActionListActionSelectedDetail<
|
||||
Item extends AddToActionListItem = AddToActionListItem,
|
||||
> {
|
||||
action: Item;
|
||||
}
|
||||
|
||||
export type AddToActionListActionSelectedEvent<
|
||||
Item extends AddToActionListItem = AddToActionListItem,
|
||||
> = HASSDomEvent<AddToActionListActionSelectedDetail<Item>>;
|
||||
|
||||
@customElement("ha-add-to-action-list")
|
||||
class HaAddToActionList extends LitElement {
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@property({ attribute: false })
|
||||
public sections: readonly AddToActionListSection[] = [];
|
||||
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
if (!this.sections.length) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`${this.sections.map((section, sectionIndex) =>
|
||||
this._renderSection(section, sectionIndex)
|
||||
)}`;
|
||||
}
|
||||
|
||||
private _renderSection(
|
||||
section: AddToActionListSection,
|
||||
sectionIndex: number
|
||||
): TemplateResult | typeof nothing {
|
||||
if (!section.actions.length && !section.empty && !section.emptyKey) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<h3 class="section-header">
|
||||
${this._localizeValue(section.title, section.titleKey)}
|
||||
</h3>
|
||||
${section.actions.length
|
||||
? html`<ha-list-base>
|
||||
${section.actions.map((action, actionIndex) =>
|
||||
this._renderActionItem(action, sectionIndex, actionIndex)
|
||||
)}
|
||||
</ha-list-base>`
|
||||
: html`<h4 class="empty">
|
||||
${this._localizeValue(section.empty, section.emptyKey)}
|
||||
</h4>`}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderActionItem(
|
||||
action: AddToActionListItem,
|
||||
sectionIndex: number,
|
||||
actionIndex: number
|
||||
): TemplateResult {
|
||||
return html`
|
||||
<ha-list-item-button
|
||||
.disabled=${action.enabled === false}
|
||||
data-section-index=${sectionIndex}
|
||||
data-action-index=${actionIndex}
|
||||
.headline=${this._localizeValue(action.name, action.nameKey)}
|
||||
.supportingText=${this._localizeValue(
|
||||
action.description,
|
||||
action.descriptionKey
|
||||
)}
|
||||
@click=${this._actionSelected}
|
||||
>
|
||||
${action.icon
|
||||
? html`<ha-icon
|
||||
class="start-icon"
|
||||
slot="start"
|
||||
.icon=${action.icon}
|
||||
></ha-icon>`
|
||||
: action.iconPath
|
||||
? html`<ha-svg-icon
|
||||
class="start-icon"
|
||||
slot="start"
|
||||
.path=${action.iconPath}
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
<ha-svg-icon class="plus" slot="end" .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-list-item-button>
|
||||
`;
|
||||
}
|
||||
|
||||
private _localizeValue(
|
||||
value?: string,
|
||||
localizeKey?: LocalizeKeys
|
||||
): string | undefined {
|
||||
return value || (localizeKey ? this._localize(localizeKey) : undefined);
|
||||
}
|
||||
|
||||
private _actionSelected(
|
||||
ev: HASSDomCurrentTargetEvent<HaListItemButton>
|
||||
): void {
|
||||
const action =
|
||||
this.sections[Number(ev.currentTarget.dataset.sectionIndex)]?.actions[
|
||||
Number(ev.currentTarget.dataset.actionIndex)
|
||||
];
|
||||
|
||||
if (!action) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.enabled === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
fireEvent(this, "add-to-list-action-selected", {
|
||||
action,
|
||||
});
|
||||
}
|
||||
|
||||
static styles: CSSResultGroup = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
padding: var(--ha-space-2) var(--ha-space-6) var(--ha-space-1);
|
||||
margin: 0;
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: var(--ha-space-2) var(--ha-space-6) var(--ha-space-1);
|
||||
margin: 0;
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
ha-list-item-button {
|
||||
--ha-row-item-padding-inline: var(--ha-space-5);
|
||||
}
|
||||
|
||||
ha-icon,
|
||||
ha-svg-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.start-icon {
|
||||
color: var(--ha-color-text-secondary);
|
||||
}
|
||||
|
||||
.plus {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
ha-list-item-button[disabled] .start-icon,
|
||||
ha-list-item-button[disabled] .plus {
|
||||
color: var(--disabled-text-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-add-to-action-list": HaAddToActionList;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"add-to-list-action-selected": AddToActionListActionSelectedDetail;
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,35 @@
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../components/ha-alert";
|
||||
import "../../components/ha-icon";
|
||||
import "../../components/ha-spinner";
|
||||
import "../../components/item/ha-list-item-button";
|
||||
import "../../components/list/ha-list-base";
|
||||
import type { HaListItemButton } from "../../components/item/ha-list-item-button";
|
||||
import { showToast } from "../../util/toast";
|
||||
|
||||
import type { HASSDomCurrentTargetEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { configContext } from "../../data/context";
|
||||
import "../add-to/ha-add-to-action-list";
|
||||
import type {
|
||||
AddToActionListActionSelectedEvent,
|
||||
AddToActionListSection,
|
||||
} from "../add-to/ha-add-to-action-list";
|
||||
import {
|
||||
type EntityAddToAction,
|
||||
type EntityAddToActions,
|
||||
addToActionHandler,
|
||||
getDefaultAddToActions,
|
||||
} from "./add-to";
|
||||
} from "../add-to/add-to";
|
||||
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
|
||||
@customElement("ha-more-info-add-to")
|
||||
export class HaMoreInfoAddTo extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
private _config?: ContextType<typeof configContext>;
|
||||
|
||||
@state()
|
||||
@consumeLocalize()
|
||||
private _localize!: LocalizeFunc;
|
||||
|
||||
@property({ attribute: false }) public entityId!: string;
|
||||
|
||||
@@ -31,18 +40,13 @@ export class HaMoreInfoAddTo extends LitElement {
|
||||
@state() private _loading = true;
|
||||
|
||||
private async _loadActions() {
|
||||
this._defaultActions = getDefaultAddToActions(
|
||||
this.hass.states,
|
||||
this.hass.localize,
|
||||
this.hass.formatEntityName,
|
||||
this.entityId
|
||||
);
|
||||
this._defaultActions = getDefaultAddToActions();
|
||||
this._externalActions = [];
|
||||
|
||||
if (this.hass.auth.external?.config.hasEntityAddTo) {
|
||||
if (this._config?.auth.external?.config.hasEntityAddTo) {
|
||||
try {
|
||||
const response =
|
||||
await this.hass.auth.external?.sendMessage<"entity/add_to/get_actions">(
|
||||
await this._config.auth.external.sendMessage<"entity/add_to/get_actions">(
|
||||
{
|
||||
type: "entity/add_to/get_actions",
|
||||
payload: { entity_id: this.entityId },
|
||||
@@ -66,13 +70,9 @@ export class HaMoreInfoAddTo extends LitElement {
|
||||
}
|
||||
|
||||
private async _actionSelected(
|
||||
ev: HASSDomCurrentTargetEvent<
|
||||
HaListItemButton & {
|
||||
action: EntityAddToAction;
|
||||
}
|
||||
>
|
||||
ev: AddToActionListActionSelectedEvent<EntityAddToAction>
|
||||
) {
|
||||
const action = ev.currentTarget.action;
|
||||
const { action } = ev.detail;
|
||||
if (!action.enabled) {
|
||||
return;
|
||||
}
|
||||
@@ -82,7 +82,10 @@ export class HaMoreInfoAddTo extends LitElement {
|
||||
if (!action.payload) {
|
||||
throw new Error("Missing external action payload");
|
||||
}
|
||||
this.hass.auth.external!.fireMessage({
|
||||
if (!this._config?.auth.external) {
|
||||
throw new Error("Missing external app connection");
|
||||
}
|
||||
this._config.auth.external.fireMessage({
|
||||
type: "entity/add_to",
|
||||
payload: {
|
||||
entity_id: this.entityId,
|
||||
@@ -92,7 +95,7 @@ export class HaMoreInfoAddTo extends LitElement {
|
||||
fireEvent(this, "add-to-action-selected");
|
||||
} catch (err: unknown) {
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
message: this._localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_failed",
|
||||
{
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
@@ -110,24 +113,6 @@ export class HaMoreInfoAddTo extends LitElement {
|
||||
addToActionHandler(action.key, { entity_id: this.entityId });
|
||||
}
|
||||
|
||||
private _renderActionItems(actions: EntityAddToActions) {
|
||||
return actions.map(
|
||||
(action) => html`
|
||||
<ha-list-item-button
|
||||
.disabled=${!action.enabled}
|
||||
.action=${action}
|
||||
@click=${this._actionSelected}
|
||||
>
|
||||
<ha-icon slot="start" .icon=${action.icon}></ha-icon>
|
||||
<span slot="headline">${action.name}</span>
|
||||
${action.description
|
||||
? html`<span slot="supporting-text">${action.description}</span>`
|
||||
: nothing}
|
||||
</ha-list-item-button>
|
||||
`
|
||||
);
|
||||
}
|
||||
|
||||
protected async firstUpdated() {
|
||||
await this._loadActions();
|
||||
this._loading = false;
|
||||
@@ -145,29 +130,38 @@ export class HaMoreInfoAddTo extends LitElement {
|
||||
if (!this._defaultActions.length && !this._externalActions.length) {
|
||||
return html`
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_to.no_actions"
|
||||
)}
|
||||
${this._localize("ui.dialogs.more_info_control.add_to.no_actions")}
|
||||
</ha-alert>
|
||||
`;
|
||||
}
|
||||
|
||||
const automationActions = this._defaultActions.filter(
|
||||
(action) => action.type === "default" && action.key !== "script_action"
|
||||
);
|
||||
const scriptActions = this._defaultActions.filter(
|
||||
(action) => action.type === "default" && action.key === "script_action"
|
||||
);
|
||||
|
||||
const sections: AddToActionListSection<EntityAddToAction>[] = [
|
||||
{
|
||||
titleKey: "ui.dialogs.more_info_control.add_to.automations_heading",
|
||||
actions: automationActions,
|
||||
},
|
||||
{
|
||||
titleKey: "ui.dialogs.more_info_control.add_to.scripts_heading",
|
||||
actions: scriptActions,
|
||||
},
|
||||
{
|
||||
titleKey: "ui.dialogs.more_info_control.add_to.app_actions",
|
||||
actions: this._externalActions,
|
||||
},
|
||||
];
|
||||
|
||||
return html`
|
||||
<ha-list-base>
|
||||
${this._renderActionItems(this._defaultActions)}
|
||||
</ha-list-base>
|
||||
${this._externalActions.length
|
||||
? html`
|
||||
<h2 class="section-title">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_to.app_actions"
|
||||
)}
|
||||
</h2>
|
||||
<ha-list-base>
|
||||
${this._renderActionItems(this._externalActions)}
|
||||
</ha-list-base>
|
||||
`
|
||||
: nothing}
|
||||
<ha-add-to-action-list
|
||||
.sections=${sections}
|
||||
@add-to-list-action-selected=${this._actionSelected}
|
||||
></ha-add-to-action-list>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -183,20 +177,6 @@ export class HaMoreInfoAddTo extends LitElement {
|
||||
align-items: center;
|
||||
padding: var(--ha-space-8);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
padding: 0 var(--ha-space-6);
|
||||
margin: var(--ha-space-4) 0 var(--ha-space-1);
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
ha-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import { customElement, property, query, state } from "lit/decorators";
|
||||
import { cache } from "lit/directives/cache";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { keyed } from "lit/directives/keyed";
|
||||
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
|
||||
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
@@ -517,7 +518,7 @@ export class MoreInfoDialog extends SubscribeMixin(
|
||||
await favoritesHandler.copy(favoritesContext);
|
||||
}
|
||||
|
||||
private _goToAddEntityTo(ev) {
|
||||
private _goToAddEntityTo(ev: CustomEvent<RequestSelectedDetail>) {
|
||||
// Only check for request-selected events (from menu items), not regular clicks (from icon button)
|
||||
if (
|
||||
ev.type === "request-selected" &&
|
||||
@@ -590,10 +591,19 @@ export class MoreInfoDialog extends SubscribeMixin(
|
||||
(v): v is string => Boolean(v)
|
||||
);
|
||||
const defaultTitle = breadcrumb.pop() || entityId;
|
||||
const addToTitle = this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_to.title",
|
||||
{ target: defaultTitle }
|
||||
);
|
||||
const addToMenuItem = this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_to.item"
|
||||
);
|
||||
const title =
|
||||
this._currView === "details"
|
||||
? this.hass.localize("ui.dialogs.more_info_control.details")
|
||||
: this._childView?.viewTitle || defaultTitle;
|
||||
: this._currView === "add_to"
|
||||
? addToTitle
|
||||
: this._childView?.viewTitle || defaultTitle;
|
||||
|
||||
const favoritesContext =
|
||||
this._entry && stateObj
|
||||
@@ -711,9 +721,7 @@ export class MoreInfoDialog extends SubscribeMixin(
|
||||
slot="icon"
|
||||
.path=${mdiPlusBoxMultipleOutline}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_to.title"
|
||||
)}
|
||||
${addToMenuItem}
|
||||
</ha-dropdown-item>
|
||||
|
||||
<wa-divider></wa-divider>
|
||||
@@ -814,9 +822,7 @@ export class MoreInfoDialog extends SubscribeMixin(
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="headerActionItems"
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_to.title"
|
||||
)}
|
||||
.label=${addToMenuItem}
|
||||
.path=${mdiPlusBoxMultipleOutline}
|
||||
@click=${this._goToAddEntityTo}
|
||||
></ha-icon-button>
|
||||
@@ -906,7 +912,6 @@ export class MoreInfoDialog extends SubscribeMixin(
|
||||
: this._currView === "add_to"
|
||||
? html`
|
||||
<ha-more-info-add-to
|
||||
.hass=${this.hass}
|
||||
.entityId=${entityId}
|
||||
@add-to-action-selected=${this._goBack}
|
||||
></ha-more-info-add-to>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { mdiDevices } from "@mdi/js";
|
||||
import { consume } from "@lit/context";
|
||||
import Fuse from "fuse.js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
@@ -47,7 +48,9 @@ import {
|
||||
type ActionCommandComboBoxItem,
|
||||
type NavigationComboBoxItem,
|
||||
} from "../../data/quick_bar";
|
||||
import type { RelatedResult } from "../../data/search";
|
||||
import type { RelatedIdSets } from "../../common/search/related-context";
|
||||
import { sortRelatedFirst } from "../../common/search/related-context";
|
||||
import { relatedContext } from "../../data/context";
|
||||
import {
|
||||
multiTermSortedSearch,
|
||||
type FuseWeightedKey,
|
||||
@@ -70,6 +73,10 @@ const SEPARATOR = "________";
|
||||
export class QuickBar extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state()
|
||||
@consume({ context: relatedContext, subscribe: true })
|
||||
private _relatedIdSets?: RelatedIdSets;
|
||||
|
||||
@state() private _open = false;
|
||||
|
||||
@state() private _loading = true;
|
||||
@@ -80,8 +87,6 @@ export class QuickBar extends LitElement {
|
||||
|
||||
@state() private _opened = false;
|
||||
|
||||
@state() private _relatedResult?: RelatedResult;
|
||||
|
||||
@query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
|
||||
|
||||
private get _showEntityId() {
|
||||
@@ -108,8 +113,6 @@ export class QuickBar extends LitElement {
|
||||
this._selectedSection = effectiveQuickBarMode(this.hass.user, params.mode);
|
||||
this._showHint = params.showHint ?? false;
|
||||
|
||||
this._relatedResult = params.contextItem ? params.related : undefined;
|
||||
|
||||
this._open = true;
|
||||
}
|
||||
|
||||
@@ -432,7 +435,7 @@ export class QuickBar extends LitElement {
|
||||
this._selectedSection = section as QuickBarSection | undefined;
|
||||
return this._getItemsMemoized(
|
||||
this._configEntryLookup,
|
||||
this._relatedResult,
|
||||
this._relatedIdSets,
|
||||
searchString,
|
||||
this._selectedSection
|
||||
);
|
||||
@@ -441,12 +444,11 @@ export class QuickBar extends LitElement {
|
||||
private _getItemsMemoized = memoizeOne(
|
||||
(
|
||||
configEntryLookup: Record<string, ConfigEntry>,
|
||||
relatedResult: RelatedResult | undefined,
|
||||
relatedIdSets: RelatedIdSets | undefined,
|
||||
filter?: string,
|
||||
section?: QuickBarSection
|
||||
) => {
|
||||
const items: (string | PickerComboBoxItem)[] = [];
|
||||
const relatedIdSets = this._getRelatedIdSets(relatedResult);
|
||||
|
||||
if (!section || section === "navigate") {
|
||||
let navigateItems = this._generateNavigationCommandsMemoized(
|
||||
@@ -498,7 +500,7 @@ export class QuickBar extends LitElement {
|
||||
let entityItems = this._getEntitiesMemoized(this.hass);
|
||||
|
||||
// Mark related items
|
||||
if (relatedIdSets.entities.size > 0) {
|
||||
if (relatedIdSets?.entities.size) {
|
||||
entityItems = entityItems.map((item) => ({
|
||||
...item,
|
||||
isRelated: relatedIdSets.entities.has(
|
||||
@@ -508,7 +510,7 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
entityItems = this._sortRelatedFirst(
|
||||
entityItems = sortRelatedFirst(
|
||||
this._filterGroup(
|
||||
"entity",
|
||||
entityItems,
|
||||
@@ -537,7 +539,7 @@ export class QuickBar extends LitElement {
|
||||
);
|
||||
|
||||
// Mark related items
|
||||
if (relatedIdSets.devices.size > 0) {
|
||||
if (relatedIdSets?.devices.size) {
|
||||
deviceItems = deviceItems.map((item) => {
|
||||
const deviceId = item.id.split(SEPARATOR)[1];
|
||||
return {
|
||||
@@ -548,7 +550,7 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
deviceItems = this._sortRelatedFirst(
|
||||
deviceItems = sortRelatedFirst(
|
||||
this._filterGroup("device", deviceItems, filter, deviceComboBoxKeys)
|
||||
);
|
||||
} else {
|
||||
@@ -569,7 +571,7 @@ export class QuickBar extends LitElement {
|
||||
let areaItems = this._getAreasMemoized(this.hass);
|
||||
|
||||
// Mark related items
|
||||
if (relatedIdSets.areas.size > 0) {
|
||||
if (relatedIdSets?.areas.size) {
|
||||
areaItems = areaItems.map((item) => {
|
||||
const areaId = item.id.split(SEPARATOR)[1];
|
||||
return {
|
||||
@@ -580,7 +582,7 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
areaItems = this._sortRelatedFirst(
|
||||
areaItems = sortRelatedFirst(
|
||||
this._filterGroup("area", areaItems, filter, areaComboBoxKeys)
|
||||
);
|
||||
} else {
|
||||
@@ -601,41 +603,13 @@ export class QuickBar extends LitElement {
|
||||
}
|
||||
);
|
||||
|
||||
private _getRelatedIdSets = memoizeOne((related?: RelatedResult) => ({
|
||||
entities: new Set(related?.entity || []),
|
||||
devices: new Set(related?.device || []),
|
||||
areas: new Set(related?.area || []),
|
||||
}));
|
||||
|
||||
private _getEntitiesMemoized = memoizeOne((hass: HomeAssistant) =>
|
||||
getEntities(
|
||||
hass,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
`entity${SEPARATOR}`
|
||||
)
|
||||
getEntities(hass, { idPrefix: `entity${SEPARATOR}` })
|
||||
);
|
||||
|
||||
private _getDevicesMemoized = memoizeOne(
|
||||
(hass: HomeAssistant, configEntryLookup: Record<string, ConfigEntry>) =>
|
||||
getDevices(
|
||||
hass,
|
||||
configEntryLookup,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
`device${SEPARATOR}`
|
||||
)
|
||||
getDevices(hass, configEntryLookup, { idPrefix: `device${SEPARATOR}` })
|
||||
);
|
||||
|
||||
private _getAreasMemoized = memoizeOne((hass: HomeAssistant) =>
|
||||
@@ -645,13 +619,9 @@ export class QuickBar extends LitElement {
|
||||
hass.devices,
|
||||
hass.entities,
|
||||
hass.states,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
`area${SEPARATOR}`
|
||||
{
|
||||
idPrefix: `area${SEPARATOR}`,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@@ -705,10 +675,13 @@ export class QuickBar extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _sortBySortingLabel = (entityA, entityB) =>
|
||||
private _sortBySortingLabel = (
|
||||
entityA: PickerComboBoxItem,
|
||||
entityB: PickerComboBoxItem
|
||||
) =>
|
||||
caseInsensitiveStringCompare(
|
||||
(entityA as PickerComboBoxItem).sorting_label!,
|
||||
(entityB as PickerComboBoxItem).sorting_label!,
|
||||
entityA.sorting_label!,
|
||||
entityB.sorting_label!,
|
||||
this.hass.locale.language
|
||||
);
|
||||
|
||||
@@ -719,16 +692,6 @@ export class QuickBar extends LitElement {
|
||||
return this._sortBySortingLabel(a, b);
|
||||
});
|
||||
|
||||
private _sortRelatedFirst = (items: PickerComboBoxItem[]) =>
|
||||
[...items].sort((a, b) => {
|
||||
const aRelated = Boolean(a.isRelated);
|
||||
const bRelated = Boolean(b.isRelated);
|
||||
if (aRelated === bRelated) {
|
||||
return 0;
|
||||
}
|
||||
return aRelated ? -1 : 1;
|
||||
});
|
||||
|
||||
// #endregion data
|
||||
|
||||
// #region interaction
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { ItemType, RelatedResult } from "../../data/search";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { closeDialog } from "../make-dialog-manager";
|
||||
|
||||
@@ -10,17 +9,10 @@ export type QuickBarSection =
|
||||
| "navigate"
|
||||
| "command";
|
||||
|
||||
export interface QuickBarContextItem {
|
||||
itemType: ItemType;
|
||||
itemId: string;
|
||||
}
|
||||
|
||||
export interface QuickBarParams {
|
||||
entityFilter?: string;
|
||||
mode?: QuickBarSection;
|
||||
showHint?: boolean;
|
||||
contextItem?: QuickBarContextItem;
|
||||
related?: RelatedResult;
|
||||
}
|
||||
|
||||
/** Non-admin users cannot scope the bar to command, device, or area (those sections are admin-only). */
|
||||
|
||||
@@ -391,7 +391,6 @@ export class HaTabsSubpageDataTable extends KeyboardShortcutMixin(LitElement) {
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.localizeFunc=${this.localizeFunc}
|
||||
.narrow=${this.narrow}
|
||||
.isWide=${this.isWide}
|
||||
.backPath=${this.backPath}
|
||||
.backCallback=${this.backCallback}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import {
|
||||
@@ -18,6 +19,7 @@ import "../components/ha-icon-button-arrow-prev";
|
||||
import "../components/ha-menu-button";
|
||||
import "../components/ha-svg-icon";
|
||||
import "../components/ha-tab";
|
||||
import { narrowViewportContext } from "../data/context";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../types";
|
||||
|
||||
@@ -59,7 +61,9 @@ export class HassTabsSubpage extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public tabs!: PageNavigation[];
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public narrow = false;
|
||||
@state()
|
||||
@consume({ context: narrowViewportContext, subscribe: true })
|
||||
private _narrow = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true, attribute: "is-wide" })
|
||||
public isWide = false;
|
||||
@@ -116,7 +120,7 @@ export class HassTabsSubpage extends LitElement {
|
||||
<a href=${page.path} @click=${this._tabClicked}>
|
||||
<ha-tab
|
||||
.active=${page.path === activeTab?.path}
|
||||
.narrow=${this.narrow}
|
||||
.narrow=${this._narrow}
|
||||
.name=${page.translationKey
|
||||
? localizeFunc(page.translationKey)
|
||||
: page.name}
|
||||
@@ -151,18 +155,18 @@ export class HassTabsSubpage extends LitElement {
|
||||
this.hass.config.components,
|
||||
this.hass.language,
|
||||
this.hass.userData,
|
||||
this.narrow,
|
||||
this._narrow,
|
||||
this.localizeFunc || this.hass.localize
|
||||
);
|
||||
return html`
|
||||
<div class="toolbar ${classMap({ narrow: this.narrow })}">
|
||||
<div class="toolbar ${classMap({ narrow: this._narrow })}">
|
||||
<slot name="toolbar">
|
||||
<div class="toolbar-content">
|
||||
${this.mainPage || (!this.backPath && history.state?.root)
|
||||
? html`
|
||||
<ha-menu-button
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.narrow=${this._narrow}
|
||||
></ha-menu-button>
|
||||
`
|
||||
: this.backPath
|
||||
@@ -178,12 +182,12 @@ export class HassTabsSubpage extends LitElement {
|
||||
@click=${this._backTapped}
|
||||
></ha-icon-button-arrow-prev>
|
||||
`}
|
||||
${this.narrow || !this.showTabs
|
||||
${this._narrow || !this.showTabs
|
||||
? html`<div class="main-title">
|
||||
<slot name="header">${!this.showTabs ? tabs[0] : ""}</slot>
|
||||
</div>`
|
||||
: ""}
|
||||
${this.showTabs && !this.narrow
|
||||
${this.showTabs && !this._narrow
|
||||
? html`<div id="tabbar">${tabs}</div>`
|
||||
: ""}
|
||||
<div id="toolbar-icon">
|
||||
@@ -191,7 +195,7 @@ export class HassTabsSubpage extends LitElement {
|
||||
</div>
|
||||
</div>
|
||||
</slot>
|
||||
${this.showTabs && this.narrow
|
||||
${this.showTabs && this._narrow
|
||||
? html`<div id="tabbar" class="bottom-bar">${tabs}</div>`
|
||||
: ""}
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ContextProvider } from "@lit/context";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -7,6 +8,7 @@ import { listenMediaQuery } from "../common/dom/media_query";
|
||||
import { toggleAttribute } from "../common/dom/toggle_attribute";
|
||||
import { computeRTLDirection } from "../common/util/compute_rtl";
|
||||
import "../components/ha-drawer";
|
||||
import { narrowViewportContext } from "../data/context";
|
||||
import { showNotificationDrawer } from "../dialogs/notifications/show-notification-drawer";
|
||||
import type { HomeAssistant, Route } from "../types";
|
||||
import "./partial-panel-resolver";
|
||||
@@ -36,6 +38,11 @@ export class HomeAssistantMain extends LitElement {
|
||||
|
||||
@state() private _drawerOpen = false;
|
||||
|
||||
private _narrowViewportProvider = new ContextProvider(this, {
|
||||
context: narrowViewportContext,
|
||||
initialValue: this.narrow,
|
||||
});
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
listenMediaQuery("(max-width: 870px)", (matches) => {
|
||||
@@ -66,7 +73,6 @@ export class HomeAssistantMain extends LitElement {
|
||||
></ha-sidebar>
|
||||
${isPanelReady
|
||||
? html`<partial-panel-resolver
|
||||
.narrow=${this.narrow}
|
||||
.hass=${this.hass}
|
||||
.route=${this.route}
|
||||
slot="appContent"
|
||||
@@ -121,6 +127,10 @@ export class HomeAssistantMain extends LitElement {
|
||||
}
|
||||
|
||||
public willUpdate(changedProps: PropertyValues<this>) {
|
||||
if (changedProps.has("narrow")) {
|
||||
this._narrowViewportProvider.setValue(this.narrow);
|
||||
}
|
||||
|
||||
if (changedProps.has("route") && this._sidebarNarrow) {
|
||||
this._drawerOpen = false;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
STATE_NOT_RUNNING,
|
||||
STATE_RUNNING,
|
||||
STATE_STARTING,
|
||||
} from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { deepActiveElement } from "../common/dom/deep-active-element";
|
||||
import { deepEqual } from "../common/util/deep-equal";
|
||||
import { narrowViewportContext } from "../data/context";
|
||||
import { getDefaultPanel } from "../data/panel";
|
||||
import type { CustomPanelInfo } from "../data/panel_custom";
|
||||
import type { HomeAssistant, Panels } from "../types";
|
||||
@@ -43,7 +45,9 @@ const COMPONENTS = {
|
||||
class PartialPanelResolver extends HassRouterPage {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
@state()
|
||||
@consume({ context: narrowViewportContext, subscribe: true })
|
||||
private _narrow = false;
|
||||
|
||||
private _waitForStart = false;
|
||||
|
||||
@@ -92,7 +96,7 @@ class PartialPanelResolver extends HassRouterPage {
|
||||
const el = super.createLoadingScreen();
|
||||
el.rootnav = true;
|
||||
el.hass = this.hass;
|
||||
el.narrow = this.narrow;
|
||||
el.narrow = this._narrow;
|
||||
return el;
|
||||
}
|
||||
|
||||
@@ -100,7 +104,7 @@ class PartialPanelResolver extends HassRouterPage {
|
||||
const hass = this.hass;
|
||||
|
||||
el.hass = hass;
|
||||
el.narrow = this.narrow;
|
||||
el.narrow = this._narrow;
|
||||
el.route = this.routeTail;
|
||||
el.panel = hass.panels[this._currentPage];
|
||||
}
|
||||
|
||||
@@ -198,10 +198,12 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
|
||||
/>
|
||||
`
|
||||
: nothing}
|
||||
${getAppDisplayName(
|
||||
this._currentAddon.name,
|
||||
this._currentAddon.stage
|
||||
)}
|
||||
${!this.narrow
|
||||
? getAppDisplayName(
|
||||
this._currentAddon.name,
|
||||
this._currentAddon.stage
|
||||
)
|
||||
: nothing}
|
||||
<div class="description">
|
||||
${this._currentAddon.version
|
||||
? html`
|
||||
|
||||
@@ -135,7 +135,6 @@ class HaConfigAppDashboard extends LitElement {
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${route}
|
||||
.tabs=${addonTabs}
|
||||
back-path=${this._fromStore ? "/config/apps/available" : "/config/apps"}
|
||||
|
||||
@@ -12,22 +12,32 @@ import {
|
||||
import { computeAreaName } from "../../../common/entity/compute_area_name";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-adaptive-dialog";
|
||||
import "../../../components/ha-list";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import {
|
||||
areasContext,
|
||||
internationalizationContext,
|
||||
} from "../../../data/context";
|
||||
import type { SceneEntities } from "../../../data/scene";
|
||||
import { showSceneEditor } from "../../../data/scene";
|
||||
import "../../../dialogs/add-to/ha-add-to-action-list";
|
||||
import type {
|
||||
AddToActionListActionSelectedEvent,
|
||||
AddToActionListItem,
|
||||
AddToActionListSection,
|
||||
} from "../../../dialogs/add-to/ha-add-to-action-list";
|
||||
import {
|
||||
addToActionHandler,
|
||||
type AddToActionKey,
|
||||
} from "../../../dialogs/more-info/add-to";
|
||||
createAddToSceneEntities,
|
||||
type AddToAutomationScriptActionKey,
|
||||
} from "../../../dialogs/add-to/add-to";
|
||||
import { haStyle, haStyleDialog } from "../../../resources/styles";
|
||||
import type { AreaAddToDialogParams } from "./show-dialog-area-add-to";
|
||||
|
||||
type AreaAddToAction =
|
||||
| (AddToActionListItem & {
|
||||
type: "automation";
|
||||
key: AddToAutomationScriptActionKey;
|
||||
})
|
||||
| (AddToActionListItem & { type: "scene" });
|
||||
|
||||
@customElement("dialog-area-add-to")
|
||||
class DialogAreaAddTo extends LitElement {
|
||||
@state()
|
||||
@@ -65,7 +75,12 @@ class DialogAreaAddTo extends LitElement {
|
||||
<ha-adaptive-dialog
|
||||
.open=${this._open}
|
||||
header-title=${this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.title"
|
||||
"ui.dialogs.more_info_control.add_to.title",
|
||||
{
|
||||
target:
|
||||
computeAreaName(this._areas[this._params.areaId]) ||
|
||||
this._params.areaId,
|
||||
}
|
||||
)}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
@@ -79,108 +94,96 @@ class DialogAreaAddTo extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const area = this._areas[this._params.areaId];
|
||||
const areaName = computeAreaName(area) || this._params.areaId;
|
||||
|
||||
return html`
|
||||
<h3 class="section-header">
|
||||
${this._i18n.localize(
|
||||
const sections: AddToActionListSection<AreaAddToAction>[] = [
|
||||
{
|
||||
title: this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.automations_heading"
|
||||
)}
|
||||
</h3>
|
||||
<ha-list>
|
||||
${this._renderActionItem(
|
||||
"automation_trigger",
|
||||
mdiRobotOutline,
|
||||
"ui.dialogs.more_info_control.add_to.actions.automation_trigger",
|
||||
areaName
|
||||
)}
|
||||
${this._renderActionItem(
|
||||
"automation_condition",
|
||||
mdiPlaylistCheck,
|
||||
"ui.dialogs.more_info_control.add_to.actions.automation_condition",
|
||||
areaName
|
||||
)}
|
||||
${this._renderActionItem(
|
||||
"automation_action",
|
||||
mdiPlayCircleOutline,
|
||||
"ui.dialogs.more_info_control.add_to.actions.automation_action",
|
||||
areaName
|
||||
)}
|
||||
</ha-list>
|
||||
<h3 class="section-header">
|
||||
${this._i18n.localize("ui.panel.config.devices.script.scripts_heading")}
|
||||
</h3>
|
||||
<ha-list>
|
||||
${this._renderActionItem(
|
||||
"script_action",
|
||||
mdiScriptTextOutline,
|
||||
"ui.dialogs.more_info_control.add_to.actions.script_action",
|
||||
areaName
|
||||
)}
|
||||
</ha-list>
|
||||
${this._renderSceneSection(areaName)}
|
||||
`;
|
||||
}
|
||||
),
|
||||
actions: [
|
||||
{
|
||||
type: "automation",
|
||||
key: "automation_trigger",
|
||||
iconPath: mdiRobotOutline,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.automation_trigger"
|
||||
),
|
||||
},
|
||||
{
|
||||
type: "automation",
|
||||
key: "automation_condition",
|
||||
iconPath: mdiPlaylistCheck,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.automation_condition"
|
||||
),
|
||||
},
|
||||
{
|
||||
type: "automation",
|
||||
key: "automation_action",
|
||||
iconPath: mdiPlayCircleOutline,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.automation_action"
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: this._i18n.localize(
|
||||
"ui.panel.config.devices.script.scripts_heading"
|
||||
),
|
||||
actions: [
|
||||
{
|
||||
type: "automation",
|
||||
key: "script_action",
|
||||
iconPath: mdiScriptTextOutline,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.script_action"
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
private _renderSceneSection(areaName: string) {
|
||||
if (!this._params?.entityIds.length) {
|
||||
return nothing;
|
||||
if (this._params.canCreateScene && this._params.entityIds.length) {
|
||||
sections.push({
|
||||
title: this._i18n.localize(
|
||||
"ui.panel.config.devices.scene.scenes_heading"
|
||||
),
|
||||
actions: [
|
||||
{
|
||||
type: "scene",
|
||||
iconPath: mdiPalette,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.scene"
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return html`
|
||||
<h3 class="section-header">
|
||||
${this._i18n.localize("ui.panel.config.devices.scene.scenes_heading")}
|
||||
</h3>
|
||||
<ha-list>
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
@click=${this._handleCreateScene}
|
||||
data-dialog="close"
|
||||
>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiPalette}></ha-svg-icon>
|
||||
${this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.actions.scene",
|
||||
{ target: areaName }
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-list>
|
||||
<ha-add-to-action-list
|
||||
.sections=${sections}
|
||||
@add-to-list-action-selected=${this._handleActionSelected}
|
||||
></ha-add-to-action-list>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderActionItem(
|
||||
key: AddToActionKey,
|
||||
path: string,
|
||||
translationKey:
|
||||
| "ui.dialogs.more_info_control.add_to.actions.automation_trigger"
|
||||
| "ui.dialogs.more_info_control.add_to.actions.automation_condition"
|
||||
| "ui.dialogs.more_info_control.add_to.actions.automation_action"
|
||||
| "ui.dialogs.more_info_control.add_to.actions.script_action",
|
||||
areaName: string
|
||||
private _handleActionSelected(
|
||||
ev: AddToActionListActionSelectedEvent<AreaAddToAction>
|
||||
) {
|
||||
return html`
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
data-type=${key}
|
||||
@click=${this._handleAction}
|
||||
data-dialog="close"
|
||||
>
|
||||
<ha-svg-icon slot="graphic" .path=${path}></ha-svg-icon>
|
||||
${this._i18n.localize(translationKey, { target: areaName })}
|
||||
</ha-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleAction(ev: Event) {
|
||||
if (!this._params) {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = (ev.currentTarget as HTMLElement).dataset
|
||||
.type as AddToActionKey;
|
||||
const { action } = ev.detail;
|
||||
|
||||
if (action.type === "scene") {
|
||||
this._handleCreateScene();
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeDialog();
|
||||
addToActionHandler(key, { area_id: this._params.areaId });
|
||||
addToActionHandler(action.key, { area_id: this._params.areaId });
|
||||
}
|
||||
|
||||
private _handleCreateScene() {
|
||||
@@ -188,13 +191,11 @@ class DialogAreaAddTo extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const entities: SceneEntities = {};
|
||||
for (const entityId of this._params.entityIds) {
|
||||
entities[entityId] = "";
|
||||
}
|
||||
|
||||
this.closeDialog();
|
||||
showSceneEditor({ entities }, this._params.areaId);
|
||||
showSceneEditor(
|
||||
{ entities: createAddToSceneEntities(this._params.entityIds) },
|
||||
this._params.areaId
|
||||
);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -205,14 +206,6 @@ class DialogAreaAddTo extends LitElement {
|
||||
ha-adaptive-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
padding: var(--ha-space-2) var(--ha-space-4) 0;
|
||||
margin: 0;
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -60,6 +60,7 @@ import type { SceneEntity } from "../../../data/scene";
|
||||
import type { ScriptEntity } from "../../../data/script";
|
||||
import type { RelatedResult } from "../../../data/search";
|
||||
import { findRelated } from "../../../data/search";
|
||||
import { filterAddToSceneEntityIds } from "../../../dialogs/add-to/add-to";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
|
||||
import "../../../layouts/hass-error-screen";
|
||||
@@ -439,7 +440,7 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
|
||||
.path=${mdiPlus}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_to.title"
|
||||
"ui.dialogs.more_info_control.add_to.item"
|
||||
)}
|
||||
</ha-button>`
|
||||
: nothing}
|
||||
@@ -781,9 +782,17 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
|
||||
if (!area) {
|
||||
return;
|
||||
}
|
||||
const sceneEntityIds = filterAddToSceneEntityIds(
|
||||
this._areaEntityIds,
|
||||
this._entityReg,
|
||||
this.hass.states
|
||||
);
|
||||
showAreaAddToDialog(this, {
|
||||
areaId: area.area_id,
|
||||
entityIds: this._areaEntityIds,
|
||||
entityIds: sceneEntityIds,
|
||||
canCreateScene:
|
||||
isComponentLoaded(this.hass.config, "scene") &&
|
||||
sceneEntityIds.length > 0,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -82,8 +82,6 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _hierarchy?: AreasFloorHierarchy;
|
||||
@@ -169,7 +167,6 @@ export class HaConfigAreasDashboard extends LitElement {
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.isWide=${this.isWide}
|
||||
.backPath=${this._searchParms.has("historyBack")
|
||||
? undefined
|
||||
|
||||
@@ -3,6 +3,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
||||
export interface AreaAddToDialogParams {
|
||||
areaId: string;
|
||||
entityIds: string[];
|
||||
canCreateScene: boolean;
|
||||
}
|
||||
|
||||
export const loadAreaAddToDialog = () => import("./dialog-area-add-to");
|
||||
|
||||
@@ -20,6 +20,10 @@ import type {
|
||||
LocalizeKeys,
|
||||
} from "../../../../common/translations/localize";
|
||||
import { computeRTL } from "../../../../common/util/compute_rtl";
|
||||
import {
|
||||
sortRelatedFirst,
|
||||
type RelatedIdSets,
|
||||
} from "../../../../common/search/related-context";
|
||||
import "../../../../components/chips/ha-chip-set";
|
||||
import "../../../../components/chips/ha-filter-chip";
|
||||
import "../../../../components/entity/state-badge";
|
||||
@@ -40,7 +44,7 @@ import {
|
||||
} from "../../../../data/area_floor_picker";
|
||||
import { CONDITION_BUILDING_BLOCKS_GROUP } from "../../../../data/condition";
|
||||
import type { ConfigEntry } from "../../../../data/config_entries";
|
||||
import { labelsContext } from "../../../../data/context";
|
||||
import { labelsContext, relatedContext } from "../../../../data/context";
|
||||
import {
|
||||
deviceComboBoxKeys,
|
||||
getDevices,
|
||||
@@ -129,6 +133,10 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
| "condition"
|
||||
| "action";
|
||||
|
||||
@state()
|
||||
@consume({ context: relatedContext, subscribe: true })
|
||||
private _relatedIdSets?: RelatedIdSets;
|
||||
|
||||
@state() private _searchSectionTitle?: string;
|
||||
|
||||
@state() private _selectedSearchSection?: SearchSection;
|
||||
@@ -141,11 +149,19 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
|
||||
@query("lit-virtualizer") private _virtualizerElement?: LitVirtualizer;
|
||||
|
||||
private _getDevicesMemoized = memoizeOne(getDevices);
|
||||
private _getDevicesMemoized = memoizeOne(
|
||||
(
|
||||
hass: HomeAssistant,
|
||||
configEntryLookup: Record<string, ConfigEntry>,
|
||||
idPrefix: string
|
||||
) => getDevices(hass, configEntryLookup, { idPrefix })
|
||||
);
|
||||
|
||||
private _getLabelsMemoized = memoizeOne(getLabels);
|
||||
|
||||
private _getEntitiesMemoized = memoizeOne(getEntities);
|
||||
private _getEntitiesMemoized = memoizeOne(
|
||||
(hass: HomeAssistant, idPrefix: string) => getEntities(hass, { idPrefix })
|
||||
);
|
||||
|
||||
private _getAreasAndFloorsMemoized = memoizeOne(getAreasAndFloors);
|
||||
|
||||
@@ -194,7 +210,8 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
this.configEntryLookup,
|
||||
this.items,
|
||||
this.newTriggersAndConditions,
|
||||
this._selectedSearchSection
|
||||
this._selectedSearchSection,
|
||||
this._relatedIdSets
|
||||
);
|
||||
|
||||
let emptySearchTranslation: string | undefined;
|
||||
@@ -487,7 +504,8 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
configEntryLookup: Record<string, ConfigEntry>,
|
||||
automationItems: AddAutomationElementListItem[],
|
||||
newTriggersAndConditions: boolean,
|
||||
selectedSection?: SearchSection
|
||||
selectedSection?: SearchSection,
|
||||
relatedIdSets?: RelatedIdSets
|
||||
) => {
|
||||
const resultItems: (
|
||||
| string
|
||||
@@ -557,24 +575,29 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
if (!selectedSection || selectedSection === "entity") {
|
||||
let entityItems = this._getEntitiesMemoized(
|
||||
this.hass,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
`entity${TARGET_SEPARATOR}`
|
||||
);
|
||||
|
||||
if (relatedIdSets?.entities.size) {
|
||||
entityItems = entityItems.map((item) => ({
|
||||
...item,
|
||||
isRelated: relatedIdSets.entities.has(
|
||||
(item as EntityComboBoxItem).stateObj?.entity_id || ""
|
||||
),
|
||||
})) as EntityComboBoxItem[];
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
entityItems = this._filterGroup(
|
||||
"entity",
|
||||
entityItems,
|
||||
searchTerm,
|
||||
entityComboBoxKeys
|
||||
entityItems = sortRelatedFirst(
|
||||
this._filterGroup(
|
||||
"entity",
|
||||
entityItems,
|
||||
searchTerm,
|
||||
entityComboBoxKeys
|
||||
)
|
||||
) as EntityComboBoxItem[];
|
||||
} else if (relatedIdSets?.entities.size) {
|
||||
entityItems = sortRelatedFirst(entityItems) as EntityComboBoxItem[];
|
||||
}
|
||||
|
||||
if (!selectedSection && entityItems.length) {
|
||||
@@ -591,23 +614,29 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
let deviceItems = this._getDevicesMemoized(
|
||||
this.hass,
|
||||
configEntryLookup,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
`device${TARGET_SEPARATOR}`
|
||||
);
|
||||
|
||||
if (relatedIdSets?.devices.size) {
|
||||
deviceItems = deviceItems.map((item) => ({
|
||||
...item,
|
||||
isRelated: relatedIdSets.devices.has(
|
||||
item.id.split(TARGET_SEPARATOR)[1] || ""
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
deviceItems = this._filterGroup(
|
||||
"device",
|
||||
deviceItems,
|
||||
searchTerm,
|
||||
deviceComboBoxKeys
|
||||
deviceItems = sortRelatedFirst(
|
||||
this._filterGroup(
|
||||
"device",
|
||||
deviceItems,
|
||||
searchTerm,
|
||||
deviceComboBoxKeys
|
||||
)
|
||||
);
|
||||
} else if (relatedIdSets?.devices.size) {
|
||||
deviceItems = sortRelatedFirst(deviceItems);
|
||||
}
|
||||
|
||||
if (!selectedSection && deviceItems.length) {
|
||||
@@ -639,13 +668,31 @@ export class HaAutomationAddSearch extends LitElement {
|
||||
undefined
|
||||
);
|
||||
|
||||
if (relatedIdSets?.areas.size) {
|
||||
areasAndFloors = areasAndFloors.map((item) => ({
|
||||
...item,
|
||||
isRelated:
|
||||
item.type === "area"
|
||||
? relatedIdSets.areas.has(
|
||||
item.id.split(TARGET_SEPARATOR)[1] || ""
|
||||
)
|
||||
: false,
|
||||
})) as FloorComboBoxItem[];
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
areasAndFloors = this._filterGroup(
|
||||
"area",
|
||||
areasAndFloors,
|
||||
searchTerm,
|
||||
areaFloorComboBoxKeys,
|
||||
false
|
||||
areasAndFloors = sortRelatedFirst(
|
||||
this._filterGroup(
|
||||
"area",
|
||||
areasAndFloors,
|
||||
searchTerm,
|
||||
areaFloorComboBoxKeys,
|
||||
false
|
||||
)
|
||||
) as FloorComboBoxItem[];
|
||||
} else if (relatedIdSets?.areas.size) {
|
||||
areasAndFloors = sortRelatedFirst(
|
||||
areasAndFloors
|
||||
) as FloorComboBoxItem[];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { CSSResult, LitElement, TemplateResult } from "lit";
|
||||
import type {
|
||||
CSSResult,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { css, html } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { transform } from "../../../common/decorators/transform";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { goBack, navigate } from "../../../common/navigate";
|
||||
import { afterNextRender } from "../../../common/util/render-status";
|
||||
import "../../../components/animation/ha-fade-in";
|
||||
@@ -136,6 +142,21 @@ export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
|
||||
value: PromiseLike<EntityRegistryEntry> | EntityRegistryEntry
|
||||
) => void;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("registryEntry")) {
|
||||
const areaId = this.registryEntry?.area_id;
|
||||
if (areaId) {
|
||||
fireEvent(this, "hass-related-context", {
|
||||
itemType: "area",
|
||||
itemId: areaId,
|
||||
});
|
||||
} else {
|
||||
fireEvent(this, "hass-related-context", undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected renderLoading(): TemplateResult {
|
||||
return html`
|
||||
<ha-fade-in .delay=${500}>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
|
||||
export const confirmDeleteCategory = (
|
||||
element: HTMLElement,
|
||||
hass: HomeAssistant
|
||||
) =>
|
||||
showConfirmationDialog(element, {
|
||||
title: hass.localize("ui.panel.config.category.editor.confirm_delete"),
|
||||
text: hass.localize("ui.panel.config.category.editor.confirm_delete_text"),
|
||||
confirmText: hass.localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
});
|
||||
@@ -0,0 +1,369 @@
|
||||
import {
|
||||
mdiDelete,
|
||||
mdiHelpCircleOutline,
|
||||
mdiPalette,
|
||||
mdiPencil,
|
||||
mdiPlus,
|
||||
mdiRobot,
|
||||
mdiScriptText,
|
||||
mdiTag,
|
||||
mdiTools,
|
||||
} from "@mdi/js";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import type {
|
||||
DataTableColumnContainer,
|
||||
DataTableRowData,
|
||||
RowClickedEvent,
|
||||
} from "../../../components/data-table/ha-data-table";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-icon-overflow-menu";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type {
|
||||
CategoryRegistryEntry,
|
||||
CategoryRegistryEntryMutableParams,
|
||||
} from "../../../data/category_registry";
|
||||
import {
|
||||
createCategoryRegistryEntry,
|
||||
deleteCategoryRegistryEntry,
|
||||
subscribeCategoryRegistry,
|
||||
updateCategoryRegistryEntry,
|
||||
} from "../../../data/category_registry";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
import { areaConfigTabs } from "../common/area-config-tabs";
|
||||
import {
|
||||
getCreatedAtTableColumn,
|
||||
getModifiedAtTableColumn,
|
||||
} from "../common/data-table-columns";
|
||||
import { confirmDeleteCategory } from "./confirm-delete-category";
|
||||
import { showCategoryRegistryDetailDialog } from "./show-dialog-category-registry-detail";
|
||||
|
||||
const CATEGORY_SCOPE_CONFIGS = [
|
||||
{
|
||||
scope: "automation",
|
||||
icon: mdiRobot,
|
||||
translationKey: "ui.panel.config.automation.caption",
|
||||
},
|
||||
{
|
||||
scope: "scene",
|
||||
icon: mdiPalette,
|
||||
translationKey: "ui.panel.config.scene.caption",
|
||||
},
|
||||
{
|
||||
scope: "script",
|
||||
icon: mdiScriptText,
|
||||
translationKey: "ui.panel.config.script.caption",
|
||||
},
|
||||
{
|
||||
scope: "helpers",
|
||||
icon: mdiTools,
|
||||
translationKey: "ui.panel.config.helpers.caption",
|
||||
},
|
||||
] as const;
|
||||
|
||||
type CategoryScope = (typeof CATEGORY_SCOPE_CONFIGS)[number]["scope"];
|
||||
|
||||
type CategoriesByScope = Record<CategoryScope, CategoryRegistryEntry[]>;
|
||||
|
||||
interface CategoryRowData extends CategoryRegistryEntry, DataTableRowData {
|
||||
id: string;
|
||||
scope: CategoryScope;
|
||||
scopeName: string;
|
||||
}
|
||||
|
||||
const EMPTY_CATEGORIES_BY_SCOPE: CategoriesByScope = {
|
||||
automation: [],
|
||||
scene: [],
|
||||
script: [],
|
||||
helpers: [],
|
||||
};
|
||||
|
||||
@customElement("ha-config-categories")
|
||||
export class HaConfigCategories extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _categories: CategoriesByScope = EMPTY_CATEGORIES_BY_SCOPE;
|
||||
|
||||
@state()
|
||||
@storage({
|
||||
storage: "sessionStorage",
|
||||
key: "categories-table-search",
|
||||
state: true,
|
||||
subscribe: false,
|
||||
})
|
||||
private _filter = "";
|
||||
|
||||
private _columns = memoizeOne((localize: LocalizeFunc) => {
|
||||
const columns: DataTableColumnContainer<CategoryRowData> = {
|
||||
icon: {
|
||||
title: "",
|
||||
moveable: false,
|
||||
showNarrow: true,
|
||||
label: localize("ui.panel.config.category.headers.icon"),
|
||||
type: "icon",
|
||||
template: (category) =>
|
||||
category.icon
|
||||
? html`<ha-icon .icon=${category.icon}></ha-icon>`
|
||||
: html`<ha-svg-icon .path=${mdiTag}></ha-svg-icon>`,
|
||||
},
|
||||
name: {
|
||||
title: localize("ui.panel.config.category.headers.name"),
|
||||
main: true,
|
||||
flex: 2,
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
},
|
||||
scopeName: {
|
||||
title: localize("ui.panel.config.category.headers.scope"),
|
||||
defaultHidden: true,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
},
|
||||
created_at: getCreatedAtTableColumn(localize, this.hass),
|
||||
modified_at: getModifiedAtTableColumn(localize, this.hass),
|
||||
actions: {
|
||||
lastFixed: true,
|
||||
title: "",
|
||||
label: localize("ui.panel.config.generic.headers.actions"),
|
||||
showNarrow: true,
|
||||
type: "overflow-menu",
|
||||
template: (category) => html`
|
||||
<ha-icon-overflow-menu
|
||||
narrow
|
||||
.items=${[
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.category.editor.edit"
|
||||
),
|
||||
path: mdiPencil,
|
||||
action: () => this._openDialog(category.scope, category),
|
||||
},
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.category.editor.delete"
|
||||
),
|
||||
path: mdiDelete,
|
||||
action: () => this._deleteCategory(category),
|
||||
warning: true,
|
||||
},
|
||||
]}
|
||||
></ha-icon-overflow-menu>
|
||||
`,
|
||||
},
|
||||
};
|
||||
return columns;
|
||||
});
|
||||
|
||||
private _data = memoizeOne(
|
||||
(
|
||||
categories: CategoriesByScope,
|
||||
localize: LocalizeFunc
|
||||
): CategoryRowData[] =>
|
||||
CATEGORY_SCOPE_CONFIGS.flatMap((scopeConfig) =>
|
||||
categories[scopeConfig.scope].map((category) => ({
|
||||
...category,
|
||||
id: `${scopeConfig.scope}:${category.category_id}`,
|
||||
scope: scopeConfig.scope,
|
||||
scopeName: localize(scopeConfig.translationKey),
|
||||
}))
|
||||
)
|
||||
);
|
||||
|
||||
private _groupOrder = memoizeOne((localize: LocalizeFunc) =>
|
||||
CATEGORY_SCOPE_CONFIGS.map((scopeConfig) =>
|
||||
localize(scopeConfig.translationKey)
|
||||
)
|
||||
);
|
||||
|
||||
protected hassSubscribe() {
|
||||
return CATEGORY_SCOPE_CONFIGS.map((scopeConfig) =>
|
||||
subscribeCategoryRegistry(
|
||||
this.hass.connection,
|
||||
scopeConfig.scope,
|
||||
(categories) => this._setCategories(scopeConfig.scope, categories)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${areaConfigTabs}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.data=${this._data(this._categories, this.hass.localize)}
|
||||
.noDataText=${this.hass.localize(
|
||||
"ui.panel.config.category.no_categories"
|
||||
)}
|
||||
has-fab
|
||||
.initialGroupColumn=${"scopeName"}
|
||||
.groupOrder=${this._groupOrder(this.hass.localize)}
|
||||
.filter=${this._filter}
|
||||
@search-changed=${this._handleSearchChange}
|
||||
@row-click=${this._editCategory}
|
||||
clickable
|
||||
id="id"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
@click=${this._showHelp}
|
||||
.label=${this.hass.localize("ui.common.help")}
|
||||
.path=${mdiHelpCircleOutline}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown slot="fab" @wa-select=${this._handleCreateScope}>
|
||||
<ha-button slot="trigger" id="fab" size="large">
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.category.editor.create")}
|
||||
</ha-button>
|
||||
${CATEGORY_SCOPE_CONFIGS.map(
|
||||
(scopeConfig) => html`
|
||||
<ha-dropdown-item .value=${scopeConfig.scope}>
|
||||
<ha-svg-icon
|
||||
.path=${scopeConfig.icon}
|
||||
slot="icon"
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(scopeConfig.translationKey)}
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
)}
|
||||
</ha-dropdown>
|
||||
</hass-tabs-subpage-data-table>
|
||||
`;
|
||||
}
|
||||
|
||||
private _showHelp() {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.category.caption"),
|
||||
text: html`
|
||||
${this.hass.localize("ui.panel.config.category.introduction")}
|
||||
<p>${this.hass.localize("ui.panel.config.category.introduction2")}</p>
|
||||
`,
|
||||
});
|
||||
}
|
||||
|
||||
private _setCategories(
|
||||
scope: CategoryScope,
|
||||
categories: CategoryRegistryEntry[]
|
||||
) {
|
||||
this._categories = {
|
||||
...this._categories,
|
||||
[scope]: categories,
|
||||
};
|
||||
}
|
||||
|
||||
private _handleCreateScope = (ev: HaDropdownSelectEvent<CategoryScope>) => {
|
||||
this._openDialog(ev.detail.item.value);
|
||||
};
|
||||
|
||||
private _editCategory(ev: CustomEvent<RowClickedEvent>) {
|
||||
const category = this._data(this._categories, this.hass.localize).find(
|
||||
(row) => row.id === ev.detail.id
|
||||
);
|
||||
if (!category) {
|
||||
return;
|
||||
}
|
||||
this._openDialog(category.scope, category);
|
||||
}
|
||||
|
||||
private _openDialog(scope: CategoryScope, entry?: CategoryRegistryEntry) {
|
||||
showCategoryRegistryDetailDialog(this, {
|
||||
scope,
|
||||
entry,
|
||||
createEntry: entry
|
||||
? undefined
|
||||
: (values) => this._createCategory(scope, values),
|
||||
updateEntry: entry
|
||||
? (values) => this._updateCategory(scope, entry, values)
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
private async _createCategory(
|
||||
scope: CategoryScope,
|
||||
values: CategoryRegistryEntryMutableParams
|
||||
): Promise<CategoryRegistryEntry> {
|
||||
const category = await createCategoryRegistryEntry(
|
||||
this.hass,
|
||||
scope,
|
||||
values
|
||||
);
|
||||
this._setCategories(scope, [...this._categories[scope], category]);
|
||||
return category;
|
||||
}
|
||||
|
||||
private async _updateCategory(
|
||||
scope: CategoryScope,
|
||||
entry: CategoryRegistryEntry,
|
||||
values: Partial<CategoryRegistryEntryMutableParams>
|
||||
): Promise<CategoryRegistryEntry> {
|
||||
const category = await updateCategoryRegistryEntry(
|
||||
this.hass,
|
||||
scope,
|
||||
entry.category_id,
|
||||
values
|
||||
);
|
||||
this._setCategories(
|
||||
scope,
|
||||
this._categories[scope].map((current) =>
|
||||
current.category_id === entry.category_id ? category : current
|
||||
)
|
||||
);
|
||||
return category;
|
||||
}
|
||||
|
||||
private async _deleteCategory(entry: CategoryRowData) {
|
||||
if (!(await confirmDeleteCategory(this, this.hass))) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await deleteCategoryRegistryEntry(
|
||||
this.hass,
|
||||
entry.scope,
|
||||
entry.category_id
|
||||
);
|
||||
this._setCategories(
|
||||
entry.scope,
|
||||
this._categories[entry.scope].filter(
|
||||
(category) => category.category_id !== entry.category_id
|
||||
)
|
||||
);
|
||||
} catch (err: unknown) {
|
||||
showAlertDialog(this, {
|
||||
text:
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.category.editor.unknown_error"
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _handleSearchChange(ev: CustomEvent) {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-config-categories": HaConfigCategories;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { mdiLabel, mdiMapMarkerRadius, mdiSofa, mdiTag } from "@mdi/js";
|
||||
import type { PageNavigation } from "../../../layouts/hass-tabs-subpage";
|
||||
|
||||
export const areaConfigTabs: PageNavigation[] = [
|
||||
{
|
||||
component: "areas",
|
||||
path: "/config/areas",
|
||||
translationKey: "ui.panel.config.areas.caption",
|
||||
iconPath: mdiSofa,
|
||||
iconColor: "#2D338F",
|
||||
core: true,
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
component: "categories",
|
||||
path: "/config/categories",
|
||||
translationKey: "ui.panel.config.category.caption",
|
||||
iconPath: mdiTag,
|
||||
iconColor: "#2D338F",
|
||||
core: true,
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
component: "labels",
|
||||
path: "/config/labels",
|
||||
translationKey: "ui.panel.config.labels.caption",
|
||||
iconPath: mdiLabel,
|
||||
iconColor: "#2D338F",
|
||||
core: true,
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
component: "zone",
|
||||
path: "/config/zone",
|
||||
translationKey: "ui.panel.config.zone.caption",
|
||||
iconPath: mdiMapMarkerRadius,
|
||||
iconColor: "#E48629",
|
||||
adminOnly: true,
|
||||
},
|
||||
];
|
||||
@@ -11,10 +11,15 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import type { EntitySources } from "../../../data/entity/entity_sources";
|
||||
import { fetchEntitySourcesWithCache } from "../../../data/entity/entity_sources";
|
||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||
import type {
|
||||
HassioSupervisorInfo,
|
||||
@@ -25,9 +30,17 @@ import {
|
||||
reloadSupervisor,
|
||||
setSupervisorOption,
|
||||
} from "../../../data/hassio/supervisor";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import type { UpdateEntity } from "../../../data/update";
|
||||
import {
|
||||
checkForEntityUpdates,
|
||||
filterUpdateEntities,
|
||||
filterUpdateEntitiesParameterized,
|
||||
installUpdates,
|
||||
isSystemUpdate,
|
||||
latestVersionIsSkipped,
|
||||
updateIsInstalling,
|
||||
UpdateEntityFeature,
|
||||
} from "../../../data/update";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../layouts/hass-subpage";
|
||||
@@ -35,6 +48,17 @@ import type { HomeAssistant } from "../../../types";
|
||||
import "../dashboard/ha-config-updates";
|
||||
import { showJoinBetaDialog } from "./updates/show-dialog-join-beta";
|
||||
|
||||
interface UpdateGroup {
|
||||
key: string;
|
||||
title: string;
|
||||
entities: UpdateEntity[];
|
||||
showUpdateAll: boolean;
|
||||
}
|
||||
|
||||
const SYSTEM_KEY = "__system__";
|
||||
const APPS_KEY = "__apps__";
|
||||
const INTEGRATIONS_KEY = "__integrations__";
|
||||
|
||||
@customElement("ha-config-section-updates")
|
||||
class HaConfigSectionUpdates extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -47,23 +71,86 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
|
||||
@state() private _supervisorInfo?: HassioSupervisorInfo;
|
||||
|
||||
@state() private _entitySources?: EntitySources;
|
||||
|
||||
@state() private _loadedIntegrationTitles = new Set<string>();
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
if (isComponentLoaded(this.hass.config, "hassio")) {
|
||||
this._refreshSupervisorInfo();
|
||||
}
|
||||
|
||||
this._loadEntitySources();
|
||||
}
|
||||
|
||||
private async _loadEntitySources() {
|
||||
try {
|
||||
this._entitySources = await fetchEntitySourcesWithCache(this.hass);
|
||||
} catch (_err) {
|
||||
// Non-fatal: grouping falls back to entity registry platform lookup.
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues<this>) {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("hass")) {
|
||||
this._loadIntegrationTitles();
|
||||
}
|
||||
}
|
||||
|
||||
private _collectUpdateDomains = memoizeOne(
|
||||
(states: HassEntities, entities: HomeAssistant["entities"]) => {
|
||||
const domains = new Set<string>();
|
||||
for (const entity of Object.values(states)) {
|
||||
if (!entity.entity_id.startsWith("update.")) continue;
|
||||
const platform = entities[entity.entity_id]?.platform;
|
||||
if (platform) {
|
||||
domains.add(platform);
|
||||
}
|
||||
}
|
||||
return domains;
|
||||
}
|
||||
);
|
||||
|
||||
private async _loadIntegrationTitles() {
|
||||
const domains = this._collectUpdateDomains(
|
||||
this.hass.states,
|
||||
this.hass.entities
|
||||
);
|
||||
const toLoad: string[] = [];
|
||||
for (const domain of domains) {
|
||||
if (!this._loadedIntegrationTitles.has(domain)) {
|
||||
toLoad.push(domain);
|
||||
}
|
||||
}
|
||||
if (!toLoad.length) return;
|
||||
toLoad.forEach((d) => this._loadedIntegrationTitles.add(d));
|
||||
await this.hass.loadBackendTranslation("title", toLoad);
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const canInstallUpdates = this._filterInstallableUpdateEntities(
|
||||
this.hass.states,
|
||||
this._showSkipped
|
||||
const installableUpdates = this._filterInstallableUpdateEntities(
|
||||
this.hass.states
|
||||
);
|
||||
const notInstallableUpdates = this._filterNotInstallableUpdateEntities(
|
||||
this.hass.states,
|
||||
this._showSkipped
|
||||
);
|
||||
const skippedUpdates = this._filterSkippedUpdateEntities(
|
||||
this.hass.states,
|
||||
this._showSkipped
|
||||
);
|
||||
|
||||
const groups = this._groupUpdates(
|
||||
installableUpdates,
|
||||
this._entitySources,
|
||||
this.hass.localize,
|
||||
this.hass.entities,
|
||||
this.hass.locale.language
|
||||
);
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
@@ -118,19 +205,60 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
</ha-dropdown>
|
||||
</div>
|
||||
<div class="content">
|
||||
${canInstallUpdates.length
|
||||
${groups.map(
|
||||
(group) => html`
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
<div class="card-header">
|
||||
<div class="title" role="heading" aria-level="2">
|
||||
${group.title}
|
||||
</div>
|
||||
${group.showUpdateAll
|
||||
? html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
size="small"
|
||||
.group=${group}
|
||||
.disabled=${group.entities.every((entity) =>
|
||||
updateIsInstalling(entity)
|
||||
)}
|
||||
@click=${this._updateAll}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.updates.update_all"
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-config-updates
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.updateEntities=${group.entities}
|
||||
showAll
|
||||
></ha-config-updates>
|
||||
</div>
|
||||
</ha-card>
|
||||
`
|
||||
)}
|
||||
${skippedUpdates.length
|
||||
? html`
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
<div class="title" role="heading" aria-level="2">
|
||||
${this.hass.localize("ui.panel.config.updates.title", {
|
||||
count: canInstallUpdates.length,
|
||||
})}
|
||||
<div class="card-header">
|
||||
<div class="title" role="heading" aria-level="2">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.updates.title_skipped",
|
||||
{
|
||||
count: skippedUpdates.length,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ha-config-updates
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.updateEntities=${canInstallUpdates}
|
||||
.updateEntities=${skippedUpdates}
|
||||
showAll
|
||||
></ha-config-updates>
|
||||
</div>
|
||||
@@ -141,13 +269,15 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
? html`
|
||||
<ha-card outlined>
|
||||
<div class="card-content">
|
||||
<div class="title" role="heading" aria-level="2">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.updates.title_not_installable",
|
||||
{
|
||||
count: notInstallableUpdates.length,
|
||||
}
|
||||
)}
|
||||
<div class="card-header">
|
||||
<div class="title" role="heading" aria-level="2">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.updates.title_not_installable",
|
||||
{
|
||||
count: notInstallableUpdates.length,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ha-config-updates
|
||||
.hass=${this.hass}
|
||||
@@ -159,7 +289,7 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
</ha-card>
|
||||
`
|
||||
: nothing}
|
||||
${canInstallUpdates.length + notInstallableUpdates.length
|
||||
${groups.length + notInstallableUpdates.length + skippedUpdates.length
|
||||
? nothing
|
||||
: html`
|
||||
<ha-card outlined>
|
||||
@@ -211,9 +341,28 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
checkForEntityUpdates(this, this.hass);
|
||||
}
|
||||
|
||||
private async _updateAll(ev: Event) {
|
||||
const group = (ev.currentTarget as any).group as UpdateGroup;
|
||||
const entityIds = group.entities
|
||||
.filter((entity) => !updateIsInstalling(entity))
|
||||
.map((entity) => entity.entity_id);
|
||||
if (!entityIds.length) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await installUpdates(this.hass, entityIds);
|
||||
} catch (err: any) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize("ui.panel.config.updates.update_all_failed"),
|
||||
text: extractApiErrorMessage(err),
|
||||
warning: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _filterInstallableUpdateEntities = memoizeOne(
|
||||
(entities: HassEntities, showSkipped: boolean) =>
|
||||
filterUpdateEntitiesParameterized(entities, showSkipped, false)
|
||||
(entities: HassEntities) =>
|
||||
filterUpdateEntitiesParameterized(entities, false, false)
|
||||
);
|
||||
|
||||
private _filterNotInstallableUpdateEntities = memoizeOne(
|
||||
@@ -221,6 +370,111 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
filterUpdateEntitiesParameterized(entities, showSkipped, true)
|
||||
);
|
||||
|
||||
private _filterSkippedUpdateEntities = memoizeOne(
|
||||
(entities: HassEntities, showSkipped: boolean): UpdateEntity[] => {
|
||||
if (!showSkipped) {
|
||||
return [];
|
||||
}
|
||||
return filterUpdateEntities(entities).filter(
|
||||
(entity) =>
|
||||
latestVersionIsSkipped(entity) &&
|
||||
supportsFeature(entity, UpdateEntityFeature.INSTALL)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
private _groupUpdates = memoizeOne(
|
||||
(
|
||||
entities: UpdateEntity[],
|
||||
entitySources: EntitySources | undefined,
|
||||
localize: HomeAssistant["localize"],
|
||||
entityRegistry: HomeAssistant["entities"],
|
||||
language: string
|
||||
): UpdateGroup[] => {
|
||||
if (!entities.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const systemEntities: UpdateEntity[] = [];
|
||||
const appEntities: UpdateEntity[] = [];
|
||||
const byDomain = new Map<string, UpdateEntity[]>();
|
||||
const otherIntegrationEntities: UpdateEntity[] = [];
|
||||
|
||||
for (const entity of entities) {
|
||||
if (isSystemUpdate(entity)) {
|
||||
systemEntities.push(entity);
|
||||
continue;
|
||||
}
|
||||
const domain =
|
||||
entitySources?.[entity.entity_id]?.domain ??
|
||||
entityRegistry[entity.entity_id]?.platform;
|
||||
if (domain === "hassio") {
|
||||
appEntities.push(entity);
|
||||
continue;
|
||||
}
|
||||
if (!domain) {
|
||||
otherIntegrationEntities.push(entity);
|
||||
continue;
|
||||
}
|
||||
if (!byDomain.has(domain)) {
|
||||
byDomain.set(domain, []);
|
||||
}
|
||||
byDomain.get(domain)!.push(entity);
|
||||
}
|
||||
|
||||
const multiInstanceGroups: UpdateGroup[] = [];
|
||||
byDomain.forEach((entries, domain) => {
|
||||
if (entries.length >= 2) {
|
||||
multiInstanceGroups.push({
|
||||
key: domain,
|
||||
title: domainToName(localize, domain),
|
||||
entities: entries,
|
||||
showUpdateAll: true,
|
||||
});
|
||||
} else {
|
||||
otherIntegrationEntities.push(...entries);
|
||||
}
|
||||
});
|
||||
|
||||
multiInstanceGroups.sort((a, b) =>
|
||||
caseInsensitiveStringCompare(a.title, b.title, language)
|
||||
);
|
||||
|
||||
const groups: UpdateGroup[] = [];
|
||||
|
||||
if (systemEntities.length) {
|
||||
groups.push({
|
||||
key: SYSTEM_KEY,
|
||||
title: localize("ui.panel.config.updates.group_system"),
|
||||
entities: systemEntities,
|
||||
showUpdateAll: false,
|
||||
});
|
||||
}
|
||||
|
||||
groups.push(...multiInstanceGroups);
|
||||
|
||||
if (otherIntegrationEntities.length) {
|
||||
groups.push({
|
||||
key: INTEGRATIONS_KEY,
|
||||
title: localize("ui.panel.config.updates.group_integrations"),
|
||||
entities: otherIntegrationEntities,
|
||||
showUpdateAll: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (appEntities.length) {
|
||||
groups.push({
|
||||
key: APPS_KEY,
|
||||
title: localize("ui.panel.config.updates.group_apps"),
|
||||
entities: appEntities,
|
||||
showUpdateAll: true,
|
||||
});
|
||||
}
|
||||
|
||||
return groups;
|
||||
}
|
||||
);
|
||||
|
||||
static styles = css`
|
||||
.content {
|
||||
padding: 28px 20px 0;
|
||||
@@ -247,8 +501,15 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--ha-space-2);
|
||||
padding: var(--ha-space-4) var(--ha-space-2) 0 var(--ha-space-4);
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: var(--ha-space-4) var(--ha-space-4) 0;
|
||||
font-size: var(--ha-font-size-l);
|
||||
}
|
||||
|
||||
|
||||
@@ -104,9 +104,6 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
|
||||
|
||||
return html`
|
||||
<ha-list-item-button
|
||||
class=${ifDefined(
|
||||
entity.attributes.skipped_version ? "skipped" : undefined
|
||||
)}
|
||||
.entity_id=${entity.entity_id}
|
||||
.hasMeta=${!this.narrow}
|
||||
@click=${this._openMoreInfo}
|
||||
@@ -166,9 +163,6 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
|
||||
static get styles(): CSSResultGroup[] {
|
||||
return [
|
||||
css`
|
||||
.skipped {
|
||||
background: var(--secondary-background-color);
|
||||
}
|
||||
ha-list-item-button {
|
||||
--md-list-item-leading-icon-size: 40px;
|
||||
}
|
||||
|
||||
@@ -12,8 +12,6 @@ import {
|
||||
import { computeDeviceNameDisplay } from "../../../../common/entity/compute_device_name";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-adaptive-dialog";
|
||||
import "../../../../components/ha-list";
|
||||
import "../../../../components/ha-list-item";
|
||||
import "../../../../components/ha-spinner";
|
||||
import type { AutomationConfig } from "../../../../data/automation";
|
||||
import { showAutomationEditor } from "../../../../data/automation";
|
||||
@@ -35,15 +33,38 @@ import {
|
||||
} from "../../../../data/device/device_automation";
|
||||
import type { ScriptConfig } from "../../../../data/script";
|
||||
import { showScriptEditor } from "../../../../data/script";
|
||||
import type { SceneEntities } from "../../../../data/scene";
|
||||
import { showSceneEditor } from "../../../../data/scene";
|
||||
import "../../../../dialogs/add-to/ha-add-to-action-list";
|
||||
import type {
|
||||
AddToActionListActionSelectedEvent,
|
||||
AddToActionListItem,
|
||||
AddToActionListSection,
|
||||
} from "../../../../dialogs/add-to/ha-add-to-action-list";
|
||||
import {
|
||||
addToActionHandler,
|
||||
type AddToActionKey,
|
||||
} from "../../../../dialogs/more-info/add-to";
|
||||
createAddToSceneEntities,
|
||||
type AddToAutomationScriptActionKey,
|
||||
} from "../../../../dialogs/add-to/add-to";
|
||||
import { haStyle, haStyleDialog } from "../../../../resources/styles";
|
||||
import type { DeviceAddToDialogParams } from "./show-dialog-device-add-to";
|
||||
|
||||
type DeviceLegacyAddToActionType =
|
||||
| "trigger"
|
||||
| "condition"
|
||||
| "automation_action"
|
||||
| "script_action";
|
||||
|
||||
type DeviceAddToAction =
|
||||
| (AddToActionListItem & {
|
||||
kind: "add-to";
|
||||
key: AddToAutomationScriptActionKey;
|
||||
})
|
||||
| (AddToActionListItem & {
|
||||
kind: "legacy";
|
||||
legacyType: DeviceLegacyAddToActionType;
|
||||
})
|
||||
| (AddToActionListItem & { kind: "scene" });
|
||||
|
||||
@customElement("dialog-device-add-to")
|
||||
export class DialogDeviceAddTo extends LitElement {
|
||||
@state()
|
||||
@@ -132,11 +153,18 @@ export class DialogDeviceAddTo extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const deviceName = computeDeviceNameDisplay(
|
||||
this._params.device,
|
||||
this._i18n.localize,
|
||||
this._states
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-adaptive-dialog
|
||||
.open=${this._open}
|
||||
header-title=${this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.title"
|
||||
"ui.dialogs.more_info_control.add_to.title",
|
||||
{ target: deviceName }
|
||||
)}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
@@ -151,80 +179,62 @@ export class DialogDeviceAddTo extends LitElement {
|
||||
if (!this._params) {
|
||||
return nothing;
|
||||
}
|
||||
const deviceName = computeDeviceNameDisplay(
|
||||
this._params.device,
|
||||
this._i18n.localize,
|
||||
this._states
|
||||
);
|
||||
|
||||
const sections: AddToActionListSection<DeviceAddToAction>[] = [
|
||||
{
|
||||
title: this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.automations_heading"
|
||||
),
|
||||
actions: [
|
||||
{
|
||||
kind: "add-to",
|
||||
key: "automation_trigger",
|
||||
iconPath: mdiRobotOutline,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.automation_trigger"
|
||||
),
|
||||
},
|
||||
{
|
||||
kind: "add-to",
|
||||
key: "automation_condition",
|
||||
iconPath: mdiPlaylistCheck,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.automation_condition"
|
||||
),
|
||||
},
|
||||
{
|
||||
kind: "add-to",
|
||||
key: "automation_action",
|
||||
iconPath: mdiPlayCircleOutline,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.automation_action"
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: this._i18n.localize(
|
||||
"ui.panel.config.devices.script.scripts_heading"
|
||||
),
|
||||
actions: [
|
||||
{
|
||||
kind: "add-to",
|
||||
key: "script_action",
|
||||
iconPath: mdiScriptTextOutline,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.script_action"
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
this._addSceneSection(sections);
|
||||
|
||||
return html`
|
||||
<h3 class="section-header">
|
||||
${this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.automations_heading"
|
||||
)}
|
||||
</h3>
|
||||
<ha-list>
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
data-type="automation_trigger"
|
||||
@click=${this._handleNewAction}
|
||||
data-dialog="close"
|
||||
>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiRobotOutline}></ha-svg-icon>
|
||||
${this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.actions.automation_trigger",
|
||||
{ target: deviceName }
|
||||
)}
|
||||
</ha-list-item>
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
data-type="automation_condition"
|
||||
@click=${this._handleNewAction}
|
||||
data-dialog="close"
|
||||
>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiPlaylistCheck}></ha-svg-icon>
|
||||
${this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.actions.automation_condition",
|
||||
{ target: deviceName }
|
||||
)}
|
||||
</ha-list-item>
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
data-type="automation_action"
|
||||
@click=${this._handleNewAction}
|
||||
data-dialog="close"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiPlayCircleOutline}
|
||||
></ha-svg-icon>
|
||||
${this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.actions.automation_action",
|
||||
{ target: deviceName }
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-list>
|
||||
<h3 class="section-header">
|
||||
${this._i18n.localize("ui.panel.config.devices.script.scripts_heading")}
|
||||
</h3>
|
||||
<ha-list>
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
data-type="script_action"
|
||||
@click=${this._handleNewAction}
|
||||
data-dialog="close"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiScriptTextOutline}
|
||||
></ha-svg-icon>
|
||||
${this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.actions.script_action",
|
||||
{ target: deviceName }
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-list>
|
||||
${this._renderSceneSection(deviceName)}
|
||||
<ha-add-to-action-list
|
||||
.sections=${sections}
|
||||
@add-to-list-action-selected=${this._handleActionSelected}
|
||||
></ha-add-to-action-list>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -242,12 +252,6 @@ export class DialogDeviceAddTo extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const deviceName = computeDeviceNameDisplay(
|
||||
this._params.device,
|
||||
this._i18n.localize,
|
||||
this._states
|
||||
);
|
||||
|
||||
const hasTriggers = Boolean(this._triggers?.length);
|
||||
const hasConditions = Boolean(this._conditions?.length);
|
||||
const hasActions = Boolean(this._actions?.length);
|
||||
@@ -263,165 +267,138 @@ export class DialogDeviceAddTo extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<h3 class="section-header">
|
||||
${this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.automations_heading"
|
||||
)}
|
||||
</h3>
|
||||
${hasTriggers || hasConditions || hasActions
|
||||
? html`
|
||||
<ha-list>
|
||||
${hasTriggers
|
||||
? html`
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
data-type="trigger"
|
||||
@click=${this._handleLegacyAction}
|
||||
data-dialog="close"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiRobotOutline}
|
||||
></ha-svg-icon>
|
||||
${this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.actions.automation_trigger",
|
||||
{ target: deviceName }
|
||||
)}
|
||||
</ha-list-item>
|
||||
`
|
||||
: nothing}
|
||||
${hasConditions
|
||||
? html`
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
data-type="condition"
|
||||
@click=${this._handleLegacyAction}
|
||||
data-dialog="close"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiPlaylistCheck}
|
||||
></ha-svg-icon>
|
||||
${this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.actions.automation_condition",
|
||||
{ target: deviceName }
|
||||
)}
|
||||
</ha-list-item>
|
||||
`
|
||||
: nothing}
|
||||
${hasActions
|
||||
? html`
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
data-type="automation_action"
|
||||
@click=${this._handleLegacyAction}
|
||||
data-dialog="close"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiPlayCircleOutline}
|
||||
></ha-svg-icon>
|
||||
${this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.actions.automation_action",
|
||||
{ target: deviceName }
|
||||
)}
|
||||
</ha-list-item>
|
||||
`
|
||||
: nothing}
|
||||
</ha-list>
|
||||
`
|
||||
: html`
|
||||
<ha-list>
|
||||
<ha-list-item noninteractive>
|
||||
${this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.no_automations"
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-list>
|
||||
`}
|
||||
<h3 class="section-header">
|
||||
${this._i18n.localize("ui.panel.config.devices.script.scripts_heading")}
|
||||
</h3>
|
||||
${hasActions
|
||||
? html`
|
||||
<ha-list>
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
data-type="script_action"
|
||||
@click=${this._handleLegacyAction}
|
||||
data-dialog="close"
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiScriptTextOutline}
|
||||
></ha-svg-icon>
|
||||
${this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.actions.script_action",
|
||||
{ target: deviceName }
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-list>
|
||||
`
|
||||
: html`
|
||||
<ha-list>
|
||||
<ha-list-item noninteractive>
|
||||
${this._i18n.localize(
|
||||
"ui.panel.config.devices.script.no_scripts"
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-list>
|
||||
`}
|
||||
${this._renderSceneSection(deviceName)}
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderSceneSection(deviceName: string) {
|
||||
if (!this._params?.entityIds.length) {
|
||||
return nothing;
|
||||
const automationActions: DeviceAddToAction[] = [];
|
||||
if (hasTriggers) {
|
||||
automationActions.push({
|
||||
kind: "legacy",
|
||||
legacyType: "trigger",
|
||||
iconPath: mdiRobotOutline,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.automation_trigger"
|
||||
),
|
||||
});
|
||||
}
|
||||
if (hasConditions) {
|
||||
automationActions.push({
|
||||
kind: "legacy",
|
||||
legacyType: "condition",
|
||||
iconPath: mdiPlaylistCheck,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.automation_condition"
|
||||
),
|
||||
});
|
||||
}
|
||||
if (hasActions) {
|
||||
automationActions.push({
|
||||
kind: "legacy",
|
||||
legacyType: "automation_action",
|
||||
iconPath: mdiPlayCircleOutline,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.automation_action"
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
const scriptActions: DeviceAddToAction[] = hasActions
|
||||
? [
|
||||
{
|
||||
kind: "legacy",
|
||||
legacyType: "script_action",
|
||||
iconPath: mdiScriptTextOutline,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.script_action"
|
||||
),
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
const sections: AddToActionListSection<DeviceAddToAction>[] = [
|
||||
{
|
||||
title: this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.automations_heading"
|
||||
),
|
||||
actions: automationActions,
|
||||
empty: automationActions.length
|
||||
? undefined
|
||||
: this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.no_automations"
|
||||
),
|
||||
},
|
||||
{
|
||||
title: this._i18n.localize(
|
||||
"ui.panel.config.devices.script.scripts_heading"
|
||||
),
|
||||
actions: scriptActions,
|
||||
empty: scriptActions.length
|
||||
? undefined
|
||||
: this._i18n.localize("ui.panel.config.devices.script.no_scripts"),
|
||||
},
|
||||
];
|
||||
this._addSceneSection(sections);
|
||||
|
||||
return html`
|
||||
<h3 class="section-header">
|
||||
${this._i18n.localize("ui.panel.config.devices.scene.scenes_heading")}
|
||||
</h3>
|
||||
<ha-list>
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
@click=${this._handleCreateScene}
|
||||
data-dialog="close"
|
||||
>
|
||||
<ha-svg-icon slot="graphic" .path=${mdiPalette}></ha-svg-icon>
|
||||
${this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.actions.scene",
|
||||
{ target: deviceName }
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-list>
|
||||
<ha-add-to-action-list
|
||||
.sections=${sections}
|
||||
@add-to-list-action-selected=${this._handleActionSelected}
|
||||
></ha-add-to-action-list>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleNewAction(ev: Event) {
|
||||
private _addSceneSection(
|
||||
sections: AddToActionListSection<DeviceAddToAction>[]
|
||||
): void {
|
||||
if (!this._params?.canCreateScene || !this._params.entityIds.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
sections.push({
|
||||
title: this._i18n.localize(
|
||||
"ui.panel.config.devices.scene.scenes_heading"
|
||||
),
|
||||
actions: [
|
||||
{
|
||||
kind: "scene",
|
||||
iconPath: mdiPalette,
|
||||
name: this._i18n.localize(
|
||||
"ui.dialogs.more_info_control.add_to.action_options.scene"
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
private _handleActionSelected(
|
||||
ev: AddToActionListActionSelectedEvent<DeviceAddToAction>
|
||||
) {
|
||||
if (!this._params) {
|
||||
return;
|
||||
}
|
||||
const key = (ev.currentTarget as HTMLElement).dataset
|
||||
.type as AddToActionKey;
|
||||
|
||||
const { action } = ev.detail;
|
||||
if (action.kind === "scene") {
|
||||
this._handleCreateScene();
|
||||
return;
|
||||
}
|
||||
|
||||
if (action.kind === "add-to") {
|
||||
this._handleAddToAction(action.key);
|
||||
return;
|
||||
}
|
||||
|
||||
this._handleLegacyAction(action.legacyType);
|
||||
}
|
||||
|
||||
private _handleAddToAction(key: AddToAutomationScriptActionKey) {
|
||||
if (!this._params) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeDialog();
|
||||
addToActionHandler(key, { device_id: this._params.device.id });
|
||||
}
|
||||
|
||||
// When new_triggers_conditions labs feature is promoted, this whole method can be removed.
|
||||
private _handleLegacyAction(ev: Event) {
|
||||
if (!this._params) {
|
||||
return;
|
||||
}
|
||||
const type = (ev.currentTarget as HTMLElement).dataset.type as
|
||||
| "trigger"
|
||||
| "condition"
|
||||
| "automation_action"
|
||||
| "script_action";
|
||||
|
||||
private _handleLegacyAction(type: DeviceLegacyAddToActionType) {
|
||||
this.closeDialog();
|
||||
|
||||
if (type === "script_action") {
|
||||
@@ -430,29 +407,28 @@ export class DialogDeviceAddTo extends LitElement {
|
||||
newScript.sequence = [this._actions[0]];
|
||||
}
|
||||
showScriptEditor(newScript, true);
|
||||
} else {
|
||||
const newAutomation = {} as AutomationConfig;
|
||||
if (type === "trigger" && this._triggers?.length) {
|
||||
newAutomation.triggers = [this._triggers[0]];
|
||||
} else if (type === "condition" && this._conditions?.length) {
|
||||
newAutomation.conditions = [this._conditions[0]];
|
||||
} else if (type === "automation_action" && this._actions?.length) {
|
||||
newAutomation.actions = [this._actions[0]];
|
||||
}
|
||||
showAutomationEditor(newAutomation, true);
|
||||
return;
|
||||
}
|
||||
|
||||
const newAutomation = {} as AutomationConfig;
|
||||
if (type === "trigger" && this._triggers?.length) {
|
||||
newAutomation.triggers = [this._triggers[0]];
|
||||
} else if (type === "condition" && this._conditions?.length) {
|
||||
newAutomation.conditions = [this._conditions[0]];
|
||||
} else if (type === "automation_action" && this._actions?.length) {
|
||||
newAutomation.actions = [this._actions[0]];
|
||||
}
|
||||
showAutomationEditor(newAutomation, true);
|
||||
}
|
||||
|
||||
private _handleCreateScene() {
|
||||
if (!this._params) {
|
||||
return;
|
||||
}
|
||||
const entities: SceneEntities = {};
|
||||
for (const entityId of this._params.entityIds) {
|
||||
entities[entityId] = "";
|
||||
}
|
||||
this.closeDialog();
|
||||
showSceneEditor({ entities });
|
||||
showSceneEditor({
|
||||
entities: createAddToSceneEntities(this._params.entityIds),
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -469,14 +445,6 @@ export class DialogDeviceAddTo extends LitElement {
|
||||
padding: var(--ha-space-4);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
padding: var(--ha-space-2) var(--ha-space-4) 0;
|
||||
margin: 0;
|
||||
font-size: var(--ha-font-size-m);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface DeviceAddToDialogParams {
|
||||
device: DeviceRegistryEntry;
|
||||
newTriggersConditions: boolean;
|
||||
entityIds: string[];
|
||||
canCreateScene: boolean;
|
||||
}
|
||||
|
||||
export const loadDeviceAddToDialog = () => import("./ha-device-add-to-dialog");
|
||||
|
||||
@@ -86,6 +86,7 @@ import { domainToName } from "../../../data/integration";
|
||||
import { regenerateEntityIds } from "../../../data/regenerate_entity_ids";
|
||||
import type { RelatedResult } from "../../../data/search";
|
||||
import { findRelated } from "../../../data/search";
|
||||
import { filterAddToSceneEntityIds } from "../../../dialogs/add-to/add-to";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
@@ -379,7 +380,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
if (changedProps.has("deviceId")) {
|
||||
this._findRelated();
|
||||
// Broadcast device context for quick bar
|
||||
fireEvent(this, "hass-quick-bar-context", {
|
||||
fireEvent(this, "hass-related-context", {
|
||||
itemType: "device",
|
||||
itemId: this.deviceId,
|
||||
});
|
||||
@@ -424,6 +425,11 @@ export class HaConfigDevicePage extends LitElement {
|
||||
this._entityReg,
|
||||
this.hass.devices
|
||||
);
|
||||
const sceneEntityIds = filterAddToSceneEntityIds(
|
||||
this._entityIds(entities),
|
||||
this._entityReg,
|
||||
this.hass.states
|
||||
);
|
||||
const entitiesByCategory = this._entitiesByCategory(entities);
|
||||
const quickLinkCounts = this._getQuickLinkCounts(entities, this._related);
|
||||
const batteryEntity = this._batteryEntity(entities);
|
||||
@@ -531,7 +537,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
: this.hass.localize("ui.panel.config.devices.add_prompt_enabled");
|
||||
|
||||
const hasSceneSupport =
|
||||
isComponentLoaded(this.hass.config, "scene") && entities.length;
|
||||
isComponentLoaded(this.hass.config, "scene") && sceneEntityIds.length;
|
||||
|
||||
const relatedCard =
|
||||
isComponentLoaded(this.hass.config, "automation") ||
|
||||
@@ -551,7 +557,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiPlus}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.add_to.title"
|
||||
"ui.dialogs.more_info_control.add_to.item"
|
||||
)}
|
||||
</ha-button>
|
||||
</h1>
|
||||
@@ -1366,10 +1372,18 @@ export class HaConfigDevicePage extends LitElement {
|
||||
this._entityReg,
|
||||
this.hass.devices
|
||||
).map((entity) => entity.entity_id);
|
||||
const sceneEntityIds = filterAddToSceneEntityIds(
|
||||
entityIds,
|
||||
this._entityReg,
|
||||
this.hass.states
|
||||
);
|
||||
showDeviceAddToDialog(this, {
|
||||
device,
|
||||
newTriggersConditions: this._newTriggersConditions,
|
||||
entityIds,
|
||||
entityIds: sceneEntityIds,
|
||||
canCreateScene:
|
||||
isComponentLoaded(this.hass.config, "scene") &&
|
||||
sceneEntityIds.length > 0,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -118,7 +118,6 @@ class HaConfigEnergy extends LitElement {
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.backPath=${this._searchParms.has("historyBack")
|
||||
? undefined
|
||||
: "/config/lovelace/dashboards"}
|
||||
|
||||
@@ -10,9 +10,7 @@ import {
|
||||
mdiFlask,
|
||||
mdiHammer,
|
||||
mdiInformationOutline,
|
||||
mdiLabel,
|
||||
mdiLightningBolt,
|
||||
mdiMapMarkerRadius,
|
||||
mdiMemory,
|
||||
mdiMicrophone,
|
||||
mdiNetwork,
|
||||
@@ -47,6 +45,7 @@ import type { RouterOptions } from "../../layouts/hass-router-page";
|
||||
import { HassRouterPage } from "../../layouts/hass-router-page";
|
||||
import type { PageNavigation } from "../../layouts/hass-tabs-subpage";
|
||||
import type { HomeAssistant, Route } from "../../types";
|
||||
import { areaConfigTabs } from "./common/area-config-tabs";
|
||||
|
||||
declare global {
|
||||
// for fire event
|
||||
@@ -417,34 +416,7 @@ export const configSections: Record<string, PageNavigation[]> = {
|
||||
adminOnly: true,
|
||||
},
|
||||
],
|
||||
areas: [
|
||||
{
|
||||
component: "areas",
|
||||
path: "/config/areas",
|
||||
translationKey: "ui.panel.config.areas.caption",
|
||||
iconPath: mdiSofa,
|
||||
iconColor: "#2D338F",
|
||||
core: true,
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
component: "labels",
|
||||
path: "/config/labels",
|
||||
translationKey: "ui.panel.config.labels.caption",
|
||||
iconPath: mdiLabel,
|
||||
iconColor: "#2D338F",
|
||||
core: true,
|
||||
adminOnly: true,
|
||||
},
|
||||
{
|
||||
component: "zone",
|
||||
path: "/config/zone",
|
||||
translationKey: "ui.panel.config.zone.caption",
|
||||
iconPath: mdiMapMarkerRadius,
|
||||
iconColor: "#E48629",
|
||||
adminOnly: true,
|
||||
},
|
||||
],
|
||||
areas: areaConfigTabs,
|
||||
general: [
|
||||
{
|
||||
path: "/config/general",
|
||||
@@ -631,6 +603,10 @@ class HaPanelConfig extends HassRouterPage {
|
||||
tag: "ha-config-integrations",
|
||||
load: () => import("./integrations/ha-config-integrations"),
|
||||
},
|
||||
categories: {
|
||||
tag: "ha-config-categories",
|
||||
load: () => import("./category/ha-config-categories"),
|
||||
},
|
||||
labels: {
|
||||
tag: "ha-config-labels",
|
||||
load: () => import("./labels/ha-config-labels"),
|
||||
|
||||
@@ -58,8 +58,6 @@ const DATA_SET_CONFIG: SeriesOption = {
|
||||
class HaConfigHardwareOverview extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _error?: string;
|
||||
@@ -249,7 +247,6 @@ class HaConfigHardwareOverview extends SubscribeMixin(LitElement) {
|
||||
<hass-tabs-subpage
|
||||
back-path="/config/system"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.tabs=${hardwareTabs(this.hass)}
|
||||
>
|
||||
|
||||
@@ -511,7 +511,6 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.backPath=${this._searchParams.has("historyBack")
|
||||
? undefined
|
||||
: "/config"}
|
||||
|
||||
+15
-10
@@ -5,7 +5,7 @@ import { dynamicElement } from "../../../../../common/dom/dynamic-element-direct
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../../common/entity/compute_domain";
|
||||
import { computeDeviceName } from "../../../../../common/entity/compute_device_name";
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import { navigate, setRefreshUrl } from "../../../../../common/navigate";
|
||||
import "../../../../../components/ha-dialog-footer";
|
||||
import "../../../../../components/ha-icon-button-arrow-prev";
|
||||
import "../../../../../components/ha-button";
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
type ExtEntityRegistryEntry,
|
||||
} from "../../../../../data/entity/entity_registry";
|
||||
import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box";
|
||||
import { OVERRIDE_DEVICE_CLASSES } from "../../../entities/entity-registry-settings-editor";
|
||||
import "./matter-add-device/matter-add-device-apple-home";
|
||||
import "./matter-add-device/matter-add-device-existing";
|
||||
import "./matter-add-device/matter-add-device-generic";
|
||||
@@ -100,6 +101,8 @@ class DialogMatterAddDevice extends LitElement {
|
||||
public showDialog(): void {
|
||||
this._open = true;
|
||||
this._unsub = watchForNewMatterDevice(this.hass, (device) => {
|
||||
// make sure a refresh of the page will navigate to the device page, old iOS apps will refresh the webview when commissioning is done
|
||||
setRefreshUrl(`/config/devices/device/${device.id}`);
|
||||
this._newDevice = device;
|
||||
this._step = "device_added";
|
||||
this._fetchMainEntity();
|
||||
@@ -137,15 +140,17 @@ class DialogMatterAddDevice extends LitElement {
|
||||
entityIds
|
||||
);
|
||||
|
||||
const mainEntry = Object.values(entries).find(
|
||||
(e) => e.original_name === null
|
||||
);
|
||||
if (!mainEntry) return;
|
||||
|
||||
const domain = computeDomain(mainEntry.entity_id);
|
||||
if (domain === "cover" || domain === "binary_sensor") {
|
||||
this._mainEntity = mainEntry;
|
||||
}
|
||||
this._mainEntity = Object.values(entries).find((entry) => {
|
||||
if (entry.entity_category) return false;
|
||||
const domain = computeDomain(entry.entity_id);
|
||||
const deviceClasses = OVERRIDE_DEVICE_CLASSES[domain];
|
||||
if (!deviceClasses) return false;
|
||||
const deviceClass = entry.device_class ?? entry.original_device_class;
|
||||
if (!deviceClass) return false;
|
||||
return deviceClasses.some(
|
||||
(classes) => classes.length > 1 && classes.includes(deviceClass)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private _dialogClosed(): void {
|
||||
|
||||
@@ -39,8 +39,6 @@ export class HaConfigPerson extends LitElement {
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _storageItems?: Person[];
|
||||
@@ -61,7 +59,6 @@ export class HaConfigPerson extends LitElement {
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
back-path="/config"
|
||||
.tabs=${configSections.persons}
|
||||
|
||||
@@ -27,8 +27,6 @@ export class HaConfigVoiceAssistantsAssistants extends LitElement {
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
protected render() {
|
||||
@@ -39,7 +37,6 @@ export class HaConfigVoiceAssistantsAssistants extends LitElement {
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
back-path="/config"
|
||||
.route=${this.route}
|
||||
.tabs=${voiceAssistantTabs}
|
||||
|
||||
@@ -234,7 +234,6 @@ export class HaConfigZone extends SubscribeMixin(LitElement) {
|
||||
return html`
|
||||
<hass-tabs-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.backPath=${this._searchParms.has("historyBack")
|
||||
? undefined
|
||||
|
||||
@@ -14,6 +14,7 @@ import "../lovelace/hui-root";
|
||||
import type { Lovelace } from "../lovelace/types";
|
||||
import "../lovelace/views/hui-view";
|
||||
import "../lovelace/views/hui-view-container";
|
||||
import { DEFAULT_POWER_COLLECTION_KEY } from "./constants";
|
||||
|
||||
@customElement("ha-panel-energy")
|
||||
class PanelEnergy extends LitElement {
|
||||
@@ -86,7 +87,15 @@ class PanelEnergy extends LitElement {
|
||||
|
||||
private async _setLovelace() {
|
||||
const config: LovelaceConfig = await generateLovelaceDashboardStrategy(
|
||||
{ strategy: { type: "energy" } },
|
||||
{
|
||||
strategy: {
|
||||
type: "energy",
|
||||
default_collection:
|
||||
this.route?.path === "/now"
|
||||
? DEFAULT_POWER_COLLECTION_KEY
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
this.hass
|
||||
);
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { getEnergyDataCollection } from "../../../data/energy";
|
||||
import {
|
||||
EMPTY_PREFERENCES,
|
||||
getEnergyDataCollection,
|
||||
} from "../../../data/energy";
|
||||
import type { EnergyPreferences } from "../../../data/energy";
|
||||
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
|
||||
import type { LovelaceConfig } from "../../../data/lovelace/config/types";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { LocalizeKeys } from "../../../common/translations/localize";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceStrategyDependency } from "../../lovelace/strategies/types";
|
||||
import {
|
||||
DEFAULT_ENERGY_COLLECTION_KEY,
|
||||
DEFAULT_POWER_COLLECTION_KEY,
|
||||
@@ -59,25 +61,18 @@ const WIZARD_VIEW = {
|
||||
cards: [{ type: "custom:energy-setup-wizard-card" }],
|
||||
};
|
||||
|
||||
const EMPTY_PREFERENCES: EnergyPreferences = {
|
||||
energy_sources: [],
|
||||
device_consumption: [],
|
||||
device_consumption_water: [],
|
||||
};
|
||||
|
||||
export interface EnergyDashboardStrategyConfig extends LovelaceStrategyConfig {
|
||||
type: "energy";
|
||||
default_collection?: string;
|
||||
}
|
||||
|
||||
@customElement("energy-dashboard-strategy")
|
||||
export class EnergyDashboardStrategy extends ReactiveElement {
|
||||
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
|
||||
|
||||
static async generate(
|
||||
_config: EnergyDashboardStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceConfig> {
|
||||
const prefs = await fetchEnergyPrefs(hass);
|
||||
const prefs = await fetchEnergyPrefs(hass, _config.default_collection);
|
||||
|
||||
if (
|
||||
!prefs ||
|
||||
@@ -150,20 +145,19 @@ export class EnergyDashboardStrategy extends ReactiveElement {
|
||||
}
|
||||
|
||||
async function fetchEnergyPrefs(
|
||||
hass: HomeAssistant
|
||||
hass: HomeAssistant,
|
||||
defaultCollection?: string
|
||||
): Promise<EnergyPreferences> {
|
||||
const collection = getEnergyDataCollection(hass, {
|
||||
key: DEFAULT_ENERGY_COLLECTION_KEY,
|
||||
key: defaultCollection || DEFAULT_ENERGY_COLLECTION_KEY,
|
||||
});
|
||||
|
||||
return await new Promise<EnergyPreferences>((resolve) => {
|
||||
const unsub = collection.subscribe((data) => {
|
||||
unsub();
|
||||
resolve(data.prefs || EMPTY_PREFERENCES);
|
||||
});
|
||||
});
|
||||
try {
|
||||
await collection.refresh();
|
||||
} catch (err: any) {
|
||||
if (err.code === "not_found") {
|
||||
return EMPTY_PREFERENCES;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
return collection.prefs || EMPTY_PREFERENCES;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -5,13 +5,10 @@ import { getEnergyDataCollection } from "../../../data/energy";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
|
||||
import type { LovelaceStrategyDependency } from "../../lovelace/strategies/types";
|
||||
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../constants";
|
||||
|
||||
@customElement("energy-overview-view-strategy")
|
||||
export class EnergyOverviewViewStrategy extends ReactiveElement {
|
||||
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
|
||||
|
||||
static async generate(
|
||||
_config: LovelaceStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
|
||||
@@ -12,12 +12,9 @@ import {
|
||||
LARGE_SCREEN_CONDITION,
|
||||
SMALL_SCREEN_CONDITION,
|
||||
} from "../../lovelace/strategies/helpers/view-columns-conditions";
|
||||
import type { LovelaceStrategyDependency } from "../../lovelace/strategies/types";
|
||||
|
||||
@customElement("energy-view-strategy")
|
||||
export class EnergyViewStrategy extends ReactiveElement {
|
||||
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
|
||||
|
||||
static async generate(
|
||||
_config: LovelaceStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
|
||||
@@ -6,12 +6,9 @@ import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
|
||||
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../constants";
|
||||
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
import type { LovelaceStrategyDependency } from "../../lovelace/strategies/types";
|
||||
|
||||
@customElement("gas-view-strategy")
|
||||
export class GasViewStrategy extends ReactiveElement {
|
||||
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
|
||||
|
||||
static async generate(
|
||||
_config: LovelaceStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
|
||||
@@ -8,12 +8,9 @@ import { DEFAULT_ENERGY_COLLECTION_KEY } from "../constants";
|
||||
import { shouldShowFloorsAndAreas } from "./show-floors-and-areas";
|
||||
import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section";
|
||||
import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge";
|
||||
import type { LovelaceStrategyDependency } from "../../lovelace/strategies/types";
|
||||
|
||||
@customElement("power-view-strategy")
|
||||
export class PowerViewStrategy extends ReactiveElement {
|
||||
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
|
||||
|
||||
static async generate(
|
||||
_config: LovelaceStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
@@ -24,7 +21,9 @@ export class PowerViewStrategy extends ReactiveElement {
|
||||
const energyCollection = getEnergyDataCollection(hass, {
|
||||
key: collectionKey,
|
||||
});
|
||||
await energyCollection.refresh();
|
||||
if (!energyCollection.prefs) {
|
||||
await energyCollection.refresh();
|
||||
}
|
||||
const prefs = energyCollection.prefs;
|
||||
|
||||
const hasPowerSources = prefs?.energy_sources.some((source) => {
|
||||
|
||||
@@ -5,14 +5,11 @@ import type { LovelaceSectionConfig } from "../../../data/lovelace/config/sectio
|
||||
import type { LovelaceStrategyConfig } from "../../../data/lovelace/config/strategy";
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceStrategyDependency } from "../../lovelace/strategies/types";
|
||||
import { DEFAULT_ENERGY_COLLECTION_KEY } from "../constants";
|
||||
import { shouldShowFloorsAndAreas } from "./show-floors-and-areas";
|
||||
|
||||
@customElement("water-view-strategy")
|
||||
export class WaterViewStrategy extends ReactiveElement {
|
||||
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
|
||||
|
||||
static async generate(
|
||||
_config: LovelaceStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
|
||||
@@ -7,6 +7,7 @@ import { styleMap } from "lit/directives/style-map";
|
||||
import { atLeastVersion } from "../../common/config/version";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-svg-icon";
|
||||
import { updateAreaRegistryEntry } from "../../data/area/area_registry";
|
||||
@@ -25,10 +26,7 @@ import { showDeviceRegistryDetailDialog } from "../config/devices/device-registr
|
||||
import { showAddIntegrationDialog } from "../config/integrations/show-add-integration-dialog";
|
||||
import "../lovelace/hui-root";
|
||||
import type { ExtraActionItem } from "../lovelace/hui-root";
|
||||
import {
|
||||
checkStrategyShouldRegenerate,
|
||||
generateLovelaceDashboardStrategy,
|
||||
} from "../lovelace/strategies/get-strategy";
|
||||
import { expandLovelaceConfigStrategies } from "../lovelace/strategies/get-strategy";
|
||||
import type { Lovelace } from "../lovelace/types";
|
||||
import { showEditHomeDialog } from "./dialogs/show-dialog-edit-home";
|
||||
import { showNewOverviewDialog } from "./dialogs/show-dialog-new-overview";
|
||||
@@ -95,37 +93,33 @@ class PanelHome extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as this["hass"] | undefined;
|
||||
if (!oldHass) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Locale changed: regenerate to refresh translated content
|
||||
if (oldHass.localize !== this.hass.localize) {
|
||||
const oldHass = changedProps.get("hass") as this["hass"];
|
||||
if (oldHass && oldHass.localize !== this.hass.localize) {
|
||||
this._setLovelace();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.hass.config.state !== "RUNNING") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Home Assistant just started: run the full setup
|
||||
if (oldHass.config.state !== "RUNNING") {
|
||||
this._setup();
|
||||
return;
|
||||
}
|
||||
|
||||
// A registry the strategy depends on changed: regenerate
|
||||
if (
|
||||
checkStrategyShouldRegenerate(
|
||||
"dashboard",
|
||||
this._strategyConfig.strategy,
|
||||
oldHass,
|
||||
this.hass
|
||||
)
|
||||
) {
|
||||
this._debounceRegenerateStrategy();
|
||||
if (oldHass && this.hass) {
|
||||
// If the entity registry changed, ask the user if they want to refresh the config
|
||||
if (
|
||||
oldHass.entities !== this.hass.entities ||
|
||||
oldHass.devices !== this.hass.devices ||
|
||||
oldHass.areas !== this.hass.areas ||
|
||||
oldHass.floors !== this.hass.floors ||
|
||||
oldHass.panels !== this.hass.panels
|
||||
) {
|
||||
if (this.hass.config.state === "RUNNING") {
|
||||
this._debounceRegistriesChanged();
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If ha started, refresh the config
|
||||
if (
|
||||
this.hass.config.state === "RUNNING" &&
|
||||
oldHass.config.state !== "RUNNING"
|
||||
) {
|
||||
this._setup();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,12 +144,12 @@ class PanelHome extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _debounceRegenerateStrategy = debounce(
|
||||
() => this._regenerateStrategyConfig(),
|
||||
private _debounceRegistriesChanged = debounce(
|
||||
() => this._registriesChanged(),
|
||||
200
|
||||
);
|
||||
|
||||
private _regenerateStrategyConfig() {
|
||||
private _registriesChanged = async () => {
|
||||
// If on an area view that no longer exists, redirect to overview
|
||||
const path = this.route?.path?.split("/")[1];
|
||||
if (path?.startsWith("areas-")) {
|
||||
@@ -166,7 +160,7 @@ class PanelHome extends LitElement {
|
||||
}
|
||||
}
|
||||
this._setLovelace();
|
||||
}
|
||||
};
|
||||
|
||||
private _updateExtraActionItems() {
|
||||
const path = this.route?.path?.split("/")[1];
|
||||
@@ -326,8 +320,11 @@ class PanelHome extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private get _strategyConfig(): LovelaceDashboardStrategyConfig {
|
||||
return {
|
||||
private async _setLovelace() {
|
||||
if (this._loadConfigPromise) {
|
||||
await this._loadConfigPromise;
|
||||
}
|
||||
const strategyConfig: LovelaceDashboardStrategyConfig = {
|
||||
strategy: {
|
||||
type: "home",
|
||||
favorite_entities: this._config.favorite_entities,
|
||||
@@ -337,17 +334,16 @@ class PanelHome extends LitElement {
|
||||
shortcuts: this._config.shortcuts,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async _setLovelace() {
|
||||
if (this._loadConfigPromise) {
|
||||
await this._loadConfigPromise;
|
||||
}
|
||||
const config = await generateLovelaceDashboardStrategy(
|
||||
this._strategyConfig,
|
||||
const config = await expandLovelaceConfigStrategies(
|
||||
strategyConfig,
|
||||
this.hass
|
||||
);
|
||||
|
||||
if (deepEqual(config, this._lovelace?.config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._lovelace = {
|
||||
config: config,
|
||||
rawConfig: config,
|
||||
|
||||
@@ -86,7 +86,6 @@ class HuiCounterActionsCardFeature
|
||||
static getStubConfig(): CounterActionsCardFeatureConfig {
|
||||
return {
|
||||
type: "counter-actions",
|
||||
actions: COUNTER_ACTIONS.map((action) => action),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -110,20 +110,9 @@ class HuiLawnMowerCommandCardFeature
|
||||
| undefined;
|
||||
}
|
||||
|
||||
static getStubConfig(
|
||||
hass: HomeAssistant,
|
||||
context: LovelaceCardFeatureContext
|
||||
): LawnMowerCommandsCardFeatureConfig {
|
||||
const stateObj = context.entity_id
|
||||
? hass.states[context.entity_id]
|
||||
: undefined;
|
||||
static getStubConfig(): LawnMowerCommandsCardFeatureConfig {
|
||||
return {
|
||||
type: "lawn-mower-commands",
|
||||
commands: stateObj
|
||||
? LAWN_MOWER_COMMANDS.filter((c) =>
|
||||
supportsLawnMowerCommand(stateObj, c)
|
||||
).slice(0, 3)
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -162,28 +151,28 @@ class HuiLawnMowerCommandCardFeature
|
||||
|
||||
const stateObj = this._stateObj as LawnMowerEntity;
|
||||
|
||||
const commands = this._config.commands ?? LAWN_MOWER_COMMANDS;
|
||||
|
||||
return html`
|
||||
<ha-control-button-group>
|
||||
${LAWN_MOWER_COMMANDS.filter(
|
||||
(command) =>
|
||||
supportsLawnMowerCommand(stateObj, command) &&
|
||||
this._config?.commands?.includes(command)
|
||||
).map((command) => {
|
||||
const button = LAWN_MOWER_COMMANDS_BUTTONS[command](stateObj);
|
||||
return html`
|
||||
<ha-control-button
|
||||
.entry=${button}
|
||||
.label=${this.hass!.localize(
|
||||
// @ts-ignore
|
||||
`ui.dialogs.more_info_control.lawn_mower.${button.translationKey}`
|
||||
)}
|
||||
@click=${this._onCommandTap}
|
||||
.disabled=${button.disabled || stateObj.state === UNAVAILABLE}
|
||||
>
|
||||
<ha-svg-icon .path=${button.icon}></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
`;
|
||||
})}
|
||||
${commands
|
||||
.filter((command) => supportsLawnMowerCommand(stateObj, command))
|
||||
.map((command) => {
|
||||
const button = LAWN_MOWER_COMMANDS_BUTTONS[command](stateObj);
|
||||
return html`
|
||||
<ha-control-button
|
||||
.entry=${button}
|
||||
.label=${this.hass!.localize(
|
||||
// @ts-ignore
|
||||
`ui.dialogs.more_info_control.lawn_mower.${button.translationKey}`
|
||||
)}
|
||||
@click=${this._onCommandTap}
|
||||
.disabled=${button.disabled || stateObj.state === UNAVAILABLE}
|
||||
>
|
||||
<ha-svg-icon .path=${button.icon}></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
`;
|
||||
})}
|
||||
</ha-control-button-group>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -52,6 +52,12 @@ export const VACUUM_COMMANDS_FEATURES: Record<
|
||||
return_home: [VacuumEntityFeature.RETURN_HOME],
|
||||
};
|
||||
|
||||
export const VACUUM_DEFAULT_COMMANDS: VacuumCommand[] = [
|
||||
"start_pause",
|
||||
"stop",
|
||||
"return_home",
|
||||
];
|
||||
|
||||
export const supportsVacuumCommand = (
|
||||
stateObj: HassEntity,
|
||||
command: VacuumCommand
|
||||
@@ -154,20 +160,9 @@ class HuiVacuumCommandCardFeature
|
||||
| undefined;
|
||||
}
|
||||
|
||||
static getStubConfig(
|
||||
hass: HomeAssistant,
|
||||
context: LovelaceCardFeatureContext
|
||||
): VacuumCommandsCardFeatureConfig {
|
||||
const stateObj = context.entity_id
|
||||
? hass.states[context.entity_id]
|
||||
: undefined;
|
||||
static getStubConfig(): VacuumCommandsCardFeatureConfig {
|
||||
return {
|
||||
type: "vacuum-commands",
|
||||
commands: stateObj
|
||||
? VACUUM_COMMANDS.filter((c) =>
|
||||
supportsVacuumCommand(stateObj, c)
|
||||
).slice(0, 3)
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -204,28 +199,28 @@ class HuiVacuumCommandCardFeature
|
||||
|
||||
const stateObj = this._stateObj as VacuumEntity;
|
||||
|
||||
const commands = this._config.commands ?? VACUUM_DEFAULT_COMMANDS;
|
||||
|
||||
return html`
|
||||
<ha-control-button-group>
|
||||
${VACUUM_COMMANDS.filter(
|
||||
(command) =>
|
||||
supportsVacuumCommand(stateObj, command) &&
|
||||
this._config?.commands?.includes(command)
|
||||
).map((command) => {
|
||||
const button = VACUUM_COMMANDS_BUTTONS[command](stateObj);
|
||||
return html`
|
||||
<ha-control-button
|
||||
.entry=${button}
|
||||
.label=${this.hass!.localize(
|
||||
// @ts-ignore
|
||||
`ui.dialogs.more_info_control.vacuum.${button.translationKey}`
|
||||
)}
|
||||
@click=${this._onCommandTap}
|
||||
.disabled=${button.disabled || stateObj.state === UNAVAILABLE}
|
||||
>
|
||||
<ha-svg-icon .path=${button.icon}></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
`;
|
||||
})}
|
||||
${commands
|
||||
.filter((command) => supportsVacuumCommand(stateObj, command))
|
||||
.map((command) => {
|
||||
const button = VACUUM_COMMANDS_BUTTONS[command](stateObj);
|
||||
return html`
|
||||
<ha-control-button
|
||||
.entry=${button}
|
||||
.label=${this.hass!.localize(
|
||||
// @ts-ignore
|
||||
`ui.dialogs.more_info_control.vacuum.${button.translationKey}`
|
||||
)}
|
||||
@click=${this._onCommandTap}
|
||||
.disabled=${button.disabled || stateObj.state === UNAVAILABLE}
|
||||
>
|
||||
<ha-svg-icon .path=${button.icon}></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
`;
|
||||
})}
|
||||
</ha-control-button-group>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from "@mdi/js";
|
||||
import type { FuseIndex } from "fuse.js";
|
||||
import Fuse from "fuse.js";
|
||||
import { getAreasFloorHierarchy } from "../../../../common/areas/areas-floor-hierarchy";
|
||||
import { computeAreaName } from "../../../../common/entity/compute_area_name";
|
||||
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
@@ -13,7 +14,6 @@ import { computeEntityName } from "../../../../common/entity/compute_entity_name
|
||||
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
||||
import { stringCompare } from "../../../../common/string/compare";
|
||||
import { entityComboBoxKeys } from "../../../../data/entity/entity_picker";
|
||||
import { getFloorAreaLookup } from "../../../../data/floor_registry";
|
||||
import { domainToName } from "../../../../data/integration";
|
||||
import { multiTermSortedSearch } from "../../../../resources/fuseMultiTerm";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -206,23 +206,24 @@ export function buildEntityTree(input: BuildEntityTreeInput): EntityTree {
|
||||
return stringCompare(an, bn, language);
|
||||
};
|
||||
|
||||
const buildDeviceNodes = (source: Map<string, string[]>): DeviceNode[] =>
|
||||
[...source.entries()]
|
||||
.map(([id, ids]) => {
|
||||
const device = deviceReg[id];
|
||||
return {
|
||||
id,
|
||||
name: (device ? computeDeviceName(device) : undefined) ?? id,
|
||||
entityIds: ids.sort(sortByName),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => stringCompare(a.name, b.name, language));
|
||||
|
||||
const buildAreaNode = (areaId: string): AreaNode | undefined => {
|
||||
const area = areaReg[areaId];
|
||||
if (!area) return undefined;
|
||||
const directIds = (areaDirectEntities.get(areaId) ?? []).sort(sortByName);
|
||||
const byDevice = areaDeviceEntities.get(areaId);
|
||||
const devices: DeviceNode[] = byDevice
|
||||
? [...byDevice.entries()]
|
||||
.map(([id, ids]) => {
|
||||
const device = deviceReg[id];
|
||||
return {
|
||||
id,
|
||||
name: (device ? computeDeviceName(device) : undefined) ?? id,
|
||||
entityIds: ids.sort(sortByName),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => stringCompare(a.name, b.name, language))
|
||||
: [];
|
||||
const devices = byDevice ? buildDeviceNodes(byDevice) : [];
|
||||
if (!directIds.length && !devices.length) return undefined;
|
||||
return {
|
||||
id: area.area_id,
|
||||
@@ -235,14 +236,14 @@ export function buildEntityTree(input: BuildEntityTreeInput): EntityTree {
|
||||
|
||||
const areas = Object.values(areaReg);
|
||||
const floors = Object.values(floorReg);
|
||||
const floorAreaLookup = getFloorAreaLookup(areas);
|
||||
const hierarchy = getAreasFloorHierarchy(floors, areas);
|
||||
|
||||
const floorNodes: FloorNode[] = floors
|
||||
.map((floor) => {
|
||||
const areaList = (floorAreaLookup[floor.floor_id] ?? [])
|
||||
.map((a) => buildAreaNode(a.area_id))
|
||||
.filter((a): a is AreaNode => !!a)
|
||||
.sort((a, b) => stringCompare(a.name, b.name, language));
|
||||
const floorNodes: FloorNode[] = hierarchy.floors
|
||||
.map(({ id, areas: areaIds }) => {
|
||||
const floor = floorReg[id];
|
||||
const areaList = areaIds
|
||||
.map((areaId) => buildAreaNode(areaId))
|
||||
.filter((a): a is AreaNode => !!a);
|
||||
if (!areaList.length) return undefined;
|
||||
return {
|
||||
id: floor.floor_id,
|
||||
@@ -252,26 +253,11 @@ export function buildEntityTree(input: BuildEntityTreeInput): EntityTree {
|
||||
areas: areaList,
|
||||
};
|
||||
})
|
||||
.filter((f): f is FloorNode => !!f)
|
||||
.sort((a, b) => stringCompare(a.name, b.name, language));
|
||||
.filter((f): f is FloorNode => !!f);
|
||||
|
||||
const otherAreas = areas
|
||||
.filter((a) => !a.floor_id || !floorReg[a.floor_id])
|
||||
.map((a) => buildAreaNode(a.area_id))
|
||||
.filter((a): a is AreaNode => !!a)
|
||||
.sort((a, b) => stringCompare(a.name, b.name, language));
|
||||
|
||||
const buildDeviceNodes = (source: Map<string, string[]>): DeviceNode[] =>
|
||||
[...source.entries()]
|
||||
.map(([id, ids]) => {
|
||||
const device = deviceReg[id];
|
||||
return {
|
||||
id,
|
||||
name: (device ? computeDeviceName(device) : undefined) ?? id,
|
||||
entityIds: ids.sort(sortByName),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => stringCompare(a.name, b.name, language));
|
||||
const otherAreas = hierarchy.areas
|
||||
.map((areaId) => buildAreaNode(areaId))
|
||||
.filter((a): a is AreaNode => !!a);
|
||||
|
||||
const buildDomainGroups = (source: Map<string, string[]>): DomainGroup[] =>
|
||||
[...source.entries()]
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { HaFormSchema } from "../../../../components/ha-form/types";
|
||||
|
||||
interface CustomizableListSchemaParams {
|
||||
field: string;
|
||||
customize: boolean;
|
||||
options: { value: string; label: string }[];
|
||||
}
|
||||
|
||||
export const customizableListSchema = ({
|
||||
field,
|
||||
customize,
|
||||
options,
|
||||
}: CustomizableListSchemaParams) =>
|
||||
[
|
||||
{
|
||||
name: "customize",
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
...(customize
|
||||
? ([
|
||||
{
|
||||
name: field,
|
||||
selector: {
|
||||
select: {
|
||||
mode: "list",
|
||||
reorder: true,
|
||||
multiple: true,
|
||||
options,
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[])
|
||||
: []),
|
||||
] as const satisfies readonly HaFormSchema[];
|
||||
|
||||
// `customize` is form-only and never stored in the config.
|
||||
export const customizableListData = <T extends object>(
|
||||
config: T,
|
||||
field: string
|
||||
): T & { customize: boolean } => ({
|
||||
...config,
|
||||
customize: (config as Record<string, unknown>)[field] !== undefined,
|
||||
});
|
||||
|
||||
// Dropping the field lets the feature fall back to its own default.
|
||||
export const processCustomizableListValue = <T extends object>(
|
||||
value: T & { customize?: boolean },
|
||||
field: string,
|
||||
defaults: readonly string[]
|
||||
): T => {
|
||||
const { customize, ...rest } = value;
|
||||
const config = rest as Record<string, unknown>;
|
||||
if (customize && !config[field]) {
|
||||
config[field] = [...defaults];
|
||||
} else if (!customize) {
|
||||
delete config[field];
|
||||
}
|
||||
return config as unknown as T;
|
||||
};
|
||||
+34
-36
@@ -2,16 +2,20 @@ import { 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 type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
COUNTER_ACTIONS,
|
||||
type LovelaceCardFeatureContext,
|
||||
type CounterActionsCardFeatureConfig,
|
||||
import type {
|
||||
CounterActionsCardFeatureConfig,
|
||||
LovelaceCardFeatureContext,
|
||||
} from "../../card-features/types";
|
||||
import { COUNTER_ACTIONS } from "../../card-features/types";
|
||||
import type { LovelaceCardFeatureEditor } from "../../types";
|
||||
import {
|
||||
customizableListData,
|
||||
customizableListSchema,
|
||||
processCustomizableListValue,
|
||||
} from "./customizable-list-feature";
|
||||
|
||||
@customElement("hui-counter-actions-card-feature-editor")
|
||||
export class HuiCounterActionsCardFeatureEditor
|
||||
@@ -28,26 +32,17 @@ export class HuiCounterActionsCardFeatureEditor
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc) =>
|
||||
[
|
||||
{
|
||||
name: "actions",
|
||||
selector: {
|
||||
select: {
|
||||
multiple: true,
|
||||
mode: "list",
|
||||
reorder: true,
|
||||
options: COUNTER_ACTIONS.map((action) => ({
|
||||
value: action,
|
||||
label: `${localize(
|
||||
`ui.panel.lovelace.editor.features.types.counter-actions.actions.${action}`
|
||||
)}`,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const
|
||||
private _schema = memoizeOne((customize: boolean) =>
|
||||
customizableListSchema({
|
||||
field: "actions",
|
||||
customize,
|
||||
options: COUNTER_ACTIONS.map((action) => ({
|
||||
value: action,
|
||||
label: this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.features.types.counter-actions.actions_list.${action}`
|
||||
),
|
||||
})),
|
||||
})
|
||||
);
|
||||
|
||||
protected render() {
|
||||
@@ -55,12 +50,13 @@ export class HuiCounterActionsCardFeatureEditor
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const schema = this._schema(this.hass.localize);
|
||||
const data = customizableListData(this._config, "actions");
|
||||
const schema = this._schema(data.customize);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._config}
|
||||
.data=${data}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
@@ -69,19 +65,21 @@ export class HuiCounterActionsCardFeatureEditor
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||
const config =
|
||||
processCustomizableListValue<CounterActionsCardFeatureConfig>(
|
||||
ev.detail.value,
|
||||
"actions",
|
||||
COUNTER_ACTIONS
|
||||
);
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
default:
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
}
|
||||
};
|
||||
) =>
|
||||
this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.features.types.counter-actions.${schema.name}`
|
||||
);
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
+39
-38
@@ -3,7 +3,6 @@ import { 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 type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -14,6 +13,11 @@ import type {
|
||||
} from "../../card-features/types";
|
||||
import { LAWN_MOWER_COMMANDS } from "../../card-features/types";
|
||||
import type { LovelaceCardFeatureEditor } from "../../types";
|
||||
import {
|
||||
customizableListData,
|
||||
customizableListSchema,
|
||||
processCustomizableListValue,
|
||||
} from "./customizable-list-feature";
|
||||
|
||||
@customElement("hui-lawn-mower-commands-card-feature-editor")
|
||||
export class HuiLawnMowerCommandsCardFeatureEditor
|
||||
@@ -31,27 +35,19 @@ export class HuiLawnMowerCommandsCardFeatureEditor
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc, stateObj?: HassEntity) =>
|
||||
[
|
||||
{
|
||||
name: "commands",
|
||||
selector: {
|
||||
select: {
|
||||
multiple: true,
|
||||
mode: "list",
|
||||
options: LAWN_MOWER_COMMANDS.filter(
|
||||
(command) =>
|
||||
stateObj && supportsLawnMowerCommand(stateObj, command)
|
||||
).map((command) => ({
|
||||
value: command,
|
||||
label: `${localize(
|
||||
`ui.panel.lovelace.editor.features.types.lawn-mower-commands.commands_list.${command}`
|
||||
)}`,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const
|
||||
(stateObj: HassEntity | undefined, customize: boolean) =>
|
||||
customizableListSchema({
|
||||
field: "commands",
|
||||
customize,
|
||||
options: LAWN_MOWER_COMMANDS.filter(
|
||||
(command) => stateObj && supportsLawnMowerCommand(stateObj, command)
|
||||
).map((command) => ({
|
||||
value: command,
|
||||
label: this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.features.types.lawn-mower-commands.commands_list.${command}`
|
||||
),
|
||||
})),
|
||||
})
|
||||
);
|
||||
|
||||
protected render() {
|
||||
@@ -60,15 +56,16 @@ export class HuiLawnMowerCommandsCardFeatureEditor
|
||||
}
|
||||
|
||||
const stateObj = this.context?.entity_id
|
||||
? this.hass.states[this.context?.entity_id]
|
||||
? this.hass.states[this.context.entity_id]
|
||||
: undefined;
|
||||
|
||||
const schema = this._schema(this.hass.localize, stateObj);
|
||||
const data = customizableListData(this._config, "commands");
|
||||
const schema = this._schema(stateObj, data.customize);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._config}
|
||||
.data=${data}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
@@ -77,23 +74,27 @@ export class HuiLawnMowerCommandsCardFeatureEditor
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||
const stateObj = this.context?.entity_id
|
||||
? this.hass!.states[this.context.entity_id]
|
||||
: undefined;
|
||||
const defaults = LAWN_MOWER_COMMANDS.filter(
|
||||
(command) => stateObj && supportsLawnMowerCommand(stateObj, command)
|
||||
);
|
||||
const config =
|
||||
processCustomizableListValue<LawnMowerCommandsCardFeatureConfig>(
|
||||
ev.detail.value,
|
||||
"commands",
|
||||
defaults
|
||||
);
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "commands":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.features.types.lawn-mower-commands.${schema.name}`
|
||||
);
|
||||
default:
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
}
|
||||
};
|
||||
) =>
|
||||
this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.features.types.lawn-mower-commands.${schema.name}`
|
||||
);
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
+43
-39
@@ -3,17 +3,24 @@ import { 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 type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { supportsVacuumCommand } from "../../card-features/hui-vacuum-commands-card-feature";
|
||||
import {
|
||||
supportsVacuumCommand,
|
||||
VACUUM_DEFAULT_COMMANDS,
|
||||
} from "../../card-features/hui-vacuum-commands-card-feature";
|
||||
import type {
|
||||
LovelaceCardFeatureContext,
|
||||
VacuumCommandsCardFeatureConfig,
|
||||
} from "../../card-features/types";
|
||||
import { VACUUM_COMMANDS } from "../../card-features/types";
|
||||
import type { LovelaceCardFeatureEditor } from "../../types";
|
||||
import {
|
||||
customizableListData,
|
||||
customizableListSchema,
|
||||
processCustomizableListValue,
|
||||
} from "./customizable-list-feature";
|
||||
|
||||
@customElement("hui-vacuum-commands-card-feature-editor")
|
||||
export class HuiVacuumCommandsCardFeatureEditor
|
||||
@@ -31,27 +38,19 @@ export class HuiVacuumCommandsCardFeatureEditor
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(localize: LocalizeFunc, stateObj?: HassEntity) =>
|
||||
[
|
||||
{
|
||||
name: "commands",
|
||||
selector: {
|
||||
select: {
|
||||
multiple: true,
|
||||
mode: "list",
|
||||
options: VACUUM_COMMANDS.filter(
|
||||
(command) =>
|
||||
stateObj && supportsVacuumCommand(stateObj, command)
|
||||
).map((command) => ({
|
||||
value: command,
|
||||
label: `${localize(
|
||||
`ui.panel.lovelace.editor.features.types.vacuum-commands.commands_list.${command}`
|
||||
)}`,
|
||||
})),
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const
|
||||
(stateObj: HassEntity | undefined, customize: boolean) =>
|
||||
customizableListSchema({
|
||||
field: "commands",
|
||||
customize,
|
||||
options: VACUUM_COMMANDS.filter(
|
||||
(command) => stateObj && supportsVacuumCommand(stateObj, command)
|
||||
).map((command) => ({
|
||||
value: command,
|
||||
label: this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.features.types.vacuum-commands.commands_list.${command}`
|
||||
),
|
||||
})),
|
||||
})
|
||||
);
|
||||
|
||||
protected render() {
|
||||
@@ -60,15 +59,16 @@ export class HuiVacuumCommandsCardFeatureEditor
|
||||
}
|
||||
|
||||
const stateObj = this.context?.entity_id
|
||||
? this.hass.states[this.context?.entity_id]
|
||||
? this.hass.states[this.context.entity_id]
|
||||
: undefined;
|
||||
|
||||
const schema = this._schema(this.hass.localize, stateObj);
|
||||
const data = customizableListData(this._config, "commands");
|
||||
const schema = this._schema(stateObj, data.customize);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this._config}
|
||||
.data=${data}
|
||||
.schema=${schema}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
@@ -77,23 +77,27 @@ export class HuiVacuumCommandsCardFeatureEditor
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||
const stateObj = this.context?.entity_id
|
||||
? this.hass!.states[this.context.entity_id]
|
||||
: undefined;
|
||||
const defaults = VACUUM_DEFAULT_COMMANDS.filter(
|
||||
(command) => stateObj && supportsVacuumCommand(stateObj, command)
|
||||
);
|
||||
const config =
|
||||
processCustomizableListValue<VacuumCommandsCardFeatureConfig>(
|
||||
ev.detail.value,
|
||||
"commands",
|
||||
defaults
|
||||
);
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "commands":
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.features.types.vacuum-commands.${schema.name}`
|
||||
);
|
||||
default:
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
}
|
||||
};
|
||||
) =>
|
||||
this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.features.types.vacuum-commands.${schema.name}`
|
||||
);
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
removeSearchParam,
|
||||
} from "../../common/url/search-params";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
import "../../components/ha-button";
|
||||
import { domainToName } from "../../data/integration";
|
||||
import { subscribeLovelaceUpdates } from "../../data/lovelace";
|
||||
@@ -35,10 +36,7 @@ import { checkLovelaceConfig } from "./common/check-lovelace-config";
|
||||
import { loadLovelaceResources } from "./common/load-resources";
|
||||
import { showSaveDialog } from "./editor/show-save-config-dialog";
|
||||
import "./hui-root";
|
||||
import {
|
||||
checkStrategyShouldRegenerate,
|
||||
generateLovelaceDashboardStrategy,
|
||||
} from "./strategies/get-strategy";
|
||||
import { generateLovelaceDashboardStrategy } from "./strategies/get-strategy";
|
||||
import type { Lovelace } from "./types";
|
||||
import { generateDefaultView } from "./views/default-view";
|
||||
import { fetchDashboards } from "../../data/lovelace/dashboard";
|
||||
@@ -52,6 +50,12 @@ interface LovelacePanelConfig {
|
||||
let editorLoaded = false;
|
||||
let resourcesLoaded = false;
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"strategy-config-changed": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@customElement("ha-panel-lovelace")
|
||||
export class LovelacePanel extends LitElement {
|
||||
@property({ attribute: false }) public panel?: PanelInfo<
|
||||
@@ -125,6 +129,7 @@ export class LovelacePanel extends LitElement {
|
||||
.route=${this.route}
|
||||
.narrow=${this.narrow}
|
||||
@config-refresh=${this._forceFetchConfig}
|
||||
@strategy-config-changed=${this._strategyConfigChanged}
|
||||
></hui-root>
|
||||
`;
|
||||
}
|
||||
@@ -190,26 +195,61 @@ export class LovelacePanel extends LitElement {
|
||||
this.lovelace &&
|
||||
isStrategyDashboard(this.lovelace.rawConfig)
|
||||
) {
|
||||
// If the entity registry changed, ask the user if they want to refresh the config
|
||||
if (
|
||||
oldHass.entities !== this.hass.entities ||
|
||||
oldHass.devices !== this.hass.devices ||
|
||||
oldHass.areas !== this.hass.areas ||
|
||||
oldHass.floors !== this.hass.floors
|
||||
) {
|
||||
if (this.hass.config.state === "RUNNING") {
|
||||
this._debounceRegistriesChanged();
|
||||
}
|
||||
}
|
||||
// If ha started, refresh the config
|
||||
if (
|
||||
this.hass.config.state === "RUNNING" &&
|
||||
(oldHass.config.state !== "RUNNING" ||
|
||||
checkStrategyShouldRegenerate(
|
||||
"dashboard",
|
||||
this.lovelace.rawConfig.strategy,
|
||||
oldHass,
|
||||
this.hass
|
||||
))
|
||||
oldHass.config.state !== "RUNNING"
|
||||
) {
|
||||
this._debounceRegenerateStrategy();
|
||||
this._regenerateStrategyConfig();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _debounceRegenerateStrategy = debounce(
|
||||
() => this._regenerateStrategyConfig(),
|
||||
private _debounceRegistriesChanged = debounce(
|
||||
() => this._registriesChanged(),
|
||||
200
|
||||
);
|
||||
|
||||
private _registriesChanged = async () => {
|
||||
if (!this.hass || !this.lovelace) {
|
||||
return;
|
||||
}
|
||||
const rawConfig = this.lovelace.rawConfig;
|
||||
|
||||
if (!isStrategyDashboard(rawConfig)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldConfig = this.lovelace.config;
|
||||
const generatedConfig = await generateLovelaceDashboardStrategy(
|
||||
rawConfig,
|
||||
this.hass!
|
||||
);
|
||||
|
||||
const newConfig = checkLovelaceConfig(generatedConfig) as LovelaceConfig;
|
||||
|
||||
// Regenerate if the config changed
|
||||
if (!deepEqual(newConfig, oldConfig)) {
|
||||
this._regenerateStrategyConfig();
|
||||
}
|
||||
};
|
||||
|
||||
private _strategyConfigChanged = (ev: CustomEvent) => {
|
||||
ev.stopPropagation();
|
||||
this._regenerateStrategyConfig();
|
||||
};
|
||||
|
||||
private async _regenerateStrategyConfig() {
|
||||
if (!this.hass || !this.lovelace) {
|
||||
return;
|
||||
|
||||
@@ -6,7 +6,6 @@ import { storage } from "../../../common/decorators/storage";
|
||||
import { deepEqual } from "../../../common/util/deep-equal";
|
||||
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { LovelaceSectionElement } from "../../../data/lovelace";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
@@ -26,10 +25,7 @@ import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog"
|
||||
import { addCard, replaceCard } from "../editor/config-util";
|
||||
import { performDeleteCard } from "../editor/delete-card";
|
||||
import { parseLovelaceCardPath } from "../editor/lovelace-path";
|
||||
import {
|
||||
checkStrategyShouldRegenerate,
|
||||
generateLovelaceSectionStrategy,
|
||||
} from "../strategies/get-strategy";
|
||||
import { generateLovelaceSectionStrategy } from "../strategies/get-strategy";
|
||||
import type { Lovelace } from "../types";
|
||||
import { DEFAULT_SECTION_LAYOUT } from "./const";
|
||||
|
||||
@@ -110,36 +106,9 @@ export class HuiSection extends ConditionalListenerMixin<LovelaceSectionConfig>(
|
||||
(!oldConfig || this.config !== oldConfig)
|
||||
) {
|
||||
this._initializeConfig();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!changedProperties.has("hass")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldHass = changedProperties.get("hass") as HomeAssistant | undefined;
|
||||
if (
|
||||
oldHass &&
|
||||
this.hass &&
|
||||
isStrategySection(this.config) &&
|
||||
this.hass.config.state === "RUNNING" &&
|
||||
(oldHass.config.state !== "RUNNING" ||
|
||||
checkStrategyShouldRegenerate(
|
||||
"section",
|
||||
this.config.strategy,
|
||||
oldHass,
|
||||
this.hass
|
||||
))
|
||||
) {
|
||||
this._debounceRefreshConfig();
|
||||
}
|
||||
}
|
||||
|
||||
private _debounceRefreshConfig = debounce(
|
||||
() => this._initializeConfig(),
|
||||
200
|
||||
);
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.removeEventListener(
|
||||
|
||||
@@ -6,7 +6,6 @@ import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
|
||||
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { LovelaceStrategyDependency } from "../types";
|
||||
import {
|
||||
AREA_STRATEGY_GROUP_ICONS,
|
||||
computeAreaTileCardConfig,
|
||||
@@ -35,12 +34,6 @@ const computeHeadingCard = (
|
||||
|
||||
@customElement("area-view-strategy")
|
||||
export class AreaViewStrategy extends ReactiveElement {
|
||||
static registryDependencies: readonly LovelaceStrategyDependency[] = [
|
||||
"entities",
|
||||
"devices",
|
||||
"areas",
|
||||
];
|
||||
|
||||
static async generate(
|
||||
config: AreaViewStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
|
||||
@@ -4,10 +4,7 @@ import { customElement } from "lit/decorators";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import type { LovelaceViewRawConfig } from "../../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type {
|
||||
LovelaceStrategyEditor,
|
||||
LovelaceStrategyDependency,
|
||||
} from "../types";
|
||||
import type { LovelaceStrategyEditor } from "../types";
|
||||
import type {
|
||||
AreaViewStrategyConfig,
|
||||
EntitiesDisplay,
|
||||
@@ -34,10 +31,6 @@ export interface AreasDashboardStrategyConfig {
|
||||
|
||||
@customElement("areas-dashboard-strategy")
|
||||
export class AreasDashboardStrategy extends ReactiveElement {
|
||||
static registryDependencies: readonly LovelaceStrategyDependency[] = [
|
||||
"areas",
|
||||
];
|
||||
|
||||
static async generate(
|
||||
config: AreasDashboardStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
type AreaControlDomain,
|
||||
} from "../../card-features/types";
|
||||
import type { AreaCardConfig, HeadingCardConfig } from "../../cards/types";
|
||||
import type { LovelaceStrategyDependency } from "../types";
|
||||
import type { EntitiesDisplay } from "./area-view-strategy";
|
||||
import {
|
||||
computeAreaPath,
|
||||
@@ -39,13 +38,6 @@ export interface AreasViewStrategyConfig {
|
||||
|
||||
@customElement("areas-overview-view-strategy")
|
||||
export class AreasOverviewViewStrategy extends ReactiveElement {
|
||||
static registryDependencies: readonly LovelaceStrategyDependency[] = [
|
||||
"entities",
|
||||
"devices",
|
||||
"areas",
|
||||
"floors",
|
||||
];
|
||||
|
||||
static async generate(
|
||||
config: AreasViewStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
|
||||
@@ -23,20 +23,12 @@ import type {
|
||||
LovelaceDashboardStrategyGetCreateSuggestions,
|
||||
LovelaceSectionStrategy,
|
||||
LovelaceStrategy,
|
||||
LovelaceStrategyDependency,
|
||||
LovelaceViewStrategy,
|
||||
} from "./types";
|
||||
|
||||
const MAX_WAIT_STRATEGY_LOAD = 5000;
|
||||
const CUSTOM_PREFIX = "custom:";
|
||||
|
||||
const DEFAULT_REGISTRY_DEPENDENCIES: readonly LovelaceStrategyDependency[] = [
|
||||
"entities",
|
||||
"devices",
|
||||
"areas",
|
||||
"floors",
|
||||
];
|
||||
|
||||
const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
|
||||
dashboard: {
|
||||
"original-states": () =>
|
||||
@@ -90,47 +82,24 @@ type StrategyConfig<T extends LovelaceStrategyConfigType> = AsyncReturnType<
|
||||
Strategies[T]["generate"]
|
||||
>;
|
||||
|
||||
type StrategyTag =
|
||||
| { type: "builtin"; tag: string }
|
||||
| { type: "custom"; tag: string; legacyTag: string };
|
||||
|
||||
// Resolves the custom element tag(s) for a strategy. Custom strategies also
|
||||
// expose a legacy tag. `undefined` means the type is neither built-in nor a
|
||||
// custom strategy.
|
||||
const getStrategyTag = (
|
||||
configType: LovelaceStrategyConfigType,
|
||||
strategyType: string
|
||||
): StrategyTag | undefined => {
|
||||
if (strategyType in STRATEGIES[configType]) {
|
||||
return { type: "builtin", tag: `${strategyType}-${configType}-strategy` };
|
||||
}
|
||||
if (strategyType.startsWith(CUSTOM_PREFIX)) {
|
||||
const name = strategyType.slice(CUSTOM_PREFIX.length);
|
||||
return {
|
||||
type: "custom",
|
||||
tag: `ll-strategy-${configType}-${name}`,
|
||||
legacyTag: `ll-strategy-${name}`,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const getLovelaceStrategy = async <T extends LovelaceStrategyConfigType>(
|
||||
configType: T,
|
||||
strategyType: string
|
||||
): Promise<LovelaceStrategy> => {
|
||||
const tags = getStrategyTag(configType, strategyType);
|
||||
if (strategyType in STRATEGIES[configType]) {
|
||||
await STRATEGIES[configType][strategyType]();
|
||||
const tag = `${strategyType}-${configType}-strategy`;
|
||||
return customElements.get(tag) as unknown as Strategies[T];
|
||||
}
|
||||
|
||||
if (!tags) {
|
||||
if (!strategyType.startsWith(CUSTOM_PREFIX)) {
|
||||
throw new Error("Unknown strategy");
|
||||
}
|
||||
|
||||
if (tags.type === "builtin") {
|
||||
await STRATEGIES[configType][strategyType]();
|
||||
return customElements.get(tags.tag) as unknown as Strategies[T];
|
||||
}
|
||||
|
||||
const { tag, legacyTag } = tags;
|
||||
const legacyTag = `ll-strategy-${strategyType.slice(CUSTOM_PREFIX.length)}`;
|
||||
const tag = `ll-strategy-${configType}-${strategyType.slice(
|
||||
CUSTOM_PREFIX.length
|
||||
)}`;
|
||||
|
||||
if (
|
||||
(await Promise.race([
|
||||
@@ -272,41 +241,6 @@ export const generateLovelaceSectionStrategy = async (
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Synchronously checks whether a strategy needs regeneration.
|
||||
* Strategies can implement `shouldRegenerate` for custom logic or declare
|
||||
* `registryDependencies` to opt in to the default reference-equality check.
|
||||
* The default list (entities, devices, areas, floors) is used when neither is
|
||||
* provided, preserving the previous behavior for third-party strategies.
|
||||
*/
|
||||
export const checkStrategyShouldRegenerate = (
|
||||
configType: LovelaceStrategyConfigType,
|
||||
strategyConfig: LovelaceStrategyConfig,
|
||||
oldHass: HomeAssistant,
|
||||
newHass: HomeAssistant
|
||||
): boolean => {
|
||||
const strategyType = strategyConfig.type;
|
||||
if (!strategyType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const tags = getStrategyTag(configType, strategyType);
|
||||
const strategy = tags
|
||||
? ((customElements.get(tags.tag) ??
|
||||
(tags.type === "custom"
|
||||
? customElements.get(tags.legacyTag)
|
||||
: undefined)) as unknown as LovelaceStrategy | undefined)
|
||||
: undefined;
|
||||
|
||||
if (strategy?.shouldRegenerate) {
|
||||
return strategy.shouldRegenerate(strategyConfig, oldHass, newHass);
|
||||
}
|
||||
|
||||
const dependencies =
|
||||
strategy?.registryDependencies ?? DEFAULT_REGISTRY_DEPENDENCIES;
|
||||
return dependencies.some((key) => oldHass[key] !== newHass[key]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Find all references to strategies and replaces them with the generated output
|
||||
*/
|
||||
|
||||
@@ -17,7 +17,6 @@ import type {
|
||||
} from "../../cards/types";
|
||||
import type { ButtonHeadingBadgeConfig } from "../../heading-badges/types";
|
||||
import { computeAreaTileCardConfig } from "../areas/helpers/areas-strategy-helper";
|
||||
import type { LovelaceStrategyDependency } from "../types";
|
||||
import {
|
||||
getSummaryLabel,
|
||||
HOME_SUMMARIES,
|
||||
@@ -34,13 +33,6 @@ export interface HomeAreaViewStrategyConfig {
|
||||
|
||||
@customElement("home-area-view-strategy")
|
||||
export class HomeAreaViewStrategy extends ReactiveElement {
|
||||
static registryDependencies: readonly LovelaceStrategyDependency[] = [
|
||||
"entities",
|
||||
"devices",
|
||||
"areas",
|
||||
"panels",
|
||||
];
|
||||
|
||||
static async generate(
|
||||
config: HomeAreaViewStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
|
||||
@@ -4,10 +4,7 @@ import { customElement } from "lit/decorators";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import type { LovelaceViewRawConfig } from "../../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type {
|
||||
LovelaceStrategyEditor,
|
||||
LovelaceStrategyDependency,
|
||||
} from "../types";
|
||||
import type { LovelaceStrategyEditor } from "../types";
|
||||
import {
|
||||
getSummaryLabel,
|
||||
HOME_SUMMARIES_ICONS,
|
||||
@@ -28,10 +25,6 @@ export interface HomeDashboardStrategyConfig {
|
||||
|
||||
@customElement("home-dashboard-strategy")
|
||||
export class HomeDashboardStrategy extends ReactiveElement {
|
||||
static registryDependencies: readonly LovelaceStrategyDependency[] = [
|
||||
"areas",
|
||||
];
|
||||
|
||||
static async generate(
|
||||
config: HomeDashboardStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
|
||||
@@ -10,7 +10,6 @@ import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/
|
||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { MediaControlCardConfig } from "../../cards/types";
|
||||
import type { LovelaceStrategyDependency } from "../types";
|
||||
import { getAreasFloorHierarchy } from "../../../../common/areas/areas-floor-hierarchy";
|
||||
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
|
||||
|
||||
@@ -81,13 +80,6 @@ const processUnassignedEntities = (
|
||||
|
||||
@customElement("home-media-players-view-strategy")
|
||||
export class HomeMMediaPlayersViewStrategy extends ReactiveElement {
|
||||
static registryDependencies: readonly LovelaceStrategyDependency[] = [
|
||||
"entities",
|
||||
"devices",
|
||||
"areas",
|
||||
"floors",
|
||||
];
|
||||
|
||||
static async generate(
|
||||
_config: HomeMediaPlayersViewStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
|
||||
@@ -15,7 +15,6 @@ import type {
|
||||
EntitiesCardConfig,
|
||||
HeadingCardConfig,
|
||||
} from "../../cards/types";
|
||||
import type { LovelaceStrategyDependency } from "../types";
|
||||
import { OTHER_DEVICES_FILTERS } from "./helpers/other-devices-filters";
|
||||
|
||||
export interface HomeOtherDevicesViewStrategyConfig {
|
||||
@@ -25,13 +24,6 @@ export interface HomeOtherDevicesViewStrategyConfig {
|
||||
|
||||
@customElement("home-other-devices-view-strategy")
|
||||
export class HomeOtherDevicesViewStrategy extends ReactiveElement {
|
||||
static registryDependencies: readonly LovelaceStrategyDependency[] = [
|
||||
"entities",
|
||||
"devices",
|
||||
"areas",
|
||||
"floors",
|
||||
];
|
||||
|
||||
static async generate(
|
||||
config: HomeOtherDevicesViewStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
|
||||
@@ -35,7 +35,6 @@ import {
|
||||
LARGE_SCREEN_CONDITION,
|
||||
SMALL_SCREEN_CONDITION,
|
||||
} from "../helpers/view-columns-conditions";
|
||||
import type { LovelaceStrategyDependency } from "../types";
|
||||
import type { CommonControlsSectionStrategyConfig } from "../usage_prediction/common-controls-section-strategy";
|
||||
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
|
||||
import { OTHER_DEVICES_FILTERS } from "./helpers/other-devices-filters";
|
||||
@@ -80,14 +79,6 @@ const computeAreaCard = (
|
||||
|
||||
@customElement("home-overview-view-strategy")
|
||||
export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
static registryDependencies: readonly LovelaceStrategyDependency[] = [
|
||||
"entities",
|
||||
"devices",
|
||||
"areas",
|
||||
"floors",
|
||||
"panels",
|
||||
];
|
||||
|
||||
static async generate(
|
||||
config: HomeOverviewViewStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import type {
|
||||
LovelaceStrategyEditor,
|
||||
LovelaceStrategyDependency,
|
||||
} from "../types";
|
||||
import type { LovelaceStrategyEditor } from "../types";
|
||||
import type { IframeViewStrategyConfig } from "./iframe-view-strategy";
|
||||
|
||||
export type IframeDashboardStrategyConfig = IframeViewStrategyConfig;
|
||||
|
||||
@customElement("iframe-dashboard-strategy")
|
||||
export class IframeDashboardStrategy extends ReactiveElement {
|
||||
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
|
||||
|
||||
static async generate(
|
||||
config: IframeDashboardStrategyConfig
|
||||
): Promise<LovelaceConfig> {
|
||||
|
||||
@@ -2,7 +2,6 @@ import { ReactiveElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import type { IframeCardConfig } from "../../cards/types";
|
||||
import type { LovelaceStrategyDependency } from "../types";
|
||||
|
||||
export interface IframeViewStrategyConfig {
|
||||
type: "iframe";
|
||||
@@ -12,8 +11,6 @@ export interface IframeViewStrategyConfig {
|
||||
|
||||
@customElement("iframe-view-strategy")
|
||||
export class IframeViewStrategy extends ReactiveElement {
|
||||
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
|
||||
|
||||
static async generate(
|
||||
config: IframeViewStrategyConfig
|
||||
): Promise<LovelaceViewConfig> {
|
||||
|
||||
@@ -3,15 +3,12 @@ import { customElement } from "lit/decorators";
|
||||
import type { LovelaceDashboardSuggestions } from "../../../../data/lovelace/dashboard";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { LovelaceStrategyDependency } from "../types";
|
||||
import type { MapViewStrategyConfig } from "./map-view-strategy";
|
||||
|
||||
export type MapDashboardStrategyConfig = MapViewStrategyConfig;
|
||||
|
||||
@customElement("map-dashboard-strategy")
|
||||
export class MapDashboardStrategy extends ReactiveElement {
|
||||
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
|
||||
|
||||
static async generate(
|
||||
config: MapDashboardStrategyConfig
|
||||
): Promise<LovelaceConfig> {
|
||||
|
||||
@@ -3,7 +3,6 @@ import { customElement } from "lit/decorators";
|
||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { MapCardConfig } from "../../cards/types";
|
||||
import type { LovelaceStrategyDependency } from "../types";
|
||||
|
||||
export interface MapViewStrategyConfig {
|
||||
type: "map";
|
||||
@@ -11,8 +10,6 @@ export interface MapViewStrategyConfig {
|
||||
|
||||
@customElement("map-view-strategy")
|
||||
export class MapViewStrategy extends ReactiveElement {
|
||||
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
|
||||
|
||||
static async generate(
|
||||
_config: MapViewStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
|
||||
+1
-6
@@ -1,10 +1,7 @@
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
|
||||
import type {
|
||||
LovelaceStrategyEditor,
|
||||
LovelaceStrategyDependency,
|
||||
} from "../types";
|
||||
import type { LovelaceStrategyEditor } from "../types";
|
||||
import type { OriginalStatesViewStrategyConfig } from "./original-states-view-strategy";
|
||||
|
||||
export type OriginalStatesDashboardStrategyConfig =
|
||||
@@ -12,8 +9,6 @@ export type OriginalStatesDashboardStrategyConfig =
|
||||
|
||||
@customElement("original-states-dashboard-strategy")
|
||||
export class OriginalStatesDashboardStrategy extends ReactiveElement {
|
||||
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
|
||||
|
||||
static async generate(
|
||||
config: OriginalStatesDashboardStrategyConfig
|
||||
): Promise<LovelaceConfig> {
|
||||
|
||||
@@ -9,7 +9,6 @@ import type { HomeAssistant } from "../../../../types";
|
||||
import type { EmptyStateCardConfig } from "../../cards/types";
|
||||
import { generateDefaultViewConfig } from "../../common/generate-lovelace-config";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import type { LovelaceStrategyDependency } from "../types";
|
||||
|
||||
export interface OriginalStatesViewStrategyConfig {
|
||||
type: "original-states";
|
||||
@@ -20,13 +19,6 @@ export interface OriginalStatesViewStrategyConfig {
|
||||
|
||||
@customElement("original-states-view-strategy")
|
||||
export class OriginalStatesViewStrategy extends ReactiveElement {
|
||||
static registryDependencies: readonly LovelaceStrategyDependency[] = [
|
||||
"entities",
|
||||
"devices",
|
||||
"areas",
|
||||
"floors",
|
||||
];
|
||||
|
||||
static async generate(
|
||||
config: OriginalStatesViewStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
|
||||
@@ -6,22 +6,8 @@ import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceGenericElementEditor } from "../types";
|
||||
|
||||
export type LovelaceStrategyDependency =
|
||||
| "entities"
|
||||
| "devices"
|
||||
| "areas"
|
||||
| "floors"
|
||||
| "labels"
|
||||
| "panels";
|
||||
|
||||
export interface LovelaceStrategy<T = any> {
|
||||
generate(config: LovelaceStrategyConfig, hass: HomeAssistant): Promise<T>;
|
||||
shouldRegenerate?(
|
||||
config: LovelaceStrategyConfig,
|
||||
oldHass: HomeAssistant,
|
||||
newHass: HomeAssistant
|
||||
): boolean;
|
||||
registryDependencies?: readonly LovelaceStrategyDependency[];
|
||||
getConfigElement?: () => LovelaceStrategyEditor;
|
||||
noEditor?: boolean;
|
||||
configRequired?: boolean;
|
||||
|
||||
@@ -6,7 +6,6 @@ import { getCommonControlsUsagePrediction } from "../../../../data/usage_predict
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { HeadingCardConfig, TileCardConfig } from "../../cards/types";
|
||||
import type { Condition } from "../../common/validate-condition";
|
||||
import type { LovelaceStrategyDependency } from "../types";
|
||||
|
||||
const DEFAULT_LIMIT = 8;
|
||||
|
||||
@@ -34,8 +33,6 @@ const toTileCard = (entity: string): TileCardConfig => ({
|
||||
|
||||
@customElement("common-controls-section-strategy")
|
||||
export class CommonControlsSectionStrategy extends ReactiveElement {
|
||||
static registryDependencies: readonly LovelaceStrategyDependency[] = [];
|
||||
|
||||
static async generate(
|
||||
config: CommonControlsSectionStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
|
||||
@@ -42,10 +42,7 @@ import { parseLovelaceCardPath } from "../editor/lovelace-path";
|
||||
import { createErrorSectionConfig } from "../sections/hui-error-section";
|
||||
import "../sections/hui-section";
|
||||
import type { HuiSection } from "../sections/hui-section";
|
||||
import {
|
||||
checkStrategyShouldRegenerate,
|
||||
generateLovelaceViewStrategy,
|
||||
} from "../strategies/get-strategy";
|
||||
import { generateLovelaceViewStrategy } from "../strategies/get-strategy";
|
||||
import type { Lovelace } from "../types";
|
||||
import { getViewType } from "./get-view-type";
|
||||
|
||||
@@ -188,13 +185,10 @@ export class HUIView extends ReactiveElement {
|
||||
if (oldHass && this.hass && this.lovelace && isStrategyView(viewConfig)) {
|
||||
if (
|
||||
this.hass.config.state === "RUNNING" &&
|
||||
(oldHass.config.state !== "RUNNING" ||
|
||||
checkStrategyShouldRegenerate(
|
||||
"view",
|
||||
viewConfig.strategy,
|
||||
oldHass,
|
||||
this.hass
|
||||
))
|
||||
(oldHass.entities !== this.hass.entities ||
|
||||
oldHass.devices !== this.hass.devices ||
|
||||
oldHass.areas !== this.hass.areas ||
|
||||
oldHass.floors !== this.hass.floors)
|
||||
) {
|
||||
this._debounceRefreshConfig();
|
||||
}
|
||||
|
||||
@@ -163,6 +163,9 @@ export const getMyRedirects = (): Redirects => ({
|
||||
entities: {
|
||||
redirect: "/config/entities",
|
||||
},
|
||||
categories: {
|
||||
redirect: "/config/categories",
|
||||
},
|
||||
labels: {
|
||||
redirect: "/config/labels",
|
||||
},
|
||||
|
||||
@@ -98,7 +98,6 @@ class HaProfileSectionGeneral extends LitElement {
|
||||
<hass-tabs-subpage
|
||||
main-page
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.tabs=${profileSections}
|
||||
.route=${this.route}
|
||||
>
|
||||
|
||||
@@ -15,8 +15,6 @@ import "./ha-refresh-tokens-card";
|
||||
class HaProfileSectionSecurity extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _refreshTokens?: RefreshToken[];
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
@@ -37,7 +35,6 @@ class HaProfileSectionSecurity extends LitElement {
|
||||
<hass-tabs-subpage
|
||||
main-page
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.tabs=${profileSections}
|
||||
.route=${this.route}
|
||||
>
|
||||
|
||||
@@ -39,6 +39,7 @@ import { subscribeLabelRegistry } from "../data/label/label_registry";
|
||||
import type { Constructor, HomeAssistant } from "../types";
|
||||
import type { HassBaseEl } from "./hass-base-mixin";
|
||||
import { LazyContextProvider } from "./lazy-context-provider";
|
||||
import { RelatedContextProvider } from "./related-context-provider";
|
||||
|
||||
export const contextMixin = <T extends Constructor<HassBaseEl>>(
|
||||
superClass: T
|
||||
@@ -201,6 +202,8 @@ export const contextMixin = <T extends Constructor<HassBaseEl>>(
|
||||
}),
|
||||
};
|
||||
|
||||
private __relatedContextProvider = new RelatedContextProvider(this);
|
||||
|
||||
protected hassConnected() {
|
||||
super.hassConnected();
|
||||
for (const [key, value] of Object.entries(this.hass!)) {
|
||||
@@ -214,6 +217,8 @@ export const contextMixin = <T extends Constructor<HassBaseEl>>(
|
||||
for (const provider of Object.values(this.__lazyContextProviders)) {
|
||||
provider.setConnection(connection);
|
||||
}
|
||||
|
||||
this.__relatedContextProvider.connect();
|
||||
}
|
||||
|
||||
protected _updateHass(obj: Partial<HomeAssistant>) {
|
||||
@@ -244,5 +249,6 @@ export const contextMixin = <T extends Constructor<HassBaseEl>>(
|
||||
for (const provider of Object.values(this.__lazyContextProviders)) {
|
||||
provider.unsubscribe();
|
||||
}
|
||||
this.__relatedContextProvider.disconnect();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,12 +5,7 @@ import { canOverrideAlphanumericInput } from "../common/dom/can-override-input";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import { ShortcutManager } from "../common/keyboard/shortcuts";
|
||||
import { extractSearchParamsObject } from "../common/url/search-params";
|
||||
import { findRelated, type RelatedResult } from "../data/search";
|
||||
import type {
|
||||
QuickBarContextItem,
|
||||
QuickBarParams,
|
||||
QuickBarSection,
|
||||
} from "../dialogs/quick-bar/show-dialog-quick-bar";
|
||||
import type { QuickBarSection } from "../dialogs/quick-bar/show-dialog-quick-bar";
|
||||
import {
|
||||
closeQuickBar,
|
||||
showQuickBar,
|
||||
@@ -24,10 +19,8 @@ import type { HassElement } from "./hass-element";
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"hass-quick-bar": QuickBarParams;
|
||||
"hass-quick-bar-trigger": KeyboardEvent;
|
||||
"hass-enable-shortcuts": HomeAssistant["enableShortcuts"];
|
||||
"hass-quick-bar-context": QuickBarContextItem | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,52 +28,6 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
|
||||
class extends superClass {
|
||||
private _quickBarOpen = false;
|
||||
|
||||
private _quickBarContext?: QuickBarContextItem;
|
||||
|
||||
private _quickBarContextRelated?: RelatedResult;
|
||||
|
||||
private _fetchRelatedMemoized = memoizeOne(
|
||||
(itemType: QuickBarContextItem["itemType"], itemId: string) =>
|
||||
findRelated(this.hass!, itemType, itemId)
|
||||
);
|
||||
|
||||
private _clearQuickBarContext = () => {
|
||||
this._quickBarContext = undefined;
|
||||
this._quickBarContextRelated = undefined;
|
||||
};
|
||||
|
||||
private _contextMatches = (context?: QuickBarContextItem) =>
|
||||
context?.itemType === this._quickBarContext?.itemType &&
|
||||
context?.itemId === this._quickBarContext?.itemId;
|
||||
|
||||
private _prefetchQuickBarContext = async (
|
||||
context?: QuickBarContextItem
|
||||
) => {
|
||||
this._quickBarContextRelated = undefined;
|
||||
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const related = await this._fetchRelatedMemoized(
|
||||
context.itemType,
|
||||
context.itemId
|
||||
);
|
||||
|
||||
if (this._contextMatches(context)) {
|
||||
this._quickBarContextRelated = related;
|
||||
}
|
||||
} catch (err) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("Error prefetching quick bar related items", err);
|
||||
|
||||
if (this._contextMatches(context)) {
|
||||
this._quickBarContextRelated = undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>) {
|
||||
super.firstUpdated(changedProps);
|
||||
|
||||
@@ -111,20 +58,6 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
|
||||
}
|
||||
});
|
||||
|
||||
this.addEventListener("hass-quick-bar-context", (ev) => {
|
||||
this._quickBarContext =
|
||||
ev.detail && "itemType" in ev.detail && "itemId" in ev.detail
|
||||
? ev.detail
|
||||
: undefined;
|
||||
this._prefetchQuickBarContext(this._quickBarContext);
|
||||
});
|
||||
|
||||
mainWindow.addEventListener(
|
||||
"location-changed",
|
||||
this._clearQuickBarContext
|
||||
);
|
||||
mainWindow.addEventListener("popstate", this._clearQuickBarContext);
|
||||
|
||||
mainWindow.addEventListener("hass-quick-bar-trigger", (ev) => {
|
||||
switch (ev.detail.key) {
|
||||
case "e":
|
||||
@@ -161,15 +94,6 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
mainWindow.removeEventListener(
|
||||
"location-changed",
|
||||
this._clearQuickBarContext
|
||||
);
|
||||
mainWindow.removeEventListener("popstate", this._clearQuickBarContext);
|
||||
}
|
||||
|
||||
private _registerShortcut() {
|
||||
const shortcutManager = new ShortcutManager();
|
||||
shortcutManager.add({
|
||||
@@ -238,11 +162,7 @@ export default <T extends Constructor<HassElement>>(superClass: T) =>
|
||||
}
|
||||
e.preventDefault();
|
||||
|
||||
showQuickBar(this, {
|
||||
mode,
|
||||
contextItem: this._quickBarContext,
|
||||
related: this._quickBarContextRelated,
|
||||
});
|
||||
showQuickBar(this, { mode });
|
||||
}
|
||||
|
||||
private _toggleQuickBar(e: KeyboardEvent, mode?: QuickBarSection) {
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
import { ContextProvider } from "@lit/context";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { HASSDomEvent } from "../common/dom/fire_event";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import { buildRelatedIdSets } from "../common/search/related-context";
|
||||
import { relatedContext, type RelatedContextItem } from "../data/context";
|
||||
import { findRelated } from "../data/search";
|
||||
import type { HassBaseEl } from "./hass-base-mixin";
|
||||
|
||||
/**
|
||||
* Standalone context provider for `relatedContext`.
|
||||
*
|
||||
* Listens for `hass-related-context` events fired by child components,
|
||||
* resolves the related entities/devices/areas via `findRelated`, and
|
||||
* provides the resolved `RelatedIdSets` to context consumers.
|
||||
*
|
||||
* Clears on actual page navigation (pathname change), not on dialog
|
||||
* history manipulation (`popstate` from dialog close).
|
||||
*
|
||||
* Instantiated from `context-mixin.ts` alongside other providers.
|
||||
*/
|
||||
export class RelatedContextProvider {
|
||||
private _relatedContext?: RelatedContextItem;
|
||||
|
||||
private _provider: ContextProvider<typeof relatedContext>;
|
||||
|
||||
private _contextPathname?: string;
|
||||
|
||||
private _fetchRelatedMemoized = memoizeOne(
|
||||
(itemType: RelatedContextItem["itemType"], itemId: string) =>
|
||||
findRelated(this._host.hass!, itemType, itemId)
|
||||
);
|
||||
|
||||
constructor(private _host: HassBaseEl) {
|
||||
this._provider = new ContextProvider(_host, { context: relatedContext });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up event listeners. Call from `firstUpdated` or `hassConnected`.
|
||||
*/
|
||||
public connect(): void {
|
||||
this._host.addEventListener("hass-related-context", this._onRelatedContext);
|
||||
mainWindow.addEventListener(
|
||||
"location-changed",
|
||||
this._maybeClearRelatedContext
|
||||
);
|
||||
mainWindow.addEventListener("popstate", this._maybeClearRelatedContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up event listeners. Call from `disconnectedCallback`.
|
||||
*/
|
||||
public disconnect(): void {
|
||||
this._host.removeEventListener(
|
||||
"hass-related-context",
|
||||
this._onRelatedContext
|
||||
);
|
||||
mainWindow.removeEventListener(
|
||||
"location-changed",
|
||||
this._maybeClearRelatedContext
|
||||
);
|
||||
mainWindow.removeEventListener("popstate", this._maybeClearRelatedContext);
|
||||
}
|
||||
|
||||
private _onRelatedContext = (
|
||||
ev: HASSDomEvent<RelatedContextItem | undefined>
|
||||
): void => {
|
||||
this._relatedContext = ev.detail;
|
||||
this._contextPathname = mainWindow.location.pathname;
|
||||
this._resolveRelatedContext(this._relatedContext);
|
||||
};
|
||||
|
||||
/**
|
||||
* Only clear context when the actual page pathname changes.
|
||||
* Dialog open/close manipulates history state without changing the URL,
|
||||
* so we ignore those popstate/location-changed events.
|
||||
*/
|
||||
private _maybeClearRelatedContext = (): void => {
|
||||
if (
|
||||
this._contextPathname &&
|
||||
mainWindow.location.pathname === this._contextPathname
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._relatedContext = undefined;
|
||||
this._contextPathname = undefined;
|
||||
this._provider.setValue(undefined);
|
||||
};
|
||||
|
||||
private _contextMatches = (context?: RelatedContextItem): boolean =>
|
||||
context?.itemType === this._relatedContext?.itemType &&
|
||||
context?.itemId === this._relatedContext?.itemId;
|
||||
|
||||
private _resolveRelatedContext = async (
|
||||
context?: RelatedContextItem
|
||||
): Promise<void> => {
|
||||
if (!context || !this._host.hass) {
|
||||
this._provider.setValue(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const related = await this._fetchRelatedMemoized(
|
||||
context.itemType,
|
||||
context.itemId
|
||||
);
|
||||
if (this._contextMatches(context)) {
|
||||
this._provider.setValue(buildRelatedIdSets(related));
|
||||
}
|
||||
} catch (_err) {
|
||||
if (this._contextMatches(context)) {
|
||||
this._provider.setValue(undefined);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
/* eslint-disable no-console */
|
||||
import type { ReactiveElement, PropertyValues } from "lit";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import { mainWindow } from "../common/dom/get_main_window";
|
||||
import { closeLastDialog } from "../dialogs/make-dialog-manager";
|
||||
import type { ProvideHassElement } from "../mixins/provide-hass-lit-mixin";
|
||||
@@ -38,6 +39,21 @@ export const urlSyncMixin = <
|
||||
protected firstUpdated(changedProperties: PropertyValues<this>): void {
|
||||
super.firstUpdated(changedProperties);
|
||||
if (mainWindow.history.state?.dialog) {
|
||||
const refreshUrl = mainWindow.history.state.refreshUrl;
|
||||
if (typeof refreshUrl === "string") {
|
||||
// Page was refreshed while a dialog had stashed an intended
|
||||
// destination URL. Clean up the stale dialog state and route
|
||||
// to the intended URL. We bypass navigate() because its
|
||||
// ensureDialogsClosed loop would spin until timeout on the
|
||||
// dangling state.dialog with no actual dialog open.
|
||||
mainWindow.history.replaceState(null, "", refreshUrl);
|
||||
// Defer: the host element's firstUpdated registers the
|
||||
// location-changed listener after super.firstUpdated() returns.
|
||||
setTimeout(() => {
|
||||
fireEvent(mainWindow, "location-changed", { replace: true });
|
||||
});
|
||||
return;
|
||||
}
|
||||
// this is a page refresh with a dialog open
|
||||
// the dialog stack must be empty in this case so this state should be cleaned up
|
||||
mainWindow.history.back();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user