Compare commits

..

1 Commits

Author SHA1 Message Date
Paulus Schoutsen 854e56947f Update IFRAME_SANDBOX to remove 'allow-same-origin'
Removed 'allow-same-origin' from the IFRAME_SANDBOX settings.
2026-05-18 12:12:21 -04:00
242 changed files with 4023 additions and 13056 deletions
+1
View File
@@ -1,2 +1,3 @@
[build.environment]
YARN_VERSION = "1.22.11"
NODE_OPTIONS = "--max_old_space_size=6144"
+6 -5
View File
@@ -62,6 +62,7 @@
"@lit-labs/virtualizer": "2.1.1",
"@lit/context": "1.1.6",
"@lit/reactive-element": "2.1.2",
"@material/data-table": "=14.0.0-canary.53b3cad2f.0",
"@material/mwc-base": "0.27.0",
"@material/mwc-formfield": "patch:@material/mwc-formfield@npm%3A0.27.0#~/.yarn/patches/@material-mwc-formfield-npm-0.27.0-9528cb60f6.patch",
"@material/mwc-list": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch",
@@ -74,8 +75,8 @@
"@replit/codemirror-indentation-markers": "6.5.3",
"@swc/helpers": "0.5.21",
"@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "4.0.2",
"@tsparticles/preset-links": "4.0.2",
"@tsparticles/engine": "4.0.0",
"@tsparticles/preset-links": "4.0.0",
"@vibrant/color": "4.0.4",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
"@webcomponents/webcomponentsjs": "2.8.0",
@@ -86,7 +87,7 @@
"core-js": "3.49.0",
"cropperjs": "1.6.2",
"culori": "4.0.2",
"date-fns": "4.2.0",
"date-fns": "4.1.0",
"deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1",
"dialog-polyfill": "0.5.6",
@@ -165,7 +166,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.3.0",
"eslint-config-prettier": "10.1.8",
"eslint-import-resolver-webpack": "0.13.11",
"eslint-plugin-import-x": "4.16.2",
@@ -187,7 +188,7 @@
"jsdom": "29.1.1",
"jszip": "3.10.1",
"license-checker-rseidelsohn": "4.4.2",
"lint-staged": "17.0.5",
"lint-staged": "17.0.4",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.18.1",
-39
View File
@@ -18,46 +18,7 @@
"enabled": true,
"schedule": ["on the 19th day of the month before 4am"]
},
"customDatasources": {
"ha-core-python": {
"defaultRegistryUrlTemplate": "https://raw.githubusercontent.com/home-assistant/core/dev/.python-version",
"format": "plain"
}
},
"customManagers": [
{
"description": "Keep PYTHON_VERSION in sync with home-assistant/core (patch + minor)",
"customType": "regex",
"managerFilePatterns": ["/^\\.github/workflows/[^/]+\\.ya?ml$/"],
"matchStrings": ["PYTHON_VERSION: \"(?<currentValue>[^\"]+)\""],
"depNameTemplate": "python",
"datasourceTemplate": "custom.ha-core-python",
"versioningTemplate": "python"
},
{
"description": "Keep devcontainer image and requires-python in sync with home-assistant/core (minor only)",
"customType": "regex",
"managerFilePatterns": [
"/^\\.devcontainer/Dockerfile$/",
"/^pyproject\\.toml$/"
],
"matchStrings": [
"devcontainers/python:(?<currentValue>[\\d.]+)",
"requires-python = \">=(?<currentValue>[^\"]+)\""
],
"depNameTemplate": "python",
"datasourceTemplate": "custom.ha-core-python",
"versioningTemplate": "python",
"extractVersionTemplate": "^(?<version>\\d+\\.\\d+)"
}
],
"packageRules": [
{
"description": "Group all Python version updates from home-assistant/core",
"matchDepNames": ["python"],
"matchDatasources": ["custom.ha-core-python"],
"groupName": "Python version"
},
{
"description": "MDC packages are pinned to the same version as MWC",
"extends": ["monorepo:material-components-web"],
+1 -1
View File
@@ -3,7 +3,7 @@
* @param arr - The array to get combinations of
* @returns A multidimensional array of all possible combinations
*/
export function getAllCombinations<T>(arr: readonly T[]): T[][] {
export function getAllCombinations<T>(arr: T[]) {
return arr.reduce<T[][]>(
(combinations, element) =>
combinations.concat(
-9
View File
@@ -114,15 +114,6 @@ export const DOMAINS_WITH_DYNAMIC_PICTURE = new Set([
export const UNIT_C = "°C";
export const UNIT_F = "°F";
/** Length units. */
export const UNIT_IN = "in";
export const UNIT_KM = "km";
export const UNIT_MM = "mm";
/** Pressure units. */
export const UNIT_HPA = "hPa";
export const UNIT_INHG = "inHg";
/** Entity ID of the default view. */
export const DEFAULT_VIEW_ENTITY_ID = "group.default_view";
@@ -1,5 +1,5 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
import { isUnavailableState } from "../../data/entity/entity";
import type { HomeAssistant } from "../../types";
interface EntityUnitStubConfig {
@@ -21,24 +21,32 @@ export const computeEntityUnitDisplay = (
stateObj: HassEntity | undefined,
config: EntityUnitStubConfig
): string => {
let unit;
if (
!stateObj ||
stateObj.state === UNAVAILABLE ||
stateObj.state === UNKNOWN ||
(!config.attribute && stateObj.attributes.device_class === "duration")
stateObj &&
!isUnavailableState(stateObj.state) &&
(config.attribute || stateObj.attributes.device_class !== "duration")
) {
return "";
// check for an explicitly defined unit in config
unit = config.unit;
if (!unit) {
if (!config.attribute) {
// use entity's unit_of_measurement
const stateParts = hass.formatEntityStateToParts(stateObj);
unit = stateParts.find((part) => part.type === "unit")?.value;
} else {
// use attribute's unit if available
const attrParts = hass.formatEntityAttributeValueToParts(
stateObj,
config.attribute
);
unit = attrParts.find((part) => part.type === "unit")?.value;
}
}
return unit ?? "";
}
// check for an explicitly defined unit in config
if (config.unit) {
return config.unit;
}
// otherwise derive from the entity's state or attribute
const parts = config.attribute
? hass.formatEntityAttributeValueToParts(stateObj, config.attribute)
: hass.formatEntityStateToParts(stateObj);
return parts.find((part) => part.type === "unit")?.value ?? "";
return "";
};
+2 -2
View File
@@ -1,5 +1,5 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
import { UNAVAILABLE_STATES } from "../../data/entity/entity";
import type { HomeAssistant } from "../../types";
import { stringCompare } from "../string/compare";
import { computeDomain } from "./compute_domain";
@@ -253,7 +253,7 @@ export const getStatesDomain = (
if (!attribute) {
// All entities can have unavailable states
result.push(UNAVAILABLE, UNKNOWN);
result.push(...UNAVAILABLE_STATES);
}
if (!attribute && domain in FIXED_DOMAIN_STATES) {
+5 -11
View File
@@ -1,5 +1,5 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
import { isUnavailableState, UNAVAILABLE } from "../../data/entity/entity";
import type { HomeAssistant } from "../../types";
import { computeStateDomain } from "./compute_state_domain";
@@ -8,18 +8,12 @@ export const computeGroupEntitiesState = (states: HassEntity[]): string => {
return UNAVAILABLE;
}
const allUnavailable = states.every(
(stateObj) => stateObj.state === UNAVAILABLE
const validState = states.some(
(stateObj) => !isUnavailableState(stateObj.state)
);
if (allUnavailable) {
return UNAVAILABLE;
}
const hasValidState = states.some(
(stateObj) => stateObj.state !== UNAVAILABLE && stateObj.state !== UNKNOWN
);
if (!hasValidState) {
return UNKNOWN;
if (!validState) {
return UNAVAILABLE;
}
// Use the first state to determine the domain
+2 -2
View File
@@ -1,5 +1,5 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { OFF, UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
import { isUnavailableState, OFF, UNAVAILABLE } from "../../data/entity/entity";
import { computeDomain } from "./compute_domain";
export function stateActive(stateObj: HassEntity, state?: string): boolean {
@@ -19,7 +19,7 @@ export function stateActive(stateObj: HassEntity, state?: string): boolean {
return compareState !== UNAVAILABLE;
}
if (compareState === UNAVAILABLE || compareState === UNKNOWN) {
if (isUnavailableState(compareState)) {
return false;
}
@@ -1,17 +0,0 @@
/**
* @summary Truncates a string to `maxLength`, appending `ellipsis` only when it actually shortens the result.
* @param text The input string.
* @param maxLength Maximum length of the prefix kept before the ellipsis.
* @param ellipsis Suffix appended when truncation occurs.
* @returns `text` unchanged when its length is `<= maxLength + ellipsis.length`, otherwise `text.substring(0, maxLength) + ellipsis`.
*/
export const truncateWithEllipsis = (
text: string,
maxLength: number,
ellipsis = "..."
): string => {
if (text.length <= maxLength + ellipsis.length) {
return text;
}
return `${text.substring(0, maxLength)}${ellipsis}`;
};
@@ -1,5 +1,6 @@
import { LitElement, css, html } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "../ha-tooltip";
export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
@@ -8,10 +9,11 @@ export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
*
* @summary
* Small status indicator dot used in automation/condition rows to surface the
* live evaluation result.
* live evaluation result. Renders an optional tooltip with details on hover.
*
* @attr {"pass"|"fail"|"invalid"|"unknown"} state - The current live-test state. Defaults to `unknown`.
* @attr {string} label - Accessible label announced by assistive technology.
* @attr {string} message - Optional tooltip body shown on hover/focus.
*/
@customElement("ha-automation-row-live-test")
export class HaAutomationRowLiveTest extends LitElement {
@@ -19,6 +21,8 @@ export class HaAutomationRowLiveTest extends LitElement {
@property() public label = "";
@property() public message?: string;
protected render() {
return html`
<div
@@ -27,6 +31,9 @@ export class HaAutomationRowLiveTest extends LitElement {
tabindex="0"
aria-label=${this.label}
></div>
${this.message
? html`<ha-tooltip for="indicator">${this.message}</ha-tooltip>`
: nothing}
`;
}
@@ -49,15 +56,31 @@ export class HaAutomationRowLiveTest extends LitElement {
background-color: var(--ha-color-fill-success-loud-resting);
border-color: var(--ha-color-fill-success-loud-resting);
}
:host([state="pass"]) #indicator:hover {
background-color: var(--ha-color-fill-success-loud-hover);
border-color: var(--ha-color-fill-success-loud-hover);
}
:host([state="fail"]) #indicator {
border-color: var(--ha-color-fill-warning-loud-resting);
}
:host([state="fail"]) #indicator:hover {
background-color: var(--ha-color-fill-warning-loud-hover);
border-color: var(--ha-color-fill-warning-loud-hover);
}
:host([state="invalid"]) #indicator {
border-color: var(--ha-color-fill-danger-loud-resting);
}
:host([state="invalid"]) #indicator:hover {
background-color: var(--ha-color-fill-danger-loud-hover);
border-color: var(--ha-color-fill-danger-loud-hover);
}
:host([state="unknown"]) #indicator {
border-color: var(--ha-color-fill-neutral-loud-resting);
}
:host([state="unknown"]) #indicator:hover {
background-color: var(--ha-color-fill-neutral-loud-hover);
border-color: var(--ha-color-fill-neutral-loud-hover);
}
`;
}
@@ -187,6 +187,7 @@ export class HaAutomationRow extends LitElement {
flex: 1;
min-width: 0;
overflow-wrap: anywhere;
margin: 0 var(--ha-space-3);
}
::slotted([slot="header"]) {
overflow-wrap: anywhere;
+1 -1
View File
@@ -116,7 +116,7 @@ export class HaProgressButton extends LitElement {
visibility: hidden;
}
:host([appearance="brand"]) ha-svg-icon {
ha-svg-icon {
color: var(--white-color);
}
`;
@@ -1,12 +1,10 @@
import { consume } from "@lit/context";
import type { HassEntities } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import type { LocalizeFunc } from "../../common/translations/localize";
import { fullEntitiesContext } from "../../data/context";
import type { DeviceAutomation } from "../../data/device/device_automation";
import {
@@ -14,7 +12,7 @@ import {
sortDeviceAutomations,
} from "../../data/device/device_automation";
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
import type { CallWS, HomeAssistant, ValueChangedEvent } from "../../types";
import type { HomeAssistant, ValueChangedEvent } from "../../types";
import "../ha-generic-picker";
import type { PickerValueRenderer } from "../ha-picker-field";
@@ -48,14 +46,13 @@ export abstract class HaDeviceAutomationPicker<
}
private _localizeDeviceAutomation: (
localize: LocalizeFunc,
states: HassEntities,
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
automation: T
) => string;
private _fetchDeviceAutomations: (
callWS: CallWS,
hass: HomeAssistant,
deviceId: string
) => Promise<T[]>;
@@ -130,8 +127,7 @@ export abstract class HaDeviceAutomationPicker<
const automationListItems = automations.map((automation, idx) => {
const primary = this._localizeDeviceAutomation(
this.hass.localize,
this.hass.states,
this.hass,
this._entityReg,
automation
);
@@ -166,12 +162,7 @@ export abstract class HaDeviceAutomationPicker<
);
const text = automation
? this._localizeDeviceAutomation(
this.hass.localize,
this.hass.states,
this._entityReg,
automation
)
? this._localizeDeviceAutomation(this.hass, this._entityReg, automation)
: value === NO_AUTOMATION_KEY
? this.NO_AUTOMATION_TEXT
: value;
@@ -181,9 +172,9 @@ export abstract class HaDeviceAutomationPicker<
private async _updateDeviceInfo() {
this._automations = this.deviceId
? (
await this._fetchDeviceAutomations(this.hass.callWS, this.deviceId)
).sort(sortDeviceAutomations)
? (await this._fetchDeviceAutomations(this.hass, this.deviceId)).sort(
sortDeviceAutomations
)
: // No device, clear the list of automations
[];
+99 -135
View File
@@ -1,9 +1,9 @@
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { mdiPlus, mdiShape } from "@mdi/js";
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
import { html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
import { fireEvent } from "../../common/dom/fire_event";
import { computeEntityPickerDisplay } from "../../common/entity/compute_entity_name_display";
import { isValidEntityId } from "../../common/entity/valid_entity_id";
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity/entity";
@@ -20,20 +20,17 @@ import {
} from "../../panels/config/helpers/const";
import { showHelperDetailDialog } from "../../panels/config/helpers/show-dialog-helper-detail";
import type { HomeAssistant } from "../../types";
import "../ha-button";
import "../ha-combo-box-item";
import "../ha-generic-picker";
import "../ha-icon";
import "../ha-picker-field";
import type { HaGenericPicker } from "../ha-generic-picker";
import type { PickerComboBoxSearchFn } from "../ha-picker-combo-box";
import type { PickerValueRenderer } from "../ha-picker-field";
import "../ha-picker-popover";
import "../ha-picker-search-list";
import type {
HaPickerSearchList,
PickerSearchFn,
} from "../ha-picker-search-list";
import "../ha-svg-icon";
import "./state-badge";
const CREATE_ID = "___create-new-entity___";
@customElement("ha-entity-picker")
export class HaEntityPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -125,17 +122,15 @@ export class HaEntityPicker extends LitElement {
@property({ attribute: "hide-clear-icon", type: Boolean })
public hideClearIcon = false;
@query(".trigger") private _trigger?: HTMLElement;
@property({ attribute: "add-button", type: Boolean })
public addButton = false;
@query("ha-picker-search-list") private _searchList?: HaPickerSearchList;
@property({ attribute: "add-button-label" }) public addButtonLabel?: string;
@state() private _pickerOpen = false;
@query("ha-generic-picker") private _picker?: HaGenericPicker;
@state() private _pendingEntityId?: string;
// Commit fires on @closed (after the hide animation) to avoid flicker.
private _pendingValue?: string;
protected willUpdate(changedProperties: PropertyValues<this>) {
if (
this._pendingEntityId &&
@@ -150,7 +145,7 @@ export class HaEntityPicker extends LitElement {
protected firstUpdated(changedProperties: PropertyValues<this>): void {
super.firstUpdated(changedProperties);
// Preload title translations so they're ready when the dropdown opens.
// Load title translations so it is available when the combo-box opens
this.hass.loadBackendTranslation("title");
}
@@ -280,48 +275,40 @@ export class HaEntityPicker extends LitElement {
`;
};
private _getCreateActions = memoizeOne(
private _getAdditionalItems = () =>
this._getCreateItems(this.hass.localize, this.createDomains);
private _getCreateItems = memoizeOne(
(
localize: this["hass"]["localize"],
createDomains: this["createDomains"]
): EntityComboBoxItem[] => {
) => {
if (!createDomains?.length) {
return [];
}
this.hass.loadFragmentTranslation("config");
return createDomains.map((domain) => ({
id: `__create-helper__${domain}`,
primary: localize("ui.components.entity.entity-picker.create_helper", {
domain: isHelperDomain(domain)
? localize(
`ui.panel.config.helpers.types.${domain as HelperDomain}`
) || domain
: domainToName(localize, domain),
}),
secondary: localize("ui.components.entity.entity-picker.new_entity"),
icon_path: mdiPlus,
onSelect: ({ close }) => {
close();
this._openCreateHelper(domain);
},
}));
return createDomains.map((domain) => {
const primary = localize(
"ui.components.entity.entity-picker.create_helper",
{
domain: isHelperDomain(domain)
? localize(
`ui.panel.config.helpers.types.${domain as HelperDomain}`
) || domain
: domainToName(localize, domain),
}
);
return {
id: CREATE_ID + domain,
primary: primary,
secondary: localize("ui.components.entity.entity-picker.new_entity"),
icon_path: mdiPlus,
} satisfies EntityComboBoxItem;
});
}
);
private _openCreateHelper(domain: string) {
showHelperDetailDialog(this, {
domain,
dialogClosedCallback: (item) => {
if (!item.entityId) return;
if (this.hass.states[item.entityId]) {
this._setValue(item.entityId);
} else {
this._pendingEntityId = item.entityId;
}
},
});
}
private _getEntitiesMemoized = memoizeOne(getEntities);
private _getItems = () => {
@@ -354,67 +341,53 @@ export class HaEntityPicker extends LitElement {
const placeholder =
this.placeholder ??
this.hass.localize("ui.components.entity.entity-picker.placeholder");
const items = this._getItems();
const actions = this._getCreateActions(
this.hass.localize,
this.createDomains
);
const hideClearIcon = this.hideClearIcon || this._shouldHideClearIcon();
return html`
<div class="picker">
<div class="trigger" @click=${this._openPicker}>
<slot name="trigger">
<ha-picker-field
type="button"
compact
.label=${this.label}
.placeholder=${placeholder}
.value=${this.value}
.valueRenderer=${this._valueRenderer}
.required=${this.required}
.disabled=${this.disabled}
.helper=${this.helper}
.hideClearIcon=${hideClearIcon}
?autofocus=${this.autofocus}
@clear=${this._clear}
></ha-picker-field>
</slot>
</div>
<ha-picker-popover
.open=${this._pickerOpen}
.anchor=${this._trigger ?? null}
.label=${this.label ?? ""}
@closed=${this._handlePickerClosed}
>
<ha-picker-search-list
autofocus
.items=${items}
.value=${this.value}
.searchKeys=${entityComboBoxKeys}
.searchFn=${this._searchFn}
.rowRenderer=${this._rowRenderer}
.actions=${actions}
.searchPlaceholder=${this.searchLabel ??
this.hass.localize("ui.common.search")}
.notFoundLabel=${this._notFoundLabel}
@item-selected=${this._handleItemSelected}
></ha-picker-search-list>
</ha-picker-popover>
</div>
<ha-generic-picker
.hass=${this.hass}
.disabled=${this.disabled}
.autofocus=${this.autofocus}
.allowCustomValue=${this.allowCustomEntity}
.required=${this.required}
.label=${this.label}
.placeholder=${placeholder}
.helper=${this.helper}
.value=${this.addButton ? undefined : this.value}
.searchLabel=${this.searchLabel}
.notFoundLabel=${this._notFoundLabel}
.rowRenderer=${this._rowRenderer}
.getItems=${this._getItems}
.getAdditionalItems=${this._getAdditionalItems}
.hideClearIcon=${this.hideClearIcon || this._shouldHideClearIcon()}
.searchFn=${this._searchFn}
.valueRenderer=${this._valueRenderer}
.searchKeys=${entityComboBoxKeys}
use-top-label
.addButtonLabel=${this.addButton
? (this.addButtonLabel ??
this.hass.localize("ui.components.entity.entity-picker.add"))
: undefined}
.unknownItemText=${this.hass.localize(
"ui.components.entity.entity-picker.unknown"
)}
@value-changed=${this._valueChanged}
>
</ha-generic-picker>
`;
}
private _searchFn: PickerSearchFn<EntityComboBoxItem> = (
private _searchFn: PickerComboBoxSearchFn<EntityComboBoxItem> = (
search,
filteredItems
) => {
// If there is exact match for entity id, put it first
const index = filteredItems.findIndex(
(item) => item.stateObj?.entity_id === search
);
if (index === -1) {
return filteredItems;
}
const [exactMatch] = filteredItems.splice(index, 1);
filteredItems.unshift(exactMatch);
return filteredItems;
@@ -422,43 +395,46 @@ export class HaEntityPicker extends LitElement {
public async open() {
await this.updateComplete;
this._openPicker();
await this._picker?.open();
}
private _openPicker = () => {
if (this.disabled) return;
this._pickerOpen = true;
};
private _handlePickerClosed = () => {
if (this._pendingValue !== undefined) {
const pending = this._pendingValue;
this._pendingValue = undefined;
this._setValue(pending);
}
this._pickerOpen = false;
this._searchList?.reset();
};
private _handleItemSelected = (
ev: HASSDomEvent<{ id: string; index: number; newTab?: boolean }>
) => {
private _valueChanged(ev) {
ev.stopPropagation();
const value = ev.detail.id;
if (!isValidEntityId(value) && !this._findExtraOption(value)) {
this._pickerOpen = false;
const value = ev.detail.value;
if (!value) {
this._setValue(undefined);
return;
}
this._pendingValue = value;
this._pickerOpen = false;
};
private _clear() {
this._setValue(undefined);
if (value.startsWith(CREATE_ID)) {
const domain = value.substring(CREATE_ID.length);
showHelperDetailDialog(this, {
domain,
dialogClosedCallback: (item) => {
if (item.entityId) {
if (this.hass.states[item.entityId]) {
this._setValue(item.entityId);
} else {
this._pendingEntityId = item.entityId;
}
}
},
});
return;
}
if (!isValidEntityId(value) && !this._findExtraOption(value)) {
return;
}
this._setValue(value);
}
private _setValue(value: string | undefined) {
this.value = value;
fireEvent(this, "value-changed", { value });
fireEvent(this, "change");
}
@@ -467,18 +443,6 @@ export class HaEntityPicker extends LitElement {
this.hass.localize("ui.components.entity.entity-picker.no_match", {
term: html`<b>${search}</b>`,
});
static styles = css`
:host {
display: block;
}
.picker {
position: relative;
}
ha-picker-field {
width: 100%;
}
`;
}
declare global {
+6 -3
View File
@@ -6,7 +6,11 @@ import { customElement, property, state } from "lit/decorators";
import { STATES_OFF } from "../../common/const";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
import {
UNAVAILABLE,
UNKNOWN,
isUnavailableState,
} from "../../data/entity/entity";
import { forwardHaptic } from "../../data/haptics";
import type { HomeAssistant } from "../../types";
import "../ha-formfield";
@@ -16,8 +20,7 @@ import "../ha-switch";
const isOn = (stateObj?: HassEntity) =>
stateObj !== undefined &&
!STATES_OFF.includes(stateObj.state) &&
stateObj.state !== UNAVAILABLE &&
stateObj.state !== UNKNOWN;
!isUnavailableState(stateObj.state);
/**
* @element ha-entity-toggle
@@ -9,7 +9,7 @@ import secondsToDuration from "../../common/datetime/seconds_to_duration";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
import { isUnavailableState, UNAVAILABLE } from "../../data/entity/entity";
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
import { timerTimeRemaining } from "../../data/timer";
import type { HomeAssistant } from "../../types";
@@ -170,8 +170,7 @@ export class HaStateLabelBadge extends LitElement {
}
// eslint-disable-next-line: disable=no-fallthrough
default:
return entityState.state === UNAVAILABLE ||
entityState.state === UNKNOWN
return isUnavailableState(entityState.state)
? "—"
: this.hass!.formatEntityStateToParts(entityState).find(
(part) => part.type === "value"
@@ -210,7 +209,7 @@ export class HaStateLabelBadge extends LitElement {
_timerTimeRemaining = 0
) {
// For unavailable states or certain domains, use a special translation that is truncated to fit within the badge label
if (entityState.state === UNAVAILABLE || entityState.state === UNKNOWN) {
if (isUnavailableState(entityState.state)) {
return this.hass!.localize(`state_badge.default.${entityState.state}`);
}
const domainStateKey = getTruncatedKey(domain, entityState.state);
+3 -3
View File
@@ -29,7 +29,7 @@ export interface AreaControlPickerItem extends PickerComboBoxItem {
deviceClass?: string;
}
const AREA_CONTROL_DOMAINS = [
const AREA_CONTROL_DOMAINS: readonly AreaControlDomain[] = [
"light",
"fan",
"switch",
@@ -43,7 +43,7 @@ const AREA_CONTROL_DOMAINS = [
"cover-door",
"cover-window",
"cover-damper",
] as const satisfies readonly AreaControlDomain[];
] as const;
@customElement("ha-area-controls-picker")
export class HaAreaControlsPicker extends LitElement {
@@ -130,7 +130,7 @@ export class HaAreaControlsPicker extends LitElement {
(excludeValues !== undefined && excludeValues.includes(id));
const controlEntities = getAreaControlEntities(
AREA_CONTROL_DOMAINS,
AREA_CONTROL_DOMAINS as unknown as AreaControlDomain[],
areaId,
excludeEntities,
this.hass
+4 -9
View File
@@ -3,7 +3,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import type { ClimateEntity } from "../data/climate";
import { CLIMATE_PRESET_NONE } from "../data/climate";
import { OFF, UNAVAILABLE, UNKNOWN } from "../data/entity/entity";
import { isUnavailableState, OFF } from "../data/entity/entity";
import type { HomeAssistant } from "../types";
@customElement("ha-climate-state")
@@ -14,11 +14,9 @@ class HaClimateState extends LitElement {
protected render(): TemplateResult {
const currentStatus = this._computeCurrentStatus();
const noValue =
this.stateObj.state === UNAVAILABLE || this.stateObj.state === UNKNOWN;
return html`<div class="target">
${!noValue
${!isUnavailableState(this.stateObj.state)
? html`<span class="state-label">
${this._localizeState()}
${this.stateObj.attributes.preset_mode &&
@@ -34,7 +32,7 @@ class HaClimateState extends LitElement {
: this._localizeState()}
</div>
${currentStatus && !noValue
${currentStatus && !isUnavailableState(this.stateObj.state)
? html`
<div class="current">
${this.hass.localize("ui.card.climate.currently")}:
@@ -121,10 +119,7 @@ class HaClimateState extends LitElement {
}
private _localizeState(): string {
if (
this.stateObj.state === UNAVAILABLE ||
this.stateObj.state === UNKNOWN
) {
if (isUnavailableState(this.stateObj.state)) {
return this.hass.localize(`state.default.${this.stateObj.state}`);
}
+4 -11
View File
@@ -6,18 +6,11 @@ import type { HaIconButton } from "./ha-icon-button";
/**
* Event type for the ha-dropdown component when an item is selected.
* @param TValue - The type of the selected item's `value`.
* @param TData - The type of the selected item's `data` when set on `ha-dropdown-item`.
* @param T - The type of the value of the selected item.
*/
export type HaDropdownSelectEvent<TValue = string, TData = undefined> = [
TData,
] extends [undefined]
? CustomEvent<{
item: Omit<HaDropdownItem, "value"> & { value: TValue };
}>
: CustomEvent<{
item: Omit<HaDropdownItem, "value"> & { value: TValue; data: TData };
}>;
export type HaDropdownSelectEvent<T = string> = CustomEvent<{
item: Omit<HaDropdownItem, "value"> & { value: T };
}>;
/**
* Home Assistant dropdown component
+4 -9
View File
@@ -1,7 +1,7 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { OFF, UNAVAILABLE, UNKNOWN } from "../data/entity/entity";
import { isUnavailableState, OFF } from "../data/entity/entity";
import type { HumidifierEntity } from "../data/humidifier";
import type { HomeAssistant } from "../types";
@@ -13,11 +13,9 @@ class HaHumidifierState extends LitElement {
protected render(): TemplateResult {
const currentStatus = this._computeCurrentStatus();
const noValue =
this.stateObj.state === UNAVAILABLE || this.stateObj.state === UNKNOWN;
return html`<div class="target">
${!noValue
${!isUnavailableState(this.stateObj.state)
? html`<span class="state-label">
${this._localizeState()}
${this.stateObj.attributes.mode
@@ -32,7 +30,7 @@ class HaHumidifierState extends LitElement {
: this._localizeState()}
</div>
${currentStatus && !noValue
${currentStatus && !isUnavailableState(this.stateObj.state)
? html`<div class="current">
${this.hass.localize("ui.card.humidifier.currently")}:
<div class="unit">${currentStatus}</div>
@@ -71,10 +69,7 @@ class HaHumidifierState extends LitElement {
}
private _localizeState(): string {
if (
this.stateObj.state === UNAVAILABLE ||
this.stateObj.state === UNKNOWN
) {
if (isUnavailableState(this.stateObj.state)) {
return this.hass.localize(`state.default.${this.stateObj.state}`);
}
-394
View File
@@ -1,394 +0,0 @@
import type { LitVirtualizer } from "@lit-labs/virtualizer";
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import { mdiMagnify, mdiMinusBoxOutline } from "@mdi/js";
import {
css,
html,
LitElement,
type CSSResultGroup,
type TemplateResult,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { tinykeys } from "tinykeys";
import {
fireEvent,
type HASSDomCurrentTargetEvent,
} from "../common/dom/fire_event";
import { loadVirtualizer } from "../resources/virtualizer";
import "./ha-combo-box-item";
import {
DEFAULT_ROW_RENDERER_CONTENT,
type PickerComboBoxItem,
} from "./ha-picker-combo-box";
import "./ha-svg-icon";
const EMPTY_ROW_ID = "___empty___";
export interface PickerActionContext {
host: HTMLElement;
close: () => void;
}
/** Items with `onSelect` are action rows: the callback fires instead of `item-selected`. */
export interface PickerListItem extends PickerComboBoxItem {
onSelect?: (ctx: PickerActionContext) => void | Promise<void>;
}
export type PickerListEntry = PickerListItem | string;
interface PickerListRowElement extends HTMLDivElement {
index: number;
item: PickerListItem;
}
const DEFAULT_ROW: RenderItemFunction<PickerListItem> = (item) =>
html`<ha-combo-box-item type="button" compact>
${DEFAULT_ROW_RENDERER_CONTENT(item)}
</ha-combo-box-item>`;
/**
* Headless virtualized list for picker UIs. Receives pre-filtered `items`,
* renders rows via `rowRenderer`. String entries are section titles.
*/
@customElement("ha-picker-list")
export class HaPickerList extends LitElement {
@property({ attribute: false }) public items: PickerListEntry[] = [];
@property() public value?: string;
@property({ attribute: false })
public rowRenderer?: RenderItemFunction<PickerListItem>;
@property({ attribute: "empty-label" }) public emptyLabel?: string;
@property({ attribute: false })
public notFoundLabel?:
| string
| TemplateResult
| ((search: string) => string | TemplateResult);
/** Current search string. Picks between empty/notFound placeholders; filtering is the consumer's job. */
@property({ attribute: "current-search" }) public currentSearch = "";
@state() private _highlightedIndex = -1;
@state() private _valuePinned = true;
@query("lit-virtualizer") private _virtualizer?: LitVirtualizer;
private _unsubscribeKeys?: () => void;
public willUpdate() {
if (!this.hasUpdated) {
loadVirtualizer();
}
}
protected firstUpdated() {
this._registerKeys();
}
public disconnectedCallback() {
super.disconnectedCallback();
this._unsubscribeKeys?.();
}
public selectNext = (ev?: KeyboardEvent) => this._next(ev);
public selectPrev = (ev?: KeyboardEvent) => this._prev(ev);
public selectFirst = (ev?: KeyboardEvent) => this._first(ev);
public selectLast = (ev?: KeyboardEvent) => this._last(ev);
public commitHighlighted = (newTab = false) =>
this._commitAt(this._highlightedIndex, newTab);
protected render() {
const items = this.items.length
? this.items
: [{ id: EMPTY_ROW_ID, primary: "" } as PickerListItem];
return html`
<lit-virtualizer
.keyFunction=${this._keyFunction}
tabindex="0"
scroller
.items=${items}
.renderItem=${this._renderEntry}
.layout=${this.value && this._valuePinned
? {
pin: {
index: this._initialPinIndex(),
block: "center",
},
}
: undefined}
@unpinned=${this._handleUnpinned}
@focus=${this._focusList}
@blur=${this._resetHighlight}
></lit-virtualizer>
`;
}
private _keyFunction = (item: PickerListEntry) =>
typeof item === "string" ? item : item.id;
private _renderEntry: RenderItemFunction<PickerListEntry> = (item, index) => {
if (typeof item === "string") {
return html`<div class="title">${item}</div>`;
}
if (item.id === EMPTY_ROW_ID) {
return this._renderEmptyRow();
}
const renderer = this.rowRenderer ?? DEFAULT_ROW;
return html`<div
id=${`list-item-${index}`}
class="row ${this.value === item.id ? "current-value" : ""}"
.item=${item}
.index=${index}
@click=${this._handleClick}
>
${renderer(item as PickerListItem, index)}
</div>`;
};
private _renderEmptyRow() {
const search = this.currentSearch;
const message = search
? typeof this.notFoundLabel === "function"
? this.notFoundLabel(search)
: (this.notFoundLabel ?? "No matching items found")
: (this.emptyLabel ?? "No items available");
return html`
<div class="row empty">
<ha-combo-box-item type="text" compact>
<ha-svg-icon
slot="start"
.path=${search ? mdiMagnify : mdiMinusBoxOutline}
></ha-svg-icon>
<span slot="headline">${message}</span>
</ha-combo-box-item>
</div>
`;
}
private _handleClick = (
ev: MouseEvent & HASSDomCurrentTargetEvent<PickerListRowElement>
) => {
ev.stopPropagation();
const row = ev.currentTarget;
if (row.item.disabled) return;
this._dispatchSelection(row.item, row.index, ev.ctrlKey || ev.metaKey);
};
private _dispatchSelection(
item: PickerListItem,
index: number,
newTab: boolean
) {
if (item.onSelect) {
void item.onSelect({
host: this,
close: () => fireEvent(this, "picker-close-request"),
});
return;
}
fireEvent(this, "item-selected", { id: item.id, index, newTab });
}
private _handleUnpinned = () => {
this._valuePinned = false;
};
private _registerKeys() {
this._unsubscribeKeys = tinykeys(this, {
ArrowDown: this._next,
ArrowUp: this._prev,
Home: this._first,
End: this._last,
Enter: this._commitHighlight,
"$mod+Enter": this._commitHighlightNewTab,
});
}
private _focusList = () => {
if (this._highlightedIndex === -1) this._initializeHighlight();
};
private _resetHighlight = () => {
this._virtualizer?.querySelector(".selected")?.classList.remove("selected");
this._highlightedIndex = -1;
};
private _initializeHighlight() {
if (!this._virtualizer) return;
const items = this._virtualizer.items as PickerListEntry[];
if (this.value) {
const i = items.findIndex(
(item) => typeof item !== "string" && item.id === this.value
);
if (i !== -1) {
this._highlightedIndex = i;
this._scrollToHighlight();
return;
}
}
this._first();
}
private _initialPinIndex(): number {
if (!this.value) return 0;
const i = this.items.findIndex(
(item) => typeof item !== "string" && item.id === this.value
);
return i === -1 ? 0 : i;
}
private _isPickable(item: PickerListEntry | undefined): boolean {
return !!item && typeof item !== "string" && item.id !== EMPTY_ROW_ID;
}
private _step(direction: 1 | -1) {
if (!this._virtualizer) return;
const items = this._virtualizer.items as PickerListEntry[];
if (!items.length) return;
let i = this._highlightedIndex + direction;
const guard = items.length;
let n = 0;
while (n++ < guard && i >= 0 && i < items.length) {
if (this._isPickable(items[i])) {
this._highlightedIndex = i;
this._scrollToHighlight();
return;
}
i += direction;
}
}
private _next = (ev?: KeyboardEvent) => {
ev?.preventDefault();
if (this._highlightedIndex === -1) {
this._initializeHighlight();
return;
}
this._step(1);
};
private _prev = (ev?: KeyboardEvent) => {
ev?.preventDefault();
if (this._highlightedIndex === -1) {
this._initializeHighlight();
return;
}
this._step(-1);
};
private _first = (ev?: KeyboardEvent) => {
ev?.preventDefault();
this._jumpTo(0, 1);
};
private _last = (ev?: KeyboardEvent) => {
ev?.preventDefault();
if (!this._virtualizer) return;
this._jumpTo(this._virtualizer.items.length - 1, -1);
};
private _jumpTo(start: number, direction: 1 | -1) {
if (!this._virtualizer) return;
const items = this._virtualizer.items as PickerListEntry[];
for (let i = start; i >= 0 && i < items.length; i += direction) {
if (this._isPickable(items[i])) {
this._highlightedIndex = i;
this._scrollToHighlight();
return;
}
}
}
private _commitHighlight = (ev: KeyboardEvent) => {
this._commitAt(this._highlightedIndex, ev.ctrlKey || ev.metaKey);
};
private _commitHighlightNewTab = () => {
this._commitAt(this._highlightedIndex, true);
};
private _commitAt(index: number, newTab: boolean) {
if (index === -1 || !this._virtualizer) return;
const item = this._virtualizer.items[index] as PickerListEntry;
if (typeof item === "string" || item.disabled) return;
this._dispatchSelection(item, index, newTab);
}
private _scrollToHighlight() {
this._virtualizer?.querySelector(".selected")?.classList.remove("selected");
this._virtualizer
?.element(this._highlightedIndex)
?.scrollIntoView({ block: "nearest" });
requestAnimationFrame(() => {
this._virtualizer
?.querySelector(`#list-item-${this._highlightedIndex}`)
?.classList.add("selected");
});
}
static styles: CSSResultGroup = css`
:host {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
}
lit-virtualizer {
flex: 1;
outline: none;
}
.row {
display: flex;
width: 100%;
align-items: center;
box-sizing: border-box;
min-height: 36px;
cursor: pointer;
}
.row.empty {
cursor: default;
}
.row ha-combo-box-item {
width: 100%;
}
.row.current-value {
background-color: var(--ha-color-fill-primary-quiet-resting);
}
.row.selected {
background-color: var(--ha-color-fill-neutral-quiet-hover);
}
@media (prefers-color-scheme: dark) {
.row.selected {
background-color: var(--ha-color-fill-neutral-normal-hover);
}
}
.title {
box-sizing: border-box;
width: 100%;
background-color: var(--ha-color-fill-neutral-quiet-resting);
padding: var(--ha-space-1) var(--ha-space-4);
font-weight: var(--ha-font-weight-bold);
color: var(--secondary-text-color);
min-height: var(--ha-space-6);
display: flex;
align-items: center;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-picker-list": HaPickerList;
}
interface HASSDomEvents {
"item-selected": { id: string; index: number; newTab?: boolean };
"picker-close-request": undefined;
}
}
-257
View File
@@ -1,257 +0,0 @@
import "@home-assistant/webawesome/dist/components/popover/popover";
import {
css,
html,
LitElement,
nothing,
type CSSResultGroup,
type PropertyValues,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-bottom-sheet";
/**
* Responsive popover for picker UIs: anchored `wa-popover` on desktop,
* `ha-bottom-sheet` on narrow viewports. Anchor drives the width.
*/
@customElement("ha-picker-popover")
export class HaPickerPopover extends LitElement {
@property({ type: Boolean, reflect: true }) public open = false;
@property({ attribute: false }) public anchor?: HTMLElement | null;
@property() public label?: string;
@property() public placement:
| "bottom"
| "top"
| "left"
| "right"
| "top-start"
| "top-end"
| "right-start"
| "right-end"
| "bottom-start"
| "bottom-end"
| "left-start"
| "left-end" = "bottom-start";
@state() private _bodyWidth = 0;
@state() private _narrow = false;
@state() private _openedNarrow = false;
// Kept true across the hide animation so wa-popover can finish its
// transition; cleared on wa-after-hide.
@state() private _mounted = false;
// Flipped one rAF after mount so wa-popover sees a false→true edge
// and runs the show flow.
@state() private _showing = false;
// Defers slot projection until after the show animation so a
// virtualized list inside isn't measured at the scaled size.
@state() private _contentReady = false;
private _openFrame?: number;
protected willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has("open")) {
if (this.open) {
this._measureAnchor();
this._openedNarrow = this._narrow;
this._mounted = true;
} else {
this._showing = false;
}
}
if (changedProperties.has("anchor") && this.open) {
this._measureAnchor();
}
}
private _measureAnchor() {
if (this.anchor) {
this._bodyWidth = this.anchor.offsetWidth;
}
}
protected updated() {
if (this.open && this._mounted && !this._showing) {
this._scheduleShow();
}
}
private _scheduleShow() {
if (this._openFrame !== undefined) return;
this._openFrame = requestAnimationFrame(() => {
this._openFrame = undefined;
if (this.open && this._mounted) {
this._showing = true;
}
});
}
private _cancelShow() {
if (this._openFrame === undefined) return;
cancelAnimationFrame(this._openFrame);
this._openFrame = undefined;
}
connectedCallback() {
super.connectedCallback();
this._handleResize();
window.addEventListener("resize", this._handleResize);
this.addEventListener("picker-close-request", this._handleCloseRequest);
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener("resize", this._handleResize);
this.removeEventListener("picker-close-request", this._handleCloseRequest);
this._cancelShow();
}
private _handleCloseRequest = (ev: Event) => {
ev.stopPropagation();
this._showing = false;
};
private _handleResize = () => {
this._narrow =
window.matchMedia("(max-width: 870px)").matches ||
window.matchMedia("(max-height: 500px)").matches;
if (!this._openedNarrow && this.open) {
this._measureAnchor();
}
};
private _handleShown = () => {
this._contentReady = true;
fireEvent(this, "opened");
// Native [autofocus] fires before the popover is visible; refocus
// a slotted search component after projection.
requestAnimationFrame(() => {
const focusable = this.querySelector(
"ha-picker-search-list, ha-picker-search"
) as (HTMLElement & { focus?: () => void }) | null;
focusable?.focus?.();
});
};
private _handleHidden = (ev: Event) => {
ev.stopPropagation();
this._mounted = false;
this._showing = false;
this._contentReady = false;
fireEvent(this, "closed");
};
protected render() {
if (!this._mounted) return nothing;
if (this._openedNarrow) {
return html`
<ha-bottom-sheet
flexcontent
.open=${this._showing}
@wa-after-show=${this._handleShown}
@closed=${this._handleHidden}
role="dialog"
aria-modal="true"
aria-label=${this.label ?? ""}
>
<div class="content">
${this._contentReady ? html`<slot></slot>` : nothing}
</div>
</ha-bottom-sheet>
`;
}
return html`
<wa-popover
.open=${this._showing}
style=${styleMap({ "--body-width": `${this._bodyWidth}px` })}
without-arrow
distance="-4"
.placement=${this.placement}
.anchor=${this.anchor ?? null}
auto-size="vertical"
auto-size-padding="16"
@wa-after-show=${this._handleShown}
@wa-after-hide=${this._handleHidden}
trap-focus
role="dialog"
aria-modal="true"
aria-label=${this.label ?? ""}
>
<div class="content">
${this._contentReady ? html`<slot></slot>` : nothing}
</div>
</wa-popover>
`;
}
static styles: CSSResultGroup = css`
:host {
display: contents;
}
wa-popover {
--wa-space-l: 0;
/* Disable wa-popover's built-in 25rem cap. */
--max-width: none;
}
wa-popover::part(dialog)::backdrop {
background: none;
}
wa-popover::part(body) {
width: var(--ha-picker-popover-width, max(var(--body-width), 250px));
max-width: var(
--ha-picker-popover-max-width,
var(--ha-picker-popover-width, max(var(--body-width), 250px))
);
max-height: 500px;
height: 70vh;
overflow: hidden;
padding: 0;
}
@media (max-height: 1000px) {
wa-popover::part(body) {
max-height: 400px;
}
}
.content {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
}
ha-bottom-sheet {
--ha-bottom-sheet-height: 90vh;
--ha-bottom-sheet-height: calc(100dvh - var(--ha-space-12));
--ha-bottom-sheet-max-height: var(--ha-bottom-sheet-height);
--ha-bottom-sheet-max-width: 600px;
--ha-bottom-sheet-padding: 0;
--ha-bottom-sheet-surface-background: var(--card-background-color);
--ha-bottom-sheet-border-radius: var(--ha-border-radius-2xl);
--ha-bottom-sheet-content-padding: 0 var(--safe-area-inset-right)
var(--safe-area-inset-bottom) var(--safe-area-inset-left);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-picker-popover": HaPickerPopover;
}
}
-188
View File
@@ -1,188 +0,0 @@
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
import Fuse from "fuse.js";
import {
css,
html,
LitElement,
type CSSResultGroup,
type TemplateResult,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import type { HASSDomEvent } from "../common/dom/fire_event";
import {
multiTermSortedSearch,
type FuseWeightedKey,
} from "../resources/fuseMultiTerm";
import { DEFAULT_SEARCH_KEYS } from "./ha-picker-combo-box";
import type { HaPickerList, PickerListItem } from "./ha-picker-list";
import "./ha-picker-list";
import "./ha-picker-search";
import type { HaPickerSearch } from "./ha-picker-search";
export type PickerSearchFn<T extends PickerListItem = PickerListItem> = (
search: string,
filtered: T[],
all: T[]
) => T[];
/**
* Search input + virtualized list with built-in Fuse.js filtering.
* For custom filtering pipelines, compose `ha-picker-search` and
* `ha-picker-list` directly instead.
*/
@customElement("ha-picker-search-list")
export class HaPickerSearchList<
T extends PickerListItem = PickerListItem,
> extends LitElement {
@property({ attribute: false }) public items: T[] = [];
@property() public value?: string;
@property({ attribute: false }) public searchKeys?: FuseWeightedKey[];
@property({ attribute: false }) public searchFn?: PickerSearchFn<T>;
@property({ attribute: false })
public rowRenderer?: RenderItemFunction<T>;
@property({ attribute: false }) public actions?: PickerListItem[];
@property({ attribute: "search-placeholder" })
public searchPlaceholder?: string;
@property({ attribute: "empty-label" }) public emptyLabel?: string;
@property({ attribute: false })
public notFoundLabel?:
| string
| TemplateResult
| ((search: string) => string | TemplateResult);
// eslint-disable-next-line lit/no-native-attributes
@property({ type: Boolean }) public autofocus = false;
@state() private _search = "";
@query("ha-picker-search") private _searchEl?: HaPickerSearch;
@query("ha-picker-list") private _listEl?: HaPickerList;
public focus() {
this._searchEl?.focus();
}
public reset() {
this._search = "";
}
protected render() {
const displayItems = this._computeDisplayItems(
this.items,
this._search,
this.searchKeys,
this.searchFn,
this.actions
);
return html`
<ha-picker-search
?autofocus=${this.autofocus}
.value=${this._search}
.placeholder=${this.searchPlaceholder ?? ""}
@search-changed=${this._handleSearchChanged}
@keydown=${this._handleSearchKeydown}
></ha-picker-search>
<ha-picker-list
.items=${displayItems}
.value=${this.value}
.rowRenderer=${this.rowRenderer as RenderItemFunction<PickerListItem>}
.currentSearch=${this._search}
.notFoundLabel=${this.notFoundLabel}
.emptyLabel=${this.emptyLabel}
></ha-picker-list>
`;
}
// Forward nav keys from search input to list (focus stays in search).
private _handleSearchKeydown = (ev: KeyboardEvent) => {
const list = this._listEl;
if (!list) return;
switch (ev.key) {
case "ArrowDown":
ev.preventDefault();
list.selectNext(ev);
break;
case "ArrowUp":
ev.preventDefault();
list.selectPrev(ev);
break;
case "Home":
ev.preventDefault();
list.selectFirst(ev);
break;
case "End":
ev.preventDefault();
list.selectLast(ev);
break;
case "Enter":
ev.preventDefault();
list.commitHighlighted(ev.ctrlKey || ev.metaKey);
break;
default:
}
};
private _fuseIndex = memoizeOne(
(items: T[], searchKeys?: FuseWeightedKey[]) =>
Fuse.createIndex(searchKeys ?? DEFAULT_SEARCH_KEYS, items)
);
private _computeDisplayItems = memoizeOne(
(
items: T[],
search: string,
searchKeys: FuseWeightedKey[] | undefined,
searchFn: PickerSearchFn<T> | undefined,
actions: PickerListItem[] | undefined
): PickerListItem[] => {
let filtered = items;
if (search) {
const keys = searchKeys ?? DEFAULT_SEARCH_KEYS;
const index = this._fuseIndex(items, keys);
filtered = multiTermSortedSearch<T>(
items,
search,
keys,
(item) => item.id,
index
);
if (searchFn) {
filtered = searchFn(search, filtered, items);
}
}
if (actions?.length) {
return [...filtered, ...actions];
}
return filtered;
}
);
private _handleSearchChanged = (ev: HASSDomEvent<{ value: string }>) => {
this._search = ev.detail.value;
};
static styles: CSSResultGroup = css`
:host {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-picker-search-list": HaPickerSearchList;
}
}
-60
View File
@@ -1,60 +0,0 @@
import { css, html, LitElement, type CSSResultGroup } from "lit";
import { customElement, property, query } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import "./input/ha-input-search";
import type { HaInputSearch } from "./input/ha-input-search";
/** Search input for picker UIs; emits `search-changed`. */
@customElement("ha-picker-search")
export class HaPickerSearch extends LitElement {
@property() public value = "";
@property() public placeholder?: string;
// eslint-disable-next-line lit/no-native-attributes
@property({ type: Boolean }) public autofocus = false;
@query("ha-input-search") private _input?: HaInputSearch;
public focus() {
// ha-input doesn't expose focus(); reach the wa-input it wraps.
this._input?.shadowRoot?.querySelector<HTMLElement>("wa-input")?.focus();
}
protected render() {
return html`
<ha-input-search
appearance="outlined"
.value=${this.value}
.placeholder=${this.placeholder ?? ""}
?autofocus=${this.autofocus}
@input=${this._handleInput}
></ha-input-search>
`;
}
private _handleInput = (ev: Event) => {
const value = (ev.target as HaInputSearch).value ?? "";
this.value = value;
fireEvent(this, "search-changed", { value });
};
static styles: CSSResultGroup = css`
:host {
display: block;
padding: 0 var(--ha-space-3) var(--ha-space-3);
}
ha-input-search {
width: 100%;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-picker-search": HaPickerSearch;
}
interface HASSDomEvents {
"search-changed": { value: string };
}
}
-94
View File
@@ -1,94 +0,0 @@
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../common/dom/fire_event";
import { isTouch } from "../util/is_touch";
import "./chips/ha-chip-set";
import "./chips/ha-filter-chip";
export interface PickerSection {
id: string;
label: string;
}
export type PickerSectionDef = PickerSection | "separator";
/** Section filter chip bar; emits `section-changed`. Toggling the active chip clears the filter. */
@customElement("ha-picker-section-chips")
export class HaPickerSectionChips extends LitElement {
@property({ attribute: false }) public sections?: PickerSectionDef[];
@property() public selected?: string;
protected render() {
if (!this.sections?.length) return nothing;
return html`
<ha-chip-set>
${this.sections.map((section) =>
section === "separator"
? html`<div class="separator"></div>`
: html`<ha-filter-chip
@mousedown=${isTouch ? undefined : this._preventBlur}
@click=${this._handleClick}
data-section-id=${section.id}
.selected=${this.selected === section.id}
.label=${section.label}
></ha-filter-chip>`
)}
</ha-chip-set>
`;
}
private _preventBlur = (ev: Event) => {
ev.preventDefault();
};
private _handleClick = (ev: Event) => {
const id = (ev.currentTarget as HTMLElement).dataset.sectionId;
if (!id) return;
const next = this.selected === id ? undefined : id;
this.selected = next;
fireEvent(this, "section-changed", { section: next });
};
static styles: CSSResultGroup = css`
:host {
display: block;
padding: 0 var(--ha-space-3) var(--ha-space-3);
}
ha-chip-set {
display: flex;
flex-wrap: nowrap;
gap: var(--ha-space-2);
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: none;
/* Room for the chip's focus ring (clipped by overflow-y: hidden). */
padding: var(--ha-space-1) 0;
margin: calc(-1 * var(--ha-space-1)) 0;
}
ha-chip-set::-webkit-scrollbar {
display: none;
}
ha-chip-set ha-filter-chip {
flex-shrink: 0;
--md-filter-chip-selected-container-color: var(
--ha-color-fill-primary-normal-hover
);
color: var(--primary-color);
}
.separator {
height: var(--ha-space-8);
width: 0;
border: 1px solid var(--ha-color-border-neutral-quiet);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-picker-section-chips": HaPickerSectionChips;
}
interface HASSDomEvents {
"section-changed": { section: string | undefined };
}
}
@@ -2,7 +2,7 @@ import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { fireEvent, type HASSDomEvent } from "../../common/dom/fire_event";
import { fireEvent } from "../../common/dom/fire_event";
import type { ColorTempSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-labeled-slider";
@@ -94,10 +94,10 @@ export class HaColorTempSelector extends LitElement {
}
);
private _valueChanged(ev: HASSDomEvent<HASSDomEvents["value-changed"]>) {
private _valueChanged(ev: CustomEvent) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: Number(ev.detail.value),
value: Number((ev.detail as any).value),
});
}
}
+1 -1
View File
@@ -837,7 +837,7 @@ export class HaServiceControl extends LitElement {
if (targetDevices.length) {
targetDevices = targetDevices.filter((device) =>
deviceMeetsTargetSelector(
this.hass.states,
this.hass,
Object.values(this.hass.entities),
this.hass.devices[device],
targetSelector
-1
View File
@@ -30,7 +30,6 @@ export class HaSettingsRow extends LitElement {
<slot name="prefix"></slot>
<div
class="body"
part="heading"
?two-line=${!this.threeLine && hasDescription}
?three-line=${this.threeLine}
>
+132 -145
View File
@@ -1,5 +1,6 @@
import "@home-assistant/webawesome/dist/components/popover/popover";
import { consume } from "@lit/context";
import { mdiPlaylistPlus, mdiPlus, mdiTextureBox } from "@mdi/js";
import { mdiPlus, mdiTextureBox } from "@mdi/js";
import Fuse from "fuse.js";
import type { HassServiceTarget } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
@@ -52,22 +53,19 @@ import {
multiTermSortedSearch,
type FuseWeightedKey,
} from "../resources/fuseMultiTerm";
import type { HomeAssistant } from "../types";
import type { HomeAssistant, ValueChangedEvent } from "../types";
import { brandsUrl } from "../util/brands-url";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-button";
import "./ha-generic-picker";
import type { HaGenericPicker } from "./ha-generic-picker";
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
import "./ha-picker-list";
import type { PickerListEntry, PickerListItem } from "./ha-picker-list";
import "./ha-picker-popover";
import "./ha-picker-search";
import "./ha-picker-section-chips";
import "./ha-svg-icon";
import "./ha-tree-indicator";
import "./target-picker/ha-target-picker-item-group";
import "./target-picker/ha-target-picker-value-chip";
const SEPARATOR = "________";
const CREATE_ID = "___create-new-entity___";
const isTargetType = (value: string): value is TargetType =>
value === "entity" ||
value === "device" ||
@@ -124,15 +122,11 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
@state() private _configEntryLookup: Record<string, ConfigEntry> = {};
@state() private _pickerOpen = false;
@state() private _search = "";
@state()
@consume({ context: labelsContext, subscribe: true })
private _labelRegistry!: LabelRegistryEntry[];
@query(".add-target-wrapper") private _addTargetWrapper?: HTMLElement;
@query("ha-generic-picker") private _picker?: HaGenericPicker;
private _newTarget?: TargetItem;
@@ -418,92 +412,56 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
},
];
const items = this._buildListEntries(
this._search,
this._selectedSection,
this.createDomains
);
return html`
<div class="add-target-wrapper">
<ha-button
class="add-target-button"
size="small"
appearance="filled"
<ha-generic-picker
.hass=${this.hass}
.disabled=${this.disabled}
@click=${this._openPicker}
.autofocus=${this.autofocus}
.helper=${this.helper}
.sections=${sections}
.notFoundLabel=${this._noTargetFoundLabel}
.emptyLabel=${this.hass.localize(
"ui.components.target-picker.no_targets"
)}
.sectionTitleFunction=${this._sectionTitleFunction}
.selectedSection=${this._selectedSection}
.popoverAnchor=${this._replaceTargetAnchor}
.rowRenderer=${this._renderRow}
.getItems=${this._getItems}
@value-changed=${this._targetPicked}
@picker-closed=${this._handlePickerClosed}
.addButtonLabel=${this.hass.localize(
"ui.components.target-picker.add_target"
)}
.getAdditionalItems=${this._getAdditionalItems}
>
<ha-svg-icon .path=${mdiPlaylistPlus} slot="start"></ha-svg-icon>
${this.hass.localize("ui.components.target-picker.add_target")}
</ha-button>
<ha-picker-popover
.open=${this._pickerOpen}
.anchor=${this._replaceTargetAnchor ?? this._addTargetWrapper}
.label=${this.hass.localize("ui.components.target-picker.add_target")}
@closed=${this._handlePickerClosed}
>
<div class="picker-body">
<ha-picker-search
autofocus
.value=${this._search}
.placeholder=${this.hass.localize("ui.common.search")}
@search-changed=${this._handleSearchChanged}
></ha-picker-search>
<ha-picker-section-chips
.sections=${sections}
.selected=${this._selectedSection}
@section-changed=${this._handleSectionChanged}
></ha-picker-section-chips>
<ha-picker-list
.items=${items}
.rowRenderer=${this._renderRow}
.currentSearch=${this._search}
.notFoundLabel=${this._noTargetFoundLabel}
.emptyLabel=${this.hass.localize(
"ui.components.target-picker.no_targets"
)}
@item-selected=${this._handleItemSelected}
></ha-picker-list>
</div>
</ha-picker-popover>
</ha-generic-picker>
</div>
`;
}
private _openPicker = () => {
if (this.disabled) return;
this._pickerOpen = true;
};
private _handleSearchChanged = (ev: HASSDomEvent<{ value: string }>) => {
this._search = ev.detail.value;
};
private _handleSectionChanged = (
ev: HASSDomEvent<{ section: string | undefined }>
) => {
this._selectedSection = ev.detail.section as
| TargetTypeFloorless
| undefined;
};
private _handleItemSelected = (
ev: HASSDomEvent<{ id: string; index: number; newTab?: boolean }>
) => {
private _targetPicked(ev: ValueChangedEvent<string>) {
ev.stopPropagation();
const value = ev.detail.id;
const value = ev.detail.value;
if (value.startsWith(CREATE_ID)) {
this._createNewDomainElement(value.substring(CREATE_ID.length));
return;
}
const [rawType, id] = value.split(SEPARATOR);
if (!id || !isTargetType(rawType)) {
return;
}
this._pickerOpen = false;
this._pendingPick = { type: rawType, id };
};
if (this._replaceTarget) {
this._replaceTargetItem(this._replaceTarget, { type: rawType, id });
return;
}
// Commit fires on @closed (after the hide animation) to avoid flicker.
private _pendingPick?: TargetItem;
this._addTarget(id, rawType);
}
private _replaceTargetItem(currentTarget: TargetItem, newTarget: TargetItem) {
const value = this._replaceTargetInValue(
@@ -776,27 +734,18 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
return;
}
this._replaceTarget = { type, id: ev.detail.id };
this._pickerOpen = true;
this._picker?.open(undefined, {
selectedValue: `${type}${SEPARATOR}${ev.detail.id}`,
});
}
private _handlePickerClosed = () => {
if (this._pendingPick) {
const pick = this._pendingPick;
this._pendingPick = undefined;
if (this._replaceTarget) {
this._replaceTargetItem(this._replaceTarget, pick);
} else {
this._addTarget(pick.id, pick.type);
}
}
this._pickerOpen = false;
this._search = "";
private _handlePickerClosed() {
if (this._replaceTarget) {
this._selectedSection = undefined;
}
this._replaceTarget = undefined;
this._replaceTargetAnchor = undefined;
};
}
private _addItems(
value: this["value"],
@@ -833,12 +782,55 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
return undefined;
}
private _buildListEntries(
searchString: string,
section: TargetTypeFloorless | undefined,
createDomains: this["createDomains"]
): PickerListEntry[] {
const items = this._getItemsMemoized(
private _sectionTitleFunction = ({
firstIndex,
lastIndex,
firstItem,
secondItem,
itemsCount,
}: {
firstIndex: number;
lastIndex: number;
firstItem: PickerComboBoxItem | string;
secondItem: PickerComboBoxItem | string;
itemsCount: number;
}) => {
if (
firstItem === undefined ||
secondItem === undefined ||
typeof firstItem === "string" ||
(typeof secondItem === "string" && secondItem !== "padding") ||
(firstIndex === 0 && lastIndex === itemsCount - 1)
) {
return undefined;
}
const type = getTargetComboBoxItemType(firstItem as PickerComboBoxItem);
const translationType:
| "areas"
| "entities"
| "devices"
| "labels"
| undefined =
type === "area" || type === "floor"
? "areas"
: type === "entity"
? "entities"
: type && type !== "empty"
? `${type}s`
: undefined;
return translationType
? this.hass.localize(
`ui.components.target-picker.type.${translationType}`
)
: undefined;
};
private _getItems = (searchString: string, section: string) => {
this._selectedSection = section as TargetTypeFloorless | undefined;
return this._getItemsMemoized(
this.hass.localize,
this.entityFilter,
this.deviceFilter,
@@ -848,37 +840,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
this._replaceTarget,
searchString,
this._configEntryLookup,
section
) as PickerListEntry[];
const actions = this._buildActionEntries(createDomains);
return actions.length ? [...items, ...actions] : items;
}
private _buildActionEntries = memoizeOne(
(createDomains: this["createDomains"]): PickerListItem[] => {
if (!createDomains?.length) return [];
return createDomains.map((domain) => ({
id: `__create-helper__${SEPARATOR}${domain}`,
primary: this.hass.localize(
"ui.components.entity.entity-picker.create_helper",
{
domain: isHelperDomain(domain)
? this.hass.localize(`ui.panel.config.helpers.types.${domain}`)
: domainToName(this.hass.localize, domain),
}
),
secondary: this.hass.localize(
"ui.components.entity.entity-picker.new_entity"
),
icon_path: mdiPlus,
onSelect: ({ close }) => {
close();
this._createNewDomainElement(domain);
},
}));
}
);
this._selectedSection
);
};
private _getItemsMemoized = memoizeOne(
(
@@ -1119,6 +1083,36 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
});
}
private _getAdditionalItems = () => this._getCreateItems(this.createDomains);
private _getCreateItems = memoizeOne(
(createDomains: this["createDomains"]) => {
if (!createDomains?.length) {
return [];
}
return createDomains.map((domain) => {
const primary = this.hass.localize(
"ui.components.entity.entity-picker.create_helper",
{
domain: isHelperDomain(domain)
? this.hass.localize(`ui.panel.config.helpers.types.${domain}`)
: domainToName(this.hass.localize, domain),
}
);
return {
id: CREATE_ID + domain,
primary: primary,
secondary: this.hass.localize(
"ui.components.entity.entity-picker.new_entity"
),
icon_path: mdiPlus,
} satisfies EntityComboBoxItem;
});
}
);
private async _loadConfigEntries() {
const configEntries = await getConfigEntries(this.hass);
this._configEntryLookup = Object.fromEntries(
@@ -1262,21 +1256,14 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
);
static styles = css`
:host {
display: block;
}
.add-target-wrapper {
display: block;
display: flex;
justify-content: flex-start;
margin-top: var(--ha-space-3);
}
.picker-body {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
padding-top: var(--ha-space-4);
ha-generic-picker {
width: 100%;
}
.items {
+1 -16
View File
@@ -1,13 +1,12 @@
import "@home-assistant/webawesome/dist/components/textarea/textarea";
import type WaTextarea from "@home-assistant/webawesome/dist/components/textarea/textarea";
import { HasSlotController } from "@home-assistant/webawesome/dist/internal/slot";
import type { PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { stopPropagation } from "../common/dom/stop_propagation";
import { WaInputMixin, waInputStyles } from "./input/wa-input-mixin";
import { stopPropagation } from "../common/dom/stop_propagation";
/**
* Home Assistant textarea component
@@ -85,20 +84,6 @@ export class HaTextArea extends WaInputMixin(LitElement) {
this.removeEventListener("keydown", stopPropagation);
}
protected override async firstUpdated(
changedProperties: PropertyValues<this>
): Promise<void> {
super.firstUpdated(changedProperties);
if (this.autofocus) {
await this._textarea?.updateComplete;
this._textarea?.focus();
}
}
public override focus(options?: FocusOptions): void {
this._textarea?.focus(options);
}
protected render() {
const hasLabelSlot = this.label
? false
@@ -17,7 +17,7 @@ import { until } from "lit/directives/until";
import { fireEvent } from "../../common/dom/fire_event";
import { slugify } from "../../common/string/slugify";
import { debounce } from "../../common/util/debounce";
import { UNAVAILABLE } from "../../data/entity/entity";
import { isUnavailableState } from "../../data/entity/entity";
import type {
MediaPickedEvent,
MediaPlayerBrowseAction,
@@ -290,7 +290,7 @@ export class HaMediaPlayerBrowse extends LitElement {
} else if (
err.code === "entity_not_found" &&
this.entityId &&
this.hass.states[this.entityId]?.state === UNAVAILABLE
isUnavailableState(this.hass.states[this.entityId]?.state)
) {
this._setError({
message: this.hass.localize(
@@ -454,7 +454,7 @@ export class HaTargetPickerItemRow extends LitElement {
}
try {
const entries = await extractFromTarget(
this.hass.callWS,
this.hass,
{
[`${this.type}_id`]: [this.itemId],
},
+1 -1
View File
@@ -62,7 +62,7 @@ export const AREA_CONTROLS_BUTTONS: Record<
};
export const getAreaControlEntities = (
controls: readonly AreaControlDomain[],
controls: AreaControlDomain[],
areaId: string,
excludeEntities: string[] | undefined,
hass: HomeAssistant
-4
View File
@@ -95,7 +95,6 @@ export interface TriggerList {
export interface BaseTrigger {
alias?: string;
comment?: string;
/** @deprecated Use `trigger` instead */
platform?: string;
trigger: string;
@@ -241,7 +240,6 @@ export type Trigger = LegacyTrigger | TriggerList | PlatformTrigger;
interface BaseCondition {
condition: string;
alias?: string;
comment?: string;
enabled?: boolean;
options?: Record<string, unknown>;
}
@@ -609,7 +607,6 @@ export interface AutomationClipboard {
export interface BaseSidebarConfig {
delete: () => void;
close: (focus?: boolean) => void;
editComment: () => void;
}
export interface TriggerSidebarConfig extends BaseSidebarConfig {
@@ -671,7 +668,6 @@ export interface OptionSidebarConfig extends BaseSidebarConfig {
rename: () => void;
duplicate: () => void;
defaultOption?: boolean;
comment?: string;
}
export interface ScriptFieldSidebarConfig extends BaseSidebarConfig {
+2 -4
View File
@@ -818,8 +818,7 @@ const describeLegacyTrigger = (
if (trigger.trigger === "device" && trigger.device_id) {
const config = trigger as DeviceTrigger;
const localized = localizeDeviceAutomationTrigger(
hass.localize,
hass.states,
hass,
entityRegistry,
config
);
@@ -1337,8 +1336,7 @@ const describeLegacyCondition = (
if (condition.condition === "device" && condition.device_id) {
const config = condition as DeviceCondition;
const localized = localizeDeviceAutomationCondition(
hass.localize,
hass.states,
hass,
entityRegistry,
config
);
+2 -2
View File
@@ -6,7 +6,7 @@ import { getColorByIndex } from "../common/color/colors";
import { computeDomain } from "../common/entity/compute_domain";
import { computeStateName } from "../common/entity/compute_state_name";
import type { HomeAssistant } from "../types";
import { UNAVAILABLE } from "./entity/entity";
import { isUnavailableState } from "./entity/entity";
import type { EntityRegistryEntry } from "./entity/entity_registry";
export interface Calendar {
@@ -120,7 +120,7 @@ export const getCalendars = (
.filter(
(eid) =>
computeDomain(eid) === "calendar" &&
hass.states[eid].state !== UNAVAILABLE &&
!isUnavailableState(hass.states[eid].state) &&
hass.entities[eid]?.hidden !== true
)
.sort()
+48 -66
View File
@@ -1,19 +1,17 @@
import type { HassEntities } from "home-assistant-js-websocket";
import { computeStateName } from "../../common/entity/compute_state_name";
import type { LocalizeFunc } from "../../common/translations/localize";
import type { HaFormSchema } from "../../components/ha-form/types";
import type { CallWS } from "../../types";
import type { HomeAssistant } from "../../types";
import type { BaseTrigger } from "../automation";
import { migrateAutomationTrigger } from "../automation";
import type { EntityRegistryEntry } from "../entity/entity_registry";
import {
computeEntityRegistryName,
entityRegistryByEntityId,
entityRegistryById,
} from "../entity/entity_registry";
export interface DeviceAutomation {
alias?: string;
comment?: string;
device_id: string;
domain: string;
entity_id?: string;
@@ -41,47 +39,49 @@ export interface DeviceCapabilities {
extra_fields: HaFormSchema[];
}
export const fetchDeviceActions = (callWS: CallWS, deviceId: string) =>
callWS<DeviceAction[]>({
export const fetchDeviceActions = (hass: HomeAssistant, deviceId: string) =>
hass.callWS<DeviceAction[]>({
type: "device_automation/action/list",
device_id: deviceId,
});
export const fetchDeviceConditions = (callWS: CallWS, deviceId: string) =>
callWS<DeviceCondition[]>({
export const fetchDeviceConditions = (hass: HomeAssistant, deviceId: string) =>
hass.callWS<DeviceCondition[]>({
type: "device_automation/condition/list",
device_id: deviceId,
});
export const fetchDeviceTriggers = (callWS: CallWS, deviceId: string) =>
callWS<DeviceTrigger[]>({
type: "device_automation/trigger/list",
device_id: deviceId,
}).then((triggers) => migrateAutomationTrigger(triggers) as DeviceTrigger[]);
export const fetchDeviceTriggers = (hass: HomeAssistant, deviceId: string) =>
hass
.callWS<DeviceTrigger[]>({
type: "device_automation/trigger/list",
device_id: deviceId,
})
.then((triggers) => migrateAutomationTrigger(triggers) as DeviceTrigger[]);
export const fetchDeviceActionCapabilities = (
callWS: CallWS,
hass: HomeAssistant,
action: DeviceAction
) =>
callWS<DeviceCapabilities>({
hass.callWS<DeviceCapabilities>({
type: "device_automation/action/capabilities",
action,
});
export const fetchDeviceConditionCapabilities = (
callWS: CallWS,
hass: HomeAssistant,
condition: DeviceCondition
) =>
callWS<DeviceCapabilities>({
hass.callWS<DeviceCapabilities>({
type: "device_automation/condition/capabilities",
condition,
});
export const fetchDeviceTriggerCapabilities = (
callWS: CallWS,
hass: HomeAssistant,
trigger: DeviceTrigger
) =>
callWS<DeviceCapabilities>({
hass.callWS<DeviceCapabilities>({
type: "device_automation/trigger/capabilities",
trigger,
});
@@ -184,16 +184,19 @@ const compareEntityIdWithEntityRegId = (
};
const getEntityName = (
localize: LocalizeFunc,
states: HassEntities,
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
entityId: string | undefined
): string => {
if (!entityId) {
return `<${localize("ui.panel.config.automation.editor.unknown_entity")}>`;
return (
"<" +
hass.localize("ui.panel.config.automation.editor.unknown_entity") +
">"
);
}
if (entityId.includes(".")) {
const state = states[entityId];
const state = hass.states[entityId];
if (state) {
return computeStateName(state);
}
@@ -201,35 +204,26 @@ const getEntityName = (
}
const entityReg = entityRegistryById(entityRegistry)[entityId];
if (entityReg) {
if (entityReg.name) {
return entityReg.name;
}
const state = states[entityReg.entity_id];
if (state) {
return computeStateName(state);
}
return entityReg.original_name ?? entityId;
return computeEntityRegistryName(hass, entityReg) || entityId;
}
return `<${localize("ui.panel.config.automation.editor.unknown_entity")}>`;
return (
"<" +
hass.localize("ui.panel.config.automation.editor.unknown_entity") +
">"
);
};
export const localizeDeviceAutomationAction = (
localize: LocalizeFunc,
states: HassEntities,
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
action: DeviceAction
): string =>
localize(
hass.localize(
`component.${action.domain}.device_automation.action_type.${action.type}`,
{
entity_name: getEntityName(
localize,
states,
entityRegistry,
action.entity_id
),
entity_name: getEntityName(hass, entityRegistry, action.entity_id),
subtype: action.subtype
? localize(
? hass.localize(
`component.${action.domain}.device_automation.action_subtype.${action.subtype}`
) || action.subtype
: "",
@@ -237,22 +231,16 @@ export const localizeDeviceAutomationAction = (
) || (action.subtype ? `"${action.subtype}" ${action.type}` : action.type!);
export const localizeDeviceAutomationCondition = (
localize: LocalizeFunc,
states: HassEntities,
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
condition: DeviceCondition
): string =>
localize(
hass.localize(
`component.${condition.domain}.device_automation.condition_type.${condition.type}`,
{
entity_name: getEntityName(
localize,
states,
entityRegistry,
condition.entity_id
),
entity_name: getEntityName(hass, entityRegistry, condition.entity_id),
subtype: condition.subtype
? localize(
? hass.localize(
`component.${condition.domain}.device_automation.condition_subtype.${condition.subtype}`
) || condition.subtype
: "",
@@ -263,22 +251,16 @@ export const localizeDeviceAutomationCondition = (
: condition.type!);
export const localizeDeviceAutomationTrigger = (
localize: LocalizeFunc,
states: HassEntities,
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
trigger: DeviceTrigger
): string =>
localize(
hass.localize(
`component.${trigger.domain}.device_automation.trigger_type.${trigger.type}`,
{
entity_name: getEntityName(
localize,
states,
entityRegistry,
trigger.entity_id
),
entity_name: getEntityName(hass, entityRegistry, trigger.entity_id),
subtype: trigger.subtype
? localize(
? hass.localize(
`component.${trigger.domain}.device_automation.trigger_subtype.${trigger.subtype}`
) || trigger.subtype
: "",
@@ -287,18 +269,18 @@ export const localizeDeviceAutomationTrigger = (
(trigger.subtype ? `"${trigger.subtype}" ${trigger.type}` : trigger.type!);
export const localizeExtraFieldsComputeLabelCallback =
(localize: LocalizeFunc, deviceAutomation: DeviceAutomation) =>
(hass: HomeAssistant, deviceAutomation: DeviceAutomation) =>
// Returns a callback for ha-form to calculate labels per schema object
(schema): string =>
localize(
hass.localize(
`component.${deviceAutomation.domain}.device_automation.extra_fields.${schema.name}`
) || schema.name;
export const localizeExtraFieldsComputeHelperCallback =
(localize: LocalizeFunc, deviceAutomation: DeviceAutomation) =>
(hass: HomeAssistant, deviceAutomation: DeviceAutomation) =>
// Returns a callback for ha-form to calculate helper texts per schema object
(schema): string | undefined =>
localize(
hass.localize(
`component.${deviceAutomation.domain}.device_automation.extra_fields_descriptions.${schema.name}`
);
-3
View File
@@ -148,7 +148,6 @@ export interface GridSourceTypeEnergyPreference {
power_config?: PowerConfig;
cost_adjustment_day: number;
name?: string;
}
export interface SolarSourceTypeEnergyPreference {
@@ -157,7 +156,6 @@ export interface SolarSourceTypeEnergyPreference {
stat_energy_from: string;
stat_rate?: string;
config_entry_solar_forecast: string[] | null;
name?: string;
}
export interface BatterySourceTypeEnergyPreference {
@@ -167,7 +165,6 @@ export interface BatterySourceTypeEnergyPreference {
stat_rate?: string; // always available if power_config is set
power_config?: PowerConfig;
stat_soc?: string;
name?: string;
}
export interface GasSourceTypeEnergyPreference {
type: "gas";
+2
View File
@@ -6,8 +6,10 @@ export const UNKNOWN = "unknown";
export const ON = "on";
export const OFF = "off";
export const UNAVAILABLE_STATES = [UNAVAILABLE, UNKNOWN] as const;
export const OFF_STATES = [UNAVAILABLE, UNKNOWN, OFF] as const;
export const isUnavailableState = arrayLiteralIncludes(UNAVAILABLE_STATES);
export const isOffState = arrayLiteralIncludes(OFF_STATES);
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
+199 -97
View File
@@ -1,15 +1,11 @@
import { atLeastVersion } from "../../common/config/version";
import type { HaFormSchema } from "../../components/ha-form/types";
import type {
CallWS,
HomeAssistant,
HomeAssistantApi,
TranslationDict,
} from "../../types";
import type { HomeAssistant, TranslationDict } from "../../types";
import { supervisorApiCall } from "../supervisor/common";
import type { StoreAddonDetails } from "../supervisor/store";
import type { Supervisor, SupervisorArch } from "../supervisor/supervisor";
import type { HassioResponse } from "./common";
import { extractApiErrorMessage } from "./common";
import { extractApiErrorMessage, hassioApiResultExtractor } from "./common";
export type AddonCapability = Exclude<
keyof TranslationDict["ui"]["panel"]["config"]["apps"]["dashboard"]["capability"],
@@ -147,38 +143,57 @@ export interface HassioAddonSetOptionParams {
}
export const reloadHassioAddons = async (hass: HomeAssistant) => {
await hass.callWS({
type: "supervisor/api",
endpoint: "/addons/reload",
method: "post",
});
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: "/addons/reload",
method: "post",
});
return;
}
await hass.callApi<HassioResponse<void>>("POST", `hassio/addons/reload`);
};
export const fetchHassioAddonsInfo = async (
hass: HomeAssistant
): Promise<HassioAddonsInfo> => {
return hass.callWS({
type: "supervisor/api",
endpoint: "/addons",
method: "get",
});
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return hass.callWS({
type: "supervisor/api",
endpoint: "/addons",
method: "get",
});
}
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioAddonsInfo>>("GET", `hassio/addons`)
);
};
export const fetchHassioAddonInfo = async (
callWS: CallWS,
hass: HomeAssistant,
slug: string
): Promise<HassioAddonDetails> => {
return callWS({
type: "supervisor/api",
endpoint: `/addons/${slug}/info`,
method: "get",
});
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return hass.callWS({
type: "supervisor/api",
endpoint: `/addons/${slug}/info`,
method: "get",
});
}
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioAddonDetails>>(
"GET",
`hassio/addons/${slug}/info`
)
);
};
export const fetchHassioAddonChangelog = async (
api: HomeAssistantApi,
hass: HomeAssistant,
slug: string
) => api.callApi<string>("GET", `hassio/addons/${slug}/changelog`);
) => hass.callApi<string>("GET", `hassio/addons/${slug}/changelog`);
export const fetchHassioAddonLogs = async (hass: HomeAssistant, slug: string) =>
hass.callApi<string>("GET", `hassio/addons/${slug}/logs`);
@@ -189,77 +204,119 @@ export const fetchHassioAddonDocumentation = async (
) => hass.callApi<string>("GET", `hassio/addons/${slug}/documentation`);
export const setHassioAddonOption = async (
callWS: CallWS,
hass: HomeAssistant,
slug: string,
data: HassioAddonSetOptionParams
) => {
const response = await callWS<HassioResponse<any>>({
type: "supervisor/api",
endpoint: `/addons/${slug}/options`,
method: "post",
data,
});
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
const response = await hass.callWS<HassioResponse<any>>({
type: "supervisor/api",
endpoint: `/addons/${slug}/options`,
method: "post",
data,
});
if (response.result === "error") {
throw Error(extractApiErrorMessage(response));
if (response.result === "error") {
throw Error(extractApiErrorMessage(response));
}
return response;
}
return response;
return hass.callApi<HassioResponse<any>>(
"POST",
`hassio/addons/${slug}/options`,
data
);
};
export const validateHassioAddonOption = async (
callWS: CallWS,
hass: HomeAssistant,
slug: string,
data?: any
): Promise<{ message: string; valid: boolean }> => {
return callWS({
type: "supervisor/api",
endpoint: `/addons/${slug}/options/validate`,
method: "post",
data,
});
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return hass.callWS({
type: "supervisor/api",
endpoint: `/addons/${slug}/options/validate`,
method: "post",
data,
});
}
return (
await hass.callApi<HassioResponse<{ message: string; valid: boolean }>>(
"POST",
`hassio/addons/${slug}/options/validate`
)
).data;
};
export const startHassioAddon = async (callWS: CallWS, slug: string) => {
return callWS({
type: "supervisor/api",
endpoint: `/addons/${slug}/start`,
method: "post",
timeout: null,
});
export const startHassioAddon = async (hass: HomeAssistant, slug: string) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return hass.callWS({
type: "supervisor/api",
endpoint: `/addons/${slug}/start`,
method: "post",
timeout: null,
});
}
return hass.callApi<string>("POST", `hassio/addons/${slug}/start`);
};
export const stopHassioAddon = async (callWS: CallWS, slug: string) => {
return callWS({
type: "supervisor/api",
endpoint: `/addons/${slug}/stop`,
method: "post",
timeout: null,
});
export const stopHassioAddon = async (hass: HomeAssistant, slug: string) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return hass.callWS({
type: "supervisor/api",
endpoint: `/addons/${slug}/stop`,
method: "post",
timeout: null,
});
}
return hass.callApi<string>("POST", `hassio/addons/${slug}/stop`);
};
export const setHassioAddonSecurity = async (
callWS: CallWS,
hass: HomeAssistant,
slug: string,
data: HassioAddonSetSecurityParams
) => {
await callWS({
type: "supervisor/api",
endpoint: `/addons/${slug}/security`,
method: "post",
data,
});
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: `/addons/${slug}/security`,
method: "post",
data,
});
return;
}
await hass.callApi<HassioResponse<void>>(
"POST",
`hassio/addons/${slug}/security`,
data
);
};
export const installHassioAddon = async (
callWS: CallWS,
hass: HomeAssistant,
slug: string
): Promise<void> => {
await callWS({
type: "supervisor/api",
endpoint: `/addons/${slug}/install`,
method: "post",
timeout: null,
});
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: `/addons/${slug}/install`,
method: "post",
timeout: null,
});
return;
}
await hass.callApi<HassioResponse<void>>(
"POST",
`hassio/addons/${slug}/install`
);
};
export const updateHassioAddon = async (
@@ -267,37 +324,74 @@ export const updateHassioAddon = async (
slug: string,
backup: boolean
): Promise<void> => {
await hass.callWS({
type: "hassio/update/addon",
addon: slug,
backup: backup,
});
if (atLeastVersion(hass.config.version, 2025, 2, 0)) {
await hass.callWS({
type: "hassio/update/addon",
addon: slug,
backup: backup,
});
return;
}
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: `/store/addons/${slug}/update`,
method: "post",
timeout: null,
data: { backup },
});
return;
}
await hass.callApi<HassioResponse<void>>(
"POST",
`hassio/addons/${slug}/update`,
{ backup }
);
};
export const restartHassioAddon = async (
callWS: CallWS,
hass: HomeAssistant,
slug: string
): Promise<void> => {
await callWS({
type: "supervisor/api",
endpoint: `/addons/${slug}/restart`,
method: "post",
timeout: null,
});
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: `/addons/${slug}/restart`,
method: "post",
timeout: null,
});
return;
}
await hass.callApi<HassioResponse<void>>(
"POST",
`hassio/addons/${slug}/restart`
);
};
export const uninstallHassioAddon = async (
callWS: CallWS,
hass: HomeAssistant,
slug: string,
removeData: boolean
): Promise<void> => {
await callWS({
type: "supervisor/api",
endpoint: `/addons/${slug}/uninstall`,
method: "post",
timeout: null,
data: { remove_config: removeData },
});
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: `/addons/${slug}/uninstall`,
method: "post",
timeout: null,
data: { remove_config: removeData },
});
return;
}
await hass.callApi<HassioResponse<void>>(
"POST",
`hassio/addons/${slug}/uninstall`,
{ remove_config: removeData }
);
};
export const fetchAddonInfo = (
@@ -313,13 +407,21 @@ export const fetchAddonInfo = (
);
export const rebuildLocalAddon = async (
callWS: CallWS,
hass: HomeAssistant,
slug: string
): Promise<void> => {
return callWS<undefined>({
type: "supervisor/api",
endpoint: `/addons/${slug}/rebuild`,
method: "post",
timeout: null,
});
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return hass.callWS<undefined>({
type: "supervisor/api",
endpoint: `/addons/${slug}/rebuild`,
method: "post",
timeout: null,
});
}
return (
await hass.callApi<HassioResponse<void>>(
"POST",
`hassio/addons/${slug}rebuild`
)
).data;
};
+17 -7
View File
@@ -1,4 +1,5 @@
import type { CallWS } from "../../types";
import { atLeastVersion } from "../../common/config/version";
import type { HomeAssistant } from "../../types";
export interface HassioResponse<T> {
data: T;
@@ -45,12 +46,21 @@ export const ignoreSupervisorError = (error): boolean => {
};
export const fetchHassioStats = async (
callWS: CallWS,
hass: HomeAssistant,
container: string
): Promise<HassioStats> => {
return callWS({
type: "supervisor/api",
endpoint: `/${container}/stats`,
method: "get",
});
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return hass.callWS({
type: "supervisor/api",
endpoint: `/${container}/stats`,
method: "get",
});
}
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioStats>>(
"GET",
`hassio/${container}/stats`
)
);
};
+1 -6
View File
@@ -7,18 +7,13 @@ export interface GenericPreview {
state: string;
attributes: Record<string, any>;
error?: string;
domain?: string;
}
export const subscribePreviewGeneric = (
hass: HomeAssistant,
domain: string,
flow_id: string,
flow_type:
| "config_flow"
| "options_flow"
| "config_subentries_flow"
| "repair_flow",
flow_type: "config_flow" | "options_flow" | "config_subentries_flow",
user_input: Record<string, any>,
callback: (preview: GenericPreview) => void
): Promise<UnsubscribeFunc> =>
-3
View File
@@ -36,7 +36,6 @@ export const isMaxMode = arrayLiteralIncludes(MODES_MAX);
export const baseActionStruct = object({
alias: optional(string()),
comment: optional(string()),
continue_on_error: optional(boolean()),
enabled: optional(boolean()),
});
@@ -106,7 +105,6 @@ export interface Field {
interface BaseAction {
alias?: string;
comment?: string;
continue_on_error?: boolean;
enabled?: boolean;
}
@@ -197,7 +195,6 @@ export interface ForEachRepeat extends BaseRepeat {
export interface Option {
alias?: string;
comment?: string;
conditions: string | Condition[];
sequence: Action | Action[];
}
+1 -2
View File
@@ -335,8 +335,7 @@ const tryDescribeAction = <T extends ActionType>(
);
}
const localized = localizeDeviceAutomationAction(
hass.localize,
hass.states,
hass,
entityRegistry,
config
);
+5 -5
View File
@@ -641,7 +641,7 @@ export const expandLabelTarget = (
if (
device.labels.includes(labelId) &&
deviceMeetsTargetSelector(
hass.states,
hass,
Object.values(entities),
device,
targetSelector,
@@ -708,7 +708,7 @@ export const expandAreaTarget = (
if (
device.area_id === areaId &&
deviceMeetsTargetSelector(
hass.states,
hass,
Object.values(entities),
device,
targetSelector,
@@ -768,7 +768,7 @@ export const areaMeetsTargetSelector = (
if (
device.area_id === areaId &&
deviceMeetsTargetSelector(
hass.states,
hass,
Object.values(entities),
device,
targetSelector,
@@ -798,7 +798,7 @@ export const areaMeetsTargetSelector = (
};
export const deviceMeetsTargetSelector = (
states: HomeAssistant["states"],
hass: HomeAssistant,
entityRegistry: EntityRegistryDisplayEntry[] | EntityRegistryEntry[],
device: DeviceRegistryEntry,
targetSelector: TargetSelector,
@@ -822,7 +822,7 @@ export const deviceMeetsTargetSelector = (
(reg) => reg.device_id === device.id
);
return entities.some((entity) => {
const entityState = states[entity.entity_id];
const entityState = hass.states[entity.entity_id];
return entityMeetsTargetSelector(
entityState,
targetSelector,
+3 -3
View File
@@ -3,7 +3,7 @@ import { ensureArray } from "../common/array/ensure-array";
import { computeDomain } from "../common/entity/compute_domain";
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
import type { CallWS, HomeAssistant } from "../types";
import type { HomeAssistant } from "../types";
import type { AreaRegistryEntry } from "./area/area_registry";
import type { FloorComboBoxItem } from "./area_floor_picker";
import type { DevicePickerItem } from "./device/device_picker";
@@ -47,12 +47,12 @@ export interface ExtractFromTargetResultReferenced {
}
export const extractFromTarget = async (
callWS: CallWS,
hass: HomeAssistant,
target: HassServiceTarget,
expandGroup = false,
primaryEntitiesOnly = true
) =>
callWS<ExtractFromTargetResult>({
hass.callWS<ExtractFromTargetResult>({
type: "extract_from_target",
target,
expand_group: expandGroup,
+2 -2
View File
@@ -2,7 +2,7 @@ import { computeDomain } from "../common/entity/compute_domain";
import { computeStateName } from "../common/entity/compute_state_name";
import { stringCompare } from "../common/string/compare";
import type { HomeAssistant, ServiceCallResponse } from "../types";
import { UNAVAILABLE } from "./entity/entity";
import { isUnavailableState } from "./entity/entity";
export interface TodoList {
entity_id: string;
@@ -49,7 +49,7 @@ export const getTodoLists = (
.filter(
(entityId) =>
computeDomain(entityId) === "todo" &&
hass.states[entityId].state !== UNAVAILABLE &&
!isUnavailableState(hass.states[entityId].state) &&
(includeHidden || hass.entities[entityId]?.hidden !== true)
)
.map((entityId) => ({
+2 -9
View File
@@ -29,13 +29,6 @@ import type {
import type { SVGTemplateResult, TemplateResult } from "lit";
import { css, html, svg } from "lit";
import { styleMap } from "lit/directives/style-map";
import {
UNIT_HPA,
UNIT_IN,
UNIT_INHG,
UNIT_KM,
UNIT_MM,
} from "../common/const";
import { supportsFeature } from "../common/entity/supports-feature";
import { round } from "../common/number/round";
import "../components/ha-svg-icon";
@@ -252,12 +245,12 @@ export const getWeatherUnit = (
case "precipitation":
return (
stateObj.attributes.precipitation_unit ||
(lengthUnit === UNIT_KM ? UNIT_MM : UNIT_IN)
(lengthUnit === "km" ? "mm" : "in")
);
case "pressure":
return (
stateObj.attributes.pressure_unit ||
(lengthUnit === UNIT_KM ? UNIT_HPA : UNIT_INHG)
(lengthUnit === "km" ? "hPa" : "inHg")
);
case "apparent_temperature":
case "dew_point":
-1
View File
@@ -24,7 +24,6 @@ interface TemplatePreviewState {
state: string;
attributes: Record<string, any>;
listeners: TemplateListeners;
domain?: string;
}
interface TemplatePreviewError {
-258
View File
@@ -1,258 +0,0 @@
import type { HomeAssistant } from "../types";
export type ZwaveCredentialType =
| "pin_code"
| "password"
| "rfid_code"
| "ble"
| "nfc"
| "uwb"
| "eye_biometric"
| "face_biometric"
| "finger_biometric"
| "hand_biometric"
| "unspecified_biometric"
| "desfire";
export const ENTERABLE_ZWAVE_CREDENTIAL_TYPES: readonly ZwaveCredentialType[] =
["pin_code", "password"];
// UI surfaces only general + disposable to stay aligned with Matter lock UX.
// Other types (programming, duress, non_access, remote_only, expiring) are
// defined in translations for display in existing-user rows, but are not
// selectable here.
export const SIMPLE_USER_TYPES: readonly string[] = ["general", "disposable"];
// Fallback bounds when a lock advertises an enterable type without
// per-type min/max — values mirror Z-Wave spec defaults.
export const DEFAULT_CREDENTIAL_MIN_LENGTH = 4;
export const DEFAULT_CREDENTIAL_MAX_LENGTH = 10;
export type CredentialErrorCode =
| "required"
| "length"
| "pin_digits_only"
| "";
export const enterableCredentialTypes = (
capabilities: ZwaveCredentialCapabilities
): ZwaveCredentialType[] => {
if (!capabilities.supported_credential_types) {
return [];
}
return ENTERABLE_ZWAVE_CREDENTIAL_TYPES.filter(
(type) => type in capabilities.supported_credential_types
);
};
export const compatibleUserTypes = (
capabilities: ZwaveCredentialCapabilities
): string[] => {
const supported = capabilities.supported_user_types ?? [];
return SIMPLE_USER_TYPES.filter((t) => supported.includes(t));
};
export const canAddZwaveUser = (
capabilities: ZwaveCredentialCapabilities
): boolean =>
enterableCredentialTypes(capabilities).length > 0 &&
compatibleUserTypes(capabilities).length > 0;
export const getCredentialError = (
data: string,
type: ZwaveCredentialType | "",
capability: ZwaveCredentialTypeCapability | undefined
): CredentialErrorCode => {
if (!data) {
return "required";
}
const minLength = capability?.min_length ?? DEFAULT_CREDENTIAL_MIN_LENGTH;
const maxLength = capability?.max_length ?? DEFAULT_CREDENTIAL_MAX_LENGTH;
if (data.length < minLength || data.length > maxLength) {
return "length";
}
if (type === "pin_code" && !/^\d+$/.test(data)) {
return "pin_digits_only";
}
return "";
};
export interface ZwaveCredentialTypeCapability {
num_slots: number;
min_length: number;
max_length: number;
supports_learn: boolean;
}
export interface ZwaveCredentialCapabilities {
supports_user_management: boolean;
max_users: number;
supported_user_types: string[];
max_user_name_length: number;
supported_credential_rules: string[];
supported_credential_types: Partial<
Record<ZwaveCredentialType, ZwaveCredentialTypeCapability>
>;
}
export interface ZwaveCredential {
type: ZwaveCredentialType;
slot: number;
}
export interface ZwaveUser {
user_id: number;
user_name: string | null;
active: boolean;
user_type: string;
credential_rule: string | null;
credentials: ZwaveCredential[];
}
export interface ZwaveUsersResponse {
max_users: number;
users: ZwaveUser[];
}
export interface SetZwaveUserParams {
user_id?: number;
user_name?: string | null;
user_type?: string;
credential_rule?: string;
active?: boolean;
}
export interface SetZwaveUserResult {
user_id: number;
}
export interface SetZwaveCredentialParams {
user_id: number;
credential_type: ZwaveCredentialType;
credential_data: string;
credential_slot?: number;
}
export interface SetZwaveCredentialResult {
credential_slot: number;
user_id: number;
}
export interface DeleteZwaveCredentialParams {
user_id: number;
credential_type: ZwaveCredentialType;
credential_slot: number;
}
// The Z-Wave services key their response by entity_id to support multi-target
// calls. The frontend only ever calls them with a single lock entity, so we
// expect exactly that key. Anything else (no response, mismatched key) is a
// backend contract violation — surface it as a localized error rather than
// letting `cannot read property of undefined` bubble up.
const unwrapEntityResponse = <T>(
hass: HomeAssistant,
response: Record<string, T> | undefined,
entity_id: string
): T => {
const value = response?.[entity_id];
if (value === undefined) {
throw new Error(
hass.localize(
"ui.panel.config.zwave_js.credentials.errors.empty_response"
)
);
}
return value;
};
const callCredentialService = async <T>(
hass: HomeAssistant,
service: string,
entity_id: string,
params: Record<string, unknown> = {}
): Promise<T> => {
// notifyOnError=false — callers surface errors in-dialog instead.
const result = await hass.callService<Record<string, T>>(
"zwave_js",
service,
params,
{ entity_id },
false,
true
);
return unwrapEntityResponse(hass, result.response, entity_id);
};
export const getZwaveCredentialCapabilities = (
hass: HomeAssistant,
entity_id: string
): Promise<ZwaveCredentialCapabilities> =>
callCredentialService<ZwaveCredentialCapabilities>(
hass,
"get_credential_capabilities",
entity_id
);
export const getZwaveUsers = (
hass: HomeAssistant,
entity_id: string
): Promise<ZwaveUsersResponse> =>
callCredentialService<ZwaveUsersResponse>(hass, "get_users", entity_id);
export const setZwaveUser = async (
hass: HomeAssistant,
entity_id: string,
params: SetZwaveUserParams
): Promise<SetZwaveUserResult> => {
// notifyOnError=false — caller surfaces errors in-dialog instead.
const result = await hass.callService<Record<string, SetZwaveUserResult>>(
"zwave_js",
"set_user",
params,
{ entity_id },
false,
true
);
return unwrapEntityResponse(hass, result.response, entity_id);
};
export const deleteZwaveUser = (
hass: HomeAssistant,
entity_id: string,
user_id: number
) =>
hass.callService(
"zwave_js",
"delete_user",
{ user_id },
{ entity_id },
false
);
export const deleteZwaveAllUsers = (hass: HomeAssistant, entity_id: string) =>
hass.callService("zwave_js", "delete_all_users", {}, { entity_id }, false);
export const setZwaveCredential = async (
hass: HomeAssistant,
entity_id: string,
params: SetZwaveCredentialParams
): Promise<SetZwaveCredentialResult> => {
// notifyOnError=false — caller surfaces errors in-dialog instead.
const result = await hass.callService<
Record<string, SetZwaveCredentialResult>
>("zwave_js", "set_credential", params, { entity_id }, false, true);
return unwrapEntityResponse(hass, result.response, entity_id);
};
export const deleteZwaveCredential = (
hass: HomeAssistant,
entity_id: string,
params: DeleteZwaveCredentialParams
) =>
hass.callService(
"zwave_js",
"delete_credential",
params,
{ entity_id },
false
);
@@ -18,7 +18,7 @@ import "../../../components/ha-slider";
import "../../../components/ha-time-input";
import "../../../components/input/ha-input";
import { isTiltOnly } from "../../../data/cover";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
import { isUnavailableState, UNAVAILABLE } from "../../../data/entity/entity";
import type { ImageEntity } from "../../../data/image";
import { computeImageUrl } from "../../../data/image";
import "../../../panels/lovelace/components/hui-timestamp-display";
@@ -108,13 +108,14 @@ class EntityPreviewRow extends LitElement {
private _renderEntityState(stateObj: HassEntity): TemplateResult | string {
const domain = stateObj.entity_id.split(".", 1)[0];
const disabled = stateObj.state === UNAVAILABLE;
const noValue =
stateObj.state === UNAVAILABLE || stateObj.state === UNKNOWN;
if (domain === "button") {
return html`
<ha-button appearance="plain" size="small" .disabled=${disabled}>
<ha-button
appearance="plain"
size="small"
.disabled=${isUnavailableState(stateObj.state)}
>
${this.hass.localize("ui.card.button.press")}
</ha-button>
`;
@@ -150,15 +151,19 @@ class EntityPreviewRow extends LitElement {
return html`
<ha-date-input
.locale=${this.hass.locale}
.disabled=${disabled}
.value=${noValue ? undefined : stateObj.state}
.disabled=${isUnavailableState(stateObj.state)}
.value=${isUnavailableState(stateObj.state)
? undefined
: stateObj.state}
>
</ha-date-input>
`;
}
if (domain === "datetime") {
const dateObj = noValue ? undefined : new Date(stateObj.state);
const dateObj = isUnavailableState(stateObj.state)
? undefined
: new Date(stateObj.state);
const time = dateObj ? format(dateObj, "HH:mm:ss") : undefined;
const date = dateObj ? format(dateObj, "yyyy-MM-dd") : undefined;
return html`
@@ -167,12 +172,12 @@ class EntityPreviewRow extends LitElement {
.label=${computeStateName(stateObj)}
.locale=${this.hass.locale}
.value=${date}
.disabled=${disabled}
.disabled=${isUnavailableState(stateObj.state)}
>
</ha-date-input>
<ha-time-input
.value=${time}
.disabled=${disabled}
.disabled=${isUnavailableState(stateObj.state)}
.locale=${this.hass.locale}
></ha-time-input>
</div>
@@ -182,7 +187,7 @@ class EntityPreviewRow extends LitElement {
if (domain === "event") {
return html`
<div class="when">
${noValue
${isUnavailableState(stateObj.state)
? this.hass.formatEntityState(stateObj)
: html`<hui-timestamp-display
.hass=${this.hass}
@@ -191,7 +196,7 @@ class EntityPreviewRow extends LitElement {
></hui-timestamp-display>`}
</div>
<div class="what">
${noValue
${isUnavailableState(stateObj.state)
? nothing
: this.hass.formatEntityAttributeValue(stateObj, "event_type")}
</div>
@@ -201,7 +206,9 @@ class EntityPreviewRow extends LitElement {
const toggleDomains = ["fan", "light", "remote", "siren", "switch"];
if (toggleDomains.includes(domain)) {
const showToggle =
stateObj.state === "on" || stateObj.state === "off" || noValue;
stateObj.state === "on" ||
stateObj.state === "off" ||
isUnavailableState(stateObj.state);
return html`
${showToggle
? html`
@@ -234,7 +241,7 @@ class EntityPreviewRow extends LitElement {
if (domain === "lock") {
return html`
<ha-button
.disabled=${disabled}
.disabled=${isUnavailableState(stateObj.state)}
class="text-content"
appearance="plain"
size="small"
@@ -259,7 +266,7 @@ class EntityPreviewRow extends LitElement {
<div class="numberflex">
<ha-slider
labeled
.disabled=${disabled}
.disabled=${stateObj.state === UNAVAILABLE}
.step=${Number(stateObj.attributes.step)}
.min=${Number(stateObj.attributes.min)}
.max=${Number(stateObj.attributes.max)}
@@ -273,7 +280,7 @@ class EntityPreviewRow extends LitElement {
: html`<div class="numberflex numberstate">
<ha-input
auto-validate
.disabled=${disabled}
.disabled=${stateObj.state === UNAVAILABLE}
pattern="[0-9]+([\\.][0-9]+)?"
.step=${Number(stateObj.attributes.step)}
.min=${Number(stateObj.attributes.min)}
@@ -296,7 +303,7 @@ class EntityPreviewRow extends LitElement {
<ha-select
.label=${computeStateName(stateObj)}
.value=${stateObj.state}
.disabled=${disabled}
.disabled=${stateObj.state === UNAVAILABLE}
.options=${stateObj.attributes.options?.map((option) => ({
value: option,
label: this.hass!.formatEntityState(stateObj, option),
@@ -310,7 +317,7 @@ class EntityPreviewRow extends LitElement {
const showSensor =
SENSOR_TIMESTAMP_DEVICE_CLASSES.includes(
stateObj.attributes.device_class
) && !noValue;
) && !isUnavailableState(stateObj.state);
return html`
${showSensor
? html`
@@ -332,7 +339,7 @@ class EntityPreviewRow extends LitElement {
return html`
<ha-input
.label=${computeStateName(stateObj)}
.disabled=${disabled}
.disabled=${isUnavailableState(stateObj.state)}
.value=${stateObj.state}
.minlength=${stateObj.attributes.min}
.maxlength=${stateObj.attributes.max}
@@ -347,9 +354,11 @@ class EntityPreviewRow extends LitElement {
if (domain === "time") {
return html`
<ha-time-input
.value=${noValue ? undefined : stateObj.state}
.value=${isUnavailableState(stateObj.state)
? undefined
: stateObj.state}
.locale=${this.hass.locale}
.disabled=${disabled}
.disabled=${isUnavailableState(stateObj.state)}
></ha-time-input>
`;
}
@@ -357,7 +366,7 @@ class EntityPreviewRow extends LitElement {
if (domain === "weather") {
return html`
<div>
${noValue ||
${isUnavailableState(stateObj.state) ||
stateObj.attributes.temperature === undefined ||
stateObj.attributes.temperature === null
? this.hass.formatEntityState(stateObj)
@@ -65,7 +65,7 @@ export class FlowPreviewGeneric extends LitElement {
}
const now = new Date().toISOString();
this._preview = {
entity_id: `${preview.domain ?? this.stepId}.___flow_preview___`,
entity_id: `${this.stepId}.___flow_preview___`,
last_changed: now,
last_updated: now,
context: { id: "", parent_id: null, user_id: null },
@@ -85,8 +85,7 @@ export class FlowPreviewGeneric extends LitElement {
if (
this.flowType !== "config_flow" &&
this.flowType !== "options_flow" &&
this.flowType !== "config_subentries_flow" &&
this.flowType !== "repair_flow"
this.flowType !== "config_subentries_flow"
) {
return;
}
@@ -130,7 +130,7 @@ class FlowPreviewTemplate extends LitElement {
this._listeners = preview.listeners;
const now = new Date().toISOString();
this._preview = {
entity_id: `${preview.domain ?? this.stepId}.___flow_preview___`,
entity_id: `${this.stepId}.___flow_preview___`,
last_changed: now,
last_updated: now,
context: { id: "", parent_id: null, user_id: null },
+3 -17
View File
@@ -9,8 +9,6 @@ import "../../components/ha-dialog";
import "../../components/ha-dialog-footer";
import "../../components/ha-dialog-header";
import "../../components/ha-svg-icon";
import "../../components/ha-textarea";
import type { HaTextArea } from "../../components/ha-textarea";
import "../../components/input/ha-input";
import type { HaInput } from "../../components/input/ha-input";
import type { HomeAssistant } from "../../types";
@@ -30,7 +28,7 @@ class DialogBox extends LitElement {
@state() private _validInput = true;
@query("ha-input, ha-textarea") private _textField?: HaInput | HaTextArea;
@query("ha-input") private _textField?: HaInput;
private _closePromise?: Promise<void>;
@@ -111,7 +109,7 @@ class DialogBox extends LitElement {
</ha-dialog-header>
<div id="dialog-box-description">
${this._params.text ? html` <p>${this._params.text}</p> ` : ""}
${this._params.prompt && !this._params.multiline
${this._params.prompt
? html`
<ha-input
autofocus
@@ -133,19 +131,7 @@ class DialogBox extends LitElement {
: nothing}
</ha-input>
`
: this._params.prompt && this._params.multiline
? html`
<ha-textarea
resize="auto"
autofocus
.value=${this._params.defaultValue}
.placeholder=${this._params.placeholder}
.label=${this._params.inputLabel}
.disabled=${this._loading}
@input=${this._validateInput}
></ha-textarea>
`
: nothing}
: nothing}
</div>
<ha-dialog-footer slot="footer">
${confirmPrompt
-1
View File
@@ -33,7 +33,6 @@ export interface PromptDialogParams extends BaseDialogBoxParams {
inputMin?: number | string;
inputMax?: number | string;
action?: (value?: string) => Promise<void>;
multiline?: boolean;
}
export interface DialogBoxParams
-128
View File
@@ -1,128 +0,0 @@
import { navigate } from "../../common/navigate";
import type { LocalizeFunc } from "../../common/translations/localize";
import { createSearchParam } from "../../common/url/search-params";
import type { SingleHassServiceTarget } from "../../data/target";
import {
ADD_AUTOMATION_ELEMENT_DEVICE_TARGET_PARAM,
ADD_AUTOMATION_ELEMENT_QUERY_PARAM,
ADD_AUTOMATION_ELEMENT_ENTITY_TARGET_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;
interface BaseEntityAddToAction {
/** Whether the action is enabled and can be selected. */
enabled: boolean;
/** Translated name of the action */
name: string;
/** Optional translated description of the action */
description?: string;
/** MDI icon name (e.g., "mdi:car") */
icon: string;
}
export interface DefaultEntityAddToAction extends BaseEntityAddToAction {
/** Type of action handled in the frontend */
type: "default";
/** Stable key used to resolve the action handler */
key: AddToActionKey;
}
export interface ExternalEntityAddToAction extends BaseEntityAddToAction {
/** Type of action. External is handled by external apps instead of in the frontend */
type: "external";
/** Opaque payload for external action handling */
payload?: string;
}
export type EntityAddToAction =
| DefaultEntityAddToAction
| ExternalEntityAddToAction;
export type EntityAddToActions = EntityAddToAction[];
interface ActionDefinition {
translation_key: AddToActionKey;
icon: string;
}
export const DEFAULT_ACTION_DEFS: ActionDefinition[] = [
{
translation_key: "automation_trigger",
icon: "mdi:robot-outline",
},
{
translation_key: "automation_condition",
icon: "mdi:playlist-check",
},
{
translation_key: "automation_action",
icon: "mdi:play-circle-outline",
},
{
translation_key: "script_action",
icon: "mdi:script-text-outline",
},
];
export const getDefaultAddToActions = (
states: HomeAssistant["states"],
localize: LocalizeFunc,
formatEntityName: HomeAssistant["formatEntityName"],
entityId: string
): 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,
}
),
icon: def.icon,
})
);
/** Handler for adding a target to an automation/script. */
export function addToActionHandler(
key: AddToActionKey,
target: SingleHassServiceTarget
): Promise<boolean> {
const searchParams: Record<string, string> = {};
if (target.entity_id) {
searchParams[ADD_AUTOMATION_ELEMENT_ENTITY_TARGET_PARAM] = target.entity_id;
} else if (target.device_id) {
searchParams[ADD_AUTOMATION_ELEMENT_DEVICE_TARGET_PARAM] = target.device_id;
}
const params = (addElement: string) =>
`?${createSearchParam({
[ADD_AUTOMATION_ELEMENT_QUERY_PARAM]: addElement,
...searchParams,
})}`;
switch (key) {
case "automation_trigger":
return navigate(`/config/automation/edit/new${params("trigger")}`);
case "automation_condition":
return navigate(`/config/automation/edit/new${params("condition")}`);
case "automation_action":
return navigate(`/config/automation/edit/new${params("action")}`);
case "script_action":
return navigate(`/config/script/edit/new${params("action")}`);
default:
return Promise.reject(new Error(`Unknown action key ${key}`));
}
}
@@ -3,7 +3,7 @@ import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../components/ha-absolute-time";
import "../../../components/ha-relative-time";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
import { isUnavailableState } from "../../../data/entity/entity";
import type { LightEntity } from "../../../data/light";
import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../../../data/sensor";
import "../../../panels/lovelace/components/hui-timestamp-display";
@@ -24,8 +24,7 @@ export class HaMoreInfoStateHeader extends LitElement {
private _localizeState(): TemplateResult | string {
if (
this.stateObj.attributes.device_class === SENSOR_DEVICE_CLASS_TIMESTAMP &&
this.stateObj.state !== UNAVAILABLE &&
this.stateObj.state !== UNKNOWN
!isUnavailableState(this.stateObj.state)
) {
return html`
<hui-timestamp-display
@@ -4,7 +4,7 @@ import { customElement, property } from "lit/decorators";
import "../../../components/ha-button";
import "../../../components/ha-relative-time";
import { triggerAutomationActions } from "../../../data/automation";
import { UNAVAILABLE } from "../../../data/entity/entity";
import { isUnavailableState } from "../../../data/entity/entity";
import type { HomeAssistant } from "../../../types";
@customElement("more-info-automation")
@@ -34,7 +34,7 @@ class MoreInfoAutomation extends LitElement {
appearance="plain"
size="small"
@click=${this._runActions}
.disabled=${this.stateObj!.state === UNAVAILABLE}
.disabled=${isUnavailableState(this.stateObj!.state)}
>
${this.hass.localize("ui.card.automation.trigger")}
</ha-button>
@@ -2,7 +2,7 @@ import type { HassEntity } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../components/ha-button";
import { UNAVAILABLE } from "../../../data/entity/entity";
import { isUnavailableState } from "../../../data/entity/entity";
import type { HomeAssistant } from "../../../types";
@customElement("more-info-counter")
@@ -16,7 +16,7 @@ class MoreInfoCounter extends LitElement {
return nothing;
}
const disabled = this.stateObj.state === UNAVAILABLE;
const disabled = isUnavailableState(this.stateObj.state);
return html`
<div class="actions">
@@ -4,7 +4,7 @@ import { customElement, property } from "lit/decorators";
import "../../../components/ha-date-input";
import "../../../components/ha-time-input";
import { setDateValue } from "../../../data/date";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
import { isUnavailableState, UNAVAILABLE } from "../../../data/entity/entity";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
@customElement("more-info-date")
@@ -21,9 +21,10 @@ class MoreInfoDate extends LitElement {
return html`
<ha-date-input
.locale=${this.hass.locale}
.value=${this.stateObj.state === UNKNOWN
.value=${isUnavailableState(this.stateObj.state)
? undefined
: this.stateObj.state}
.disabled=${this.stateObj.state === UNAVAILABLE}
@value-changed=${this._dateChanged}
>
</ha-date-input>
@@ -5,7 +5,7 @@ import { customElement, property } from "lit/decorators";
import "../../../components/ha-date-input";
import "../../../components/ha-time-input";
import { setDateTimeValue } from "../../../data/datetime";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
import { isUnavailableState, UNAVAILABLE } from "../../../data/entity/entity";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
@customElement("more-info-datetime")
@@ -19,22 +19,23 @@ class MoreInfoDatetime extends LitElement {
return nothing;
}
const dateObj =
this.stateObj.state === UNKNOWN
? undefined
: new Date(this.stateObj.state);
const dateObj = isUnavailableState(this.stateObj.state)
? undefined
: new Date(this.stateObj.state);
const time = dateObj ? format(dateObj, "HH:mm:ss") : undefined;
const date = dateObj ? format(dateObj, "yyyy-MM-dd") : undefined;
return html`<ha-date-input
.locale=${this.hass.locale}
.value=${date}
.disabled=${this.stateObj.state === UNAVAILABLE}
@value-changed=${this._dateChanged}
>
</ha-date-input>
<ha-time-input
.value=${time}
.locale=${this.hass.locale}
.disabled=${this.stateObj.state === UNAVAILABLE}
@value-changed=${this._timeChanged}
@click=${this._stopEventPropagation}
></ha-time-input>`;
@@ -200,13 +200,12 @@ class MoreInfoMediaPlayer extends LitElement {
protected _renderSourceControl() {
if (
!this.stateObj ||
!supportsFeature(this.stateObj, MediaPlayerEntityFeature.SELECT_SOURCE)
!supportsFeature(this.stateObj, MediaPlayerEntityFeature.SELECT_SOURCE) ||
!this.stateObj.attributes.source_list?.length
) {
return nothing;
}
const sourceList = this.stateObj.attributes.source_list || [];
return html`<ha-tooltip for="source-button">
${this.hass.localize(`ui.card.media_player.source`)}
</ha-tooltip>
@@ -218,7 +217,7 @@ class MoreInfoMediaPlayer extends LitElement {
.path=${mdiLoginVariant}
>
</ha-icon-button>
${sourceList.map(
${this.stateObj.attributes.source_list!.map(
(source) =>
html`<ha-dropdown-item
.value=${source}
@@ -11,7 +11,7 @@ import "../../../components/ha-control-button-group";
import "../../../components/ha-markdown";
import "../../../components/ha-relative-time";
import "../../../components/ha-service-control";
import { UNAVAILABLE } from "../../../data/entity/entity";
import { isUnavailableState } from "../../../data/entity/entity";
import type { ExtEntityRegistryEntry } from "../../../data/entity/entity_registry";
import type { ScriptEntity } from "../../../data/script";
import {
@@ -141,7 +141,7 @@ class MoreInfoScript extends LitElement {
<ha-control-button
class="run-button"
@click=${this._runScript}
.disabled=${stateObj.state === UNAVAILABLE || !this._canRun()}
.disabled=${isUnavailableState(stateObj.state) || !this._canRun()}
>
<ha-svg-icon .path=${mdiPlay}></ha-svg-icon>
${this.hass!.localize("ui.card.script.run")}
@@ -3,7 +3,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../components/ha-date-input";
import "../../../components/ha-time-input";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
import { isUnavailableState, UNAVAILABLE } from "../../../data/entity/entity";
import { setTimeValue } from "../../../data/time";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
@@ -20,10 +20,11 @@ class MoreInfoTime extends LitElement {
return html`
<ha-time-input
.value=${this.stateObj.state === UNKNOWN
.value=${isUnavailableState(this.stateObj.state)
? undefined
: this.stateObj.state}
.locale=${this.hass.locale}
.disabled=${this.stateObj.state === UNAVAILABLE}
@value-changed=${this._timeChanged}
@click=${this._stopEventPropagation}
></ha-time-input>
@@ -15,7 +15,7 @@ import "../../../components/item/ha-row-item";
import "../../../components/progress/ha-progress-bar";
import type { BackupConfig } from "../../../data/backup";
import { fetchBackupConfig } from "../../../data/backup";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
import { isUnavailableState } from "../../../data/entity/entity";
import type { EntitySources } from "../../../data/entity/entity_sources";
import { fetchEntitySourcesWithCache } from "../../../data/entity/entity_sources";
import { getSupervisorUpdateConfig } from "../../../data/supervisor/update";
@@ -176,8 +176,7 @@ class MoreInfoUpdate extends LitElement {
if (
!this.hass ||
!this.stateObj ||
this.stateObj.state === UNAVAILABLE ||
this.stateObj.state === UNKNOWN
isUnavailableState(this.stateObj.state)
) {
return nothing;
}
+55 -123
View File
@@ -5,18 +5,14 @@ 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 type {
ExternalEntityAddToAction,
ExternalEntityAddToActions,
} from "../../external_app/external_messaging";
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 {
type EntityAddToAction,
type EntityAddToActions,
addToActionHandler,
getDefaultAddToActions,
} from "./add-to";
@customElement("ha-more-info-add-to")
export class HaMoreInfoAddTo extends LitElement {
@@ -24,112 +20,54 @@ export class HaMoreInfoAddTo extends LitElement {
@property({ attribute: false }) public entityId!: string;
@state() private _defaultActions: EntityAddToActions = [];
@state() private _externalActions: EntityAddToActions = [];
@state() private _externalActions?: ExternalEntityAddToActions = {
actions: [],
};
@state() private _loading = true;
private async _loadActions() {
this._defaultActions = getDefaultAddToActions(
this.hass.states,
this.hass.localize,
this.hass.formatEntityName,
this.entityId
);
this._externalActions = [];
private async _loadExternalActions() {
if (this.hass.auth.external?.config.hasEntityAddTo) {
try {
const response =
await this.hass.auth.external?.sendMessage<"entity/add_to/get_actions">(
{
type: "entity/add_to/get_actions",
payload: { entity_id: this.entityId },
}
);
if (response?.actions) {
this._externalActions = response.actions.map((action) => ({
type: "external",
enabled: action.enabled,
name: action.name,
description: action.details,
icon: action.mdi_icon,
payload: action.app_payload,
}));
}
} catch (err: unknown) {
// eslint-disable-next-line no-console
console.warn("Failed to fetch add to actions", err);
}
this._externalActions =
await this.hass.auth.external?.sendMessage<"entity/add_to/get_actions">(
{
type: "entity/add_to/get_actions",
payload: { entity_id: this.entityId },
}
);
}
}
private async _actionSelected(
ev: HASSDomCurrentTargetEvent<
HaListItemButton & {
action: EntityAddToAction;
}
>
) {
const action = ev.currentTarget.action;
private async _actionSelected(ev: CustomEvent) {
const action = (ev.currentTarget as any)
.action as ExternalEntityAddToAction;
if (!action.enabled) {
return;
}
if (action.type === "external") {
try {
if (!action.payload) {
throw new Error("Missing external action payload");
}
this.hass.auth.external!.fireMessage({
type: "entity/add_to",
payload: {
entity_id: this.entityId,
app_payload: action.payload,
},
});
fireEvent(this, "add-to-action-selected");
} catch (err: unknown) {
showToast(this, {
message: this.hass.localize(
"ui.dialogs.more_info_control.add_to.action_failed",
{
error: err instanceof Error ? err.message : String(err),
}
),
});
}
return;
try {
await this.hass.auth.external!.fireMessage({
type: "entity/add_to",
payload: {
entity_id: this.entityId,
app_payload: action.app_payload,
},
});
fireEvent(this, "add-to-action-selected");
} catch (err: any) {
showToast(this, {
message: this.hass.localize(
"ui.dialogs.more_info_control.add_to.action_failed",
{
error: err.message || err,
}
),
});
}
if (action.type !== "default") {
return;
}
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();
await this._loadExternalActions();
this._loading = false;
}
@@ -142,7 +80,7 @@ export class HaMoreInfoAddTo extends LitElement {
`;
}
if (!this._defaultActions.length && !this._externalActions.length) {
if (!this._externalActions?.actions.length) {
return html`
<ha-alert alert-type="info">
${this.hass.localize(
@@ -154,27 +92,30 @@ export class HaMoreInfoAddTo extends LitElement {
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>
${this._externalActions?.actions.map(
(action) => html`
<ha-list-item-button
.disabled=${!action.enabled}
.action=${action}
@click=${this._actionSelected}
>
<ha-icon slot="start" .icon=${action.mdi_icon}></ha-icon>
<span slot="headline">${action.name}</span>
${action.details
? html`<span slot="supporting-text">${action.details}</span>`
: nothing}
</ha-list-item-button>
`
: nothing}
)}
</ha-list-base>
`;
}
static styles = css`
:host {
display: block;
padding: var(--ha-space-3) 0 var(--ha-space-4);
padding: var(--ha-space-2) var(--ha-space-6) var(--ha-space-6)
var(--ha-space-6);
}
.loading {
@@ -184,15 +125,6 @@ export class HaMoreInfoAddTo extends LitElement {
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;
+20 -46
View File
@@ -1,4 +1,3 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import {
mdiBackupRestore,
mdiChartBoxOutline,
@@ -18,13 +17,13 @@ import {
} from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { LitElement, css, html, nothing } from "lit";
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 { dynamicElement } from "../../common/dom/dynamic-element-directive";
import type { HASSDomEvent } from "../../common/dom/fire_event";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { computeAreaName } from "../../common/entity/compute_area_name";
@@ -58,12 +57,10 @@ import {
getExtendedEntityRegistryEntry,
updateEntityRegistryEntry,
} from "../../data/entity/entity_registry";
import { subscribeLabFeature } from "../../data/labs";
import type { ItemType } from "../../data/search";
import { SearchableDomains } from "../../data/search";
import { getSensorNumericDeviceClasses } from "../../data/sensor";
import { ScrollableFadeMixin } from "../../mixins/scrollable-fade-mixin";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import {
haStyleDialog,
haStyleDialogFixedTop,
@@ -73,12 +70,12 @@ import "../../state-summary/state-card-content";
import type { HomeAssistant } from "../../types";
import { showConfirmationDialog } from "../generic/show-dialog-box";
import {
computeShowHistoryComponent,
computeShowLogBookComponent,
DOMAINS_WITH_MORE_INFO,
EDITABLE_DOMAINS_WITH_ID,
EDITABLE_DOMAINS_WITH_UNIQUE_ID,
type MoreInfoView,
computeShowHistoryComponent,
computeShowLogBookComponent,
} from "./const";
import "./controls/more-info-default";
import type { FavoritesDialogContext } from "./favorites";
@@ -120,9 +117,7 @@ declare global {
const DEFAULT_VIEW: MoreInfoView = "info";
@customElement("ha-more-info-dialog")
export class MoreInfoDialog extends SubscribeMixin(
ScrollableFadeMixin(LitElement)
) {
export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public large = false;
@@ -161,8 +156,6 @@ export class MoreInfoDialog extends SubscribeMixin(
@state() private _sensorNumericDeviceClasses?: string[] = [];
@state() private _newTriggersAndConditions = false;
protected scrollFadeThreshold = 24;
protected get scrollableElement(): HTMLElement | null {
@@ -261,24 +254,7 @@ export class MoreInfoDialog extends SubscribeMixin(
}
private _shouldShowAddEntityTo(): boolean {
// When new_triggers_conditions labs feature is promoted, this whole check can be removed.
return (
this._newTriggersAndConditions ||
!!this.hass.auth.external?.config.hasEntityAddTo
);
}
protected hassSubscribe() {
return [
subscribeLabFeature(
this.hass.connection,
"automation",
"new_triggers_conditions",
(feature) => {
this._newTriggersAndConditions = feature.enabled;
}
),
];
return !!this.hass.auth.external?.config.hasEntityAddTo;
}
private _getDeviceId(): string | null {
@@ -704,21 +680,6 @@ export class MoreInfoDialog extends SubscribeMixin(
.path=${mdiDotsVertical}
></ha-icon-button>
${this._shouldShowAddEntityTo()
? html`
<ha-dropdown-item value="add_to">
<ha-svg-icon
slot="icon"
.path=${mdiPlusBoxMultipleOutline}
></ha-svg-icon>
${this.hass.localize(
"ui.dialogs.more_info_control.add_to.title"
)}
</ha-dropdown-item>
<wa-divider></wa-divider>
`
: nothing}
${supportsFavorites
? html`
<ha-dropdown-item value="toggle_edit">
@@ -808,6 +769,19 @@ export class MoreInfoDialog extends SubscribeMixin(
"ui.dialogs.more_info_control.details"
)}
</ha-dropdown-item>
${this._shouldShowAddEntityTo()
? html`
<ha-dropdown-item value="add_to">
<ha-svg-icon
slot="icon"
.path=${mdiPlusBoxMultipleOutline}
></ha-svg-icon>
${this.hass.localize(
"ui.dialogs.more_info_control.add_entity_to"
)}
</ha-dropdown-item>
`
: nothing}
</ha-dropdown>
`
: !__DEMO__ && this._shouldShowAddEntityTo()
@@ -815,7 +789,7 @@ export class MoreInfoDialog extends SubscribeMixin(
<ha-icon-button
slot="headerActionItems"
.label=${this.hass.localize(
"ui.dialogs.more_info_control.add_to.title"
"ui.dialogs.more_info_control.add_entity_to"
)}
.path=${mdiPlusBoxMultipleOutline}
@click=${this._goToAddEntityTo}
@@ -199,13 +199,13 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement {
this._detailState = this.hass.localize(
`ui.panel.config.voice_assistants.satellite_wizard.local.state.installing_${this._ttsProviderName}`
);
await installHassioAddon(this.hass.callWS, this._ttsAddonName);
await installHassioAddon(this.hass, this._ttsAddonName);
}
if (!ttsAddon || ttsAddon.state !== "started") {
this._detailState = this.hass.localize(
`ui.panel.config.voice_assistants.satellite_wizard.local.state.starting_${this._ttsProviderName}`
);
await startHassioAddon(this.hass.callWS, this._ttsAddonName);
await startHassioAddon(this.hass, this._ttsAddonName);
}
this._detailState = this.hass.localize(
`ui.panel.config.voice_assistants.satellite_wizard.local.state.setup_${this._ttsProviderName}`
@@ -217,13 +217,13 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement {
this._detailState = this.hass.localize(
`ui.panel.config.voice_assistants.satellite_wizard.local.state.installing_${this._sttProviderName}`
);
await installHassioAddon(this.hass.callWS, this._sttAddonName);
await installHassioAddon(this.hass, this._sttAddonName);
}
if (!sttAddon || sttAddon.state !== "started") {
this._detailState = this.hass.localize(
`ui.panel.config.voice_assistants.satellite_wizard.local.state.starting_${this._sttProviderName}`
);
await startHassioAddon(this.hass.callWS, this._sttAddonName);
await startHassioAddon(this.hass, this._sttAddonName);
}
this._detailState = this.hass.localize(
`ui.panel.config.voice_assistants.satellite_wizard.local.state.setup_${this._sttProviderName}`
+2 -2
View File
@@ -213,7 +213,7 @@ class HaPanelApp extends LitElement {
let addon: HassioAddonDetails;
try {
addon = await fetchHassioAddonInfo(this.hass.callWS, addonSlug);
addon = await fetchHassioAddonInfo(this.hass, addonSlug);
} catch (err: any) {
await this._showErrorAndNavigateHome(
addonSlug,
@@ -253,7 +253,7 @@ class HaPanelApp extends LitElement {
);
// Set auto-retry window for after starting the app
this._autoRetryUntil = Date.now() + START_WAIT_TIME;
await startHassioAddon(this.hass.callWS, addonSlug);
await startHassioAddon(this.hass, addonSlug);
this._fetchData(addonSlug);
return;
} catch (_err) {
@@ -3,7 +3,7 @@ import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import "../../../../../components/ha-bar";
import "../../../../../components/item/ha-row-item";
import "../../../../../components/ha-settings-row";
import { roundWithOneDecimal } from "../../../../../util/calculate";
@customElement("supervisor-app-metric")
@@ -16,9 +16,9 @@ class SupervisorAppMetric extends LitElement {
protected render(): TemplateResult {
const roundedValue = roundWithOneDecimal(this.value);
return html`<ha-row-item empty>
<span slot="headline"> ${this.description} </span>
<div slot="supporting-text" .title=${this.tooltip ?? ""}>
return html`<ha-settings-row empty>
<span slot="heading"> ${this.description} </span>
<div slot="description" .title=${this.tooltip ?? ""}>
<span class="value"> ${roundedValue} % </span>
<ha-bar
class=${classMap({
@@ -28,14 +28,16 @@ class SupervisorAppMetric extends LitElement {
.value=${this.value}
></ha-bar>
</div>
</ha-row-item>`;
</ha-settings-row>`;
}
static styles = css`
ha-row-item {
ha-settings-row {
padding: 0;
height: 54px;
width: 100%;
}
ha-row-item > div[slot="supporting-text"] {
ha-settings-row > div[slot="description"] {
white-space: normal;
color: var(--secondary-text-color);
display: flex;
@@ -0,0 +1,283 @@
import {
css,
type CSSResultGroup,
html,
LitElement,
nothing,
type PropertyValues,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { atLeastVersion } from "../../../../../common/config/version";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/buttons/ha-progress-button";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-button";
import "../../../../../components/ha-card";
import "../../../../../components/ha-faded";
import "../../../../../components/ha-markdown";
import "../../../../../components/ha-spinner";
import "../../../../../components/ha-switch";
import type { HaSwitch } from "../../../../../components/ha-switch";
import "../../../../../components/item/ha-row-item";
import type { HassioAddonDetails } from "../../../../../data/hassio/addon";
import {
fetchHassioAddonChangelog,
updateHassioAddon,
} from "../../../../../data/hassio/addon";
import {
extractApiErrorMessage,
ignoreSupervisorError,
} from "../../../../../data/hassio/common";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import { extractChangelog } from "../util/supervisor-app";
declare global {
interface HASSDomEvents {
"update-complete": undefined;
}
}
@customElement("supervisor-app-update-available-card")
class SupervisorAppUpdateAvailableCard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public addon!: HassioAddonDetails;
@state() private _changelogContent?: string;
@state() private _updating = false;
@state() private _error?: string;
protected render() {
if (!this.addon) {
return nothing;
}
const createBackupTexts = this._computeCreateBackupTexts();
return html`
<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.apps.dashboard.update_available.update_name",
{
name: this.addon.name,
}
)}
>
<div class="card-content">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
${this.addon.version === this.addon.version_latest
? html`<p>
${this.hass.localize(
"ui.panel.config.apps.dashboard.update_available.no_update",
{
name: this.addon.name,
}
)}
</p>`
: !this._updating
? html`
${this._changelogContent
? html`
<ha-faded>
<ha-markdown .content=${this._changelogContent}>
</ha-markdown>
</ha-faded>
`
: nothing}
<div class="versions">
<p>
${this.hass.localize(
"ui.panel.config.apps.dashboard.update_available.description",
{
name: this.addon.name,
version: this.addon.version,
newest_version: this.addon.version_latest,
}
)}
</p>
</div>
${createBackupTexts
? html`
<hr />
<ha-row-item>
<span slot="headline">
${createBackupTexts.title}
</span>
${createBackupTexts.description
? html`
<span slot="supporting-text">
${createBackupTexts.description}
</span>
`
: nothing}
<ha-switch slot="end" id="create-backup"></ha-switch>
</ha-row-item>
`
: nothing}
`
: html`<ha-spinner
aria-label="Updating"
size="large"
></ha-spinner>
<p class="progress-text">
${this.hass.localize(
"ui.panel.config.apps.dashboard.update_available.updating",
{
name: this.addon.name,
version: this.addon.version_latest,
}
)}
</p>`}
</div>
${this.addon.version !== this.addon.version_latest && !this._updating
? html`
<div class="card-actions">
<span></span>
<ha-progress-button @click=${this._update}>
${this.hass.localize("ui.common.update")}
</ha-progress-button>
</div>
`
: nothing}
</ha-card>
`;
}
protected firstUpdated(changedProps: PropertyValues<this>) {
super.firstUpdated(changedProps);
this._loadAddonData();
}
private _computeCreateBackupTexts():
| { title: string; description?: string }
| undefined {
if (atLeastVersion(this.hass.config.version, 2025, 2, 0)) {
const version = this.addon.version;
return {
title: this.hass.localize(
"ui.panel.config.apps.dashboard.update_available.create_backup.app"
),
description: this.hass.localize(
"ui.panel.config.apps.dashboard.update_available.create_backup.app_description",
{ version: version }
),
};
}
return {
title: this.hass.localize(
"ui.panel.config.apps.dashboard.update_available.create_backup.generic"
),
};
}
get _shouldCreateBackup(): boolean {
const createBackupSwitch = this.shadowRoot?.getElementById(
"create-backup"
) as HaSwitch;
if (createBackupSwitch) {
return createBackupSwitch.checked;
}
return true;
}
private async _loadAddonData() {
if (this.addon.changelog) {
try {
const content = await fetchHassioAddonChangelog(
this.hass,
this.addon.slug
);
this._changelogContent = extractChangelog(
this.addon as HassioAddonDetails,
content
);
} catch (err) {
this._error = extractApiErrorMessage(err);
}
}
}
private async _update() {
this._error = undefined;
this._updating = true;
try {
await updateHassioAddon(
this.hass,
this.addon.slug,
this._shouldCreateBackup
);
} catch (err: any) {
if (this.hass.connection.connected && !ignoreSupervisorError(err)) {
this._error = extractApiErrorMessage(err);
this._updating = false;
return;
}
}
fireEvent(this, "update-complete");
this._updating = false;
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
display: block;
}
ha-card {
margin: auto;
}
a {
text-decoration: none;
color: var(--primary-text-color);
}
.card-actions {
display: flex;
justify-content: space-between;
}
ha-spinner {
display: block;
margin: 32px;
text-align: center;
}
.progress-text {
text-align: center;
}
ha-markdown {
padding-bottom: var(--ha-space-2);
}
hr {
border-color: var(--divider-color);
border-bottom: none;
margin: var(--ha-space-4) 0 0 0;
}
ha-row-item {
--ha-row-item-padding-inline: 0;
margin-bottom: calc(-1 * var(--ha-space-4));
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"supervisor-app-update-available-card": SupervisorAppUpdateAvailableCard;
}
}
@@ -183,7 +183,7 @@ class SupervisorAppAudio extends LitElement {
this._selectedOutput === "default" ? null : this._selectedOutput,
};
try {
await setHassioAddonOption(this.hass.callWS, this.addon.slug, data);
await setHassioAddonOption(this.hass, this.addon.slug, data);
if (this.addon?.state === "started") {
await suggestSupervisorAppRestart(this, this.hass, this.addon);
}
@@ -449,7 +449,7 @@ class SupervisorAppConfig extends LitElement {
options: null,
};
try {
await setHassioAddonOption(this.hass.callWS, this.addon.slug, data);
await setHassioAddonOption(this.hass, this.addon.slug, data);
this._configHasChanged = false;
const eventdata = {
success: true,
@@ -488,14 +488,14 @@ class SupervisorAppConfig extends LitElement {
try {
const validation = await validateHassioAddonOption(
this.hass.callWS,
this.hass,
this.addon.slug,
options
);
if (!validation.valid) {
throw Error(validation.message);
}
await setHassioAddonOption(this.hass.callWS, this.addon.slug, {
await setHassioAddonOption(this.hass, this.addon.slug, {
options,
});
@@ -6,9 +6,9 @@ import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/buttons/ha-progress-button";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-card";
import "../../../../../components/ha-formfield";
import "../../../../../components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../../components/ha-form/types";
import "../../../../../components/ha-formfield";
import type {
HassioAddonDetails,
HassioAddonSetOptionParams,
@@ -17,8 +17,8 @@ import { setHassioAddonOption } from "../../../../../data/hassio/addon";
import { extractApiErrorMessage } from "../../../../../data/hassio/common";
import { haStyle } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import { supervisorAppsStyle } from "../../resources/supervisor-apps-style";
import { suggestSupervisorAppRestart } from "../dialogs/suggestSupervisorAppRestart";
import { supervisorAppsStyle } from "../../resources/supervisor-apps-style";
@customElement("supervisor-app-network")
class SupervisorAppNetwork extends LitElement {
@@ -160,7 +160,7 @@ class SupervisorAppNetwork extends LitElement {
};
try {
await setHassioAddonOption(this.hass.callWS, this.addon.slug, data);
await setHassioAddonOption(this.hass, this.addon.slug, data);
this._configHasChanged = false;
const eventdata = {
success: true,
@@ -205,7 +205,7 @@ class SupervisorAppNetwork extends LitElement {
};
try {
await setHassioAddonOption(this.hass.callWS, this.addon.slug, data);
await setHassioAddonOption(this.hass, this.addon.slug, data);
this._configHasChanged = false;
const eventdata = {
success: true,
@@ -28,7 +28,7 @@ export const suggestSupervisorAppRestart = async (
});
if (confirmed) {
try {
await restartHassioAddon(hass.callWS, addon.slug);
await restartHassioAddon(hass, addon.slug);
} catch (err: any) {
showAlertDialog(element, {
title: hass.localize(
@@ -46,8 +46,8 @@ class SupervisorAppInfoDashboard extends LitElement {
css`
.content {
margin: auto;
padding: var(--ha-space-4);
max-width: 1200px;
padding: var(--ha-space-2);
max-width: 1024px;
}
`,
];
File diff suppressed because it is too large Load Diff
@@ -1,19 +1,16 @@
import { consume, type ContextType } from "@lit/context";
import type { TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-button";
import { internationalizationContext } from "../../../../../data/context";
import type { HomeAssistant } from "../../../../../types";
@customElement("supervisor-app-system-managed")
class SupervisorAppSystemManaged extends LitElement {
@property({ type: Boolean }) public narrow = false;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private i18n!: ContextType<typeof internationalizationContext>;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, attribute: "hide-button" }) public hideButton =
false;
@@ -22,18 +19,18 @@ class SupervisorAppSystemManaged extends LitElement {
return html`
<ha-alert
alert-type="warning"
.title=${this.i18n.localize(
.title=${this.hass.localize(
"ui.panel.config.apps.dashboard.system_managed.title"
)}
.narrow=${this.narrow}
>
${this.i18n.localize(
${this.hass.localize(
"ui.panel.config.apps.dashboard.system_managed.description"
)}
${!this.hideButton
? html`
<ha-button slot="action" @click=${this._takeControl}>
${this.i18n.localize(
${this.hass.localize(
"ui.panel.config.apps.dashboard.system_managed.take_control"
)}
</ha-button>
@@ -161,7 +161,7 @@ class HaConfigAppDashboard extends LitElement {
}
try {
this._addon = await fetchHassioAddonInfo(this.hass.callWS, slug);
this._addon = await fetchHassioAddonInfo(this.hass, slug);
} catch (err: any) {
if (repositoryUrl) {
try {
@@ -210,7 +210,7 @@ class HaConfigAppDashboard extends LitElement {
}
await addStoreRepository(this.hass, repositoryUrl);
this._addon = await fetchHassioAddonInfo(this.hass.callWS, slug);
this._addon = await fetchHassioAddonInfo(this.hass, slug);
}
private async _apiCalled(ev): Promise<void> {
+16 -19
View File
@@ -220,13 +220,6 @@ class HaConfigAreaPage extends LitElement {
));
}
const nonAutomatedEntities = entities.filter(
(entity) =>
!["scene", "script", "automation"].includes(
computeDomain(entity.entity_id)
)
);
return html`
<hass-subpage
.hass=${this.hass}
@@ -310,19 +303,23 @@ class HaConfigAreaPage extends LitElement {
"ui.panel.config.areas.editor.linked_entities_caption"
)}
>
${nonAutomatedEntities.length
${entities.length
? html`<ha-list>
${nonAutomatedEntities.map(
(entity) => html`
<ha-list-item
@click=${this._openEntity}
.entity=${entity}
hasMeta
>
<span>${entity.name}</span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
`
${entities.map((entity) =>
["scene", "script", "automation"].includes(
computeDomain(entity.entity_id)
)
? ""
: html`
<ha-list-item
@click=${this._openEntity}
.entity=${entity}
hasMeta
>
<span>${entity.name}</span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
`
)}</ha-list
>`
: html`
@@ -107,7 +107,6 @@ export default class HaAutomationActionEditor extends LitElement {
ev.stopPropagation();
const value = {
...(this.action.alias ? { alias: this.action.alias } : {}),
...(this.action.comment ? { comment: this.action.comment } : {}),
...ev.detail.value,
};
fireEvent(this, "value-changed", { value });
@@ -7,8 +7,6 @@ import {
mdiArrowUp,
mdiCheckboxBlankOutline,
mdiCheckboxOutline,
mdiCommentEditOutline,
mdiCommentTextOutline,
mdiContentCopy,
mdiContentCut,
mdiContentPaste,
@@ -36,7 +34,6 @@ import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { computeDomain } from "../../../../common/entity/compute_domain";
import { computeObjectId } from "../../../../common/entity/compute_object_id";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
import { truncateWithEllipsis } from "../../../../common/string/truncate-with-ellipsis";
import { handleStructError } from "../../../../common/structs/handle-errors";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import "../../../../components/automation/ha-automation-row";
@@ -297,11 +294,6 @@ export default class HaAutomationActionRow extends LitElement {
?.target
: undefined;
const commentTooltipText = truncateWithEllipsis(
this.action.comment?.trim() || "",
250
);
return html`
${type === "service" && "action" in this.action && this.action.action
? html`
@@ -337,21 +329,6 @@ export default class HaAutomationActionRow extends LitElement {
serviceTargetSpec
)
: nothing}
${commentTooltipText
? html`
<ha-svg-icon
id="comment-icon"
.path=${mdiCommentTextOutline}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.comment.label"
)}
class="comment-indicator"
></ha-svg-icon
><ha-tooltip for="comment-icon"
><p>${commentTooltipText}</p></ha-tooltip
>
`
: nothing}
${type !== "condition" &&
(this.action as NonConditionAction).continue_on_error === true
? html`<ha-svg-icon
@@ -407,14 +384,6 @@ export default class HaAutomationActionRow extends LitElement {
)
)}
</ha-dropdown-item>
<ha-dropdown-item value="edit_comment">
<ha-svg-icon slot="icon" .path=${mdiCommentEditOutline}></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
`ui.panel.config.automation.editor.comment.${this.action.comment ? "edit" : "add"}`
)
)}
</ha-dropdown-item>
<wa-divider></wa-divider>
<ha-dropdown-item value="duplicate" .disabled=${this.disabled}>
<ha-svg-icon
@@ -941,38 +910,6 @@ export default class HaAutomationActionRow extends LitElement {
}
};
private _editCommentAction = async (): Promise<void> => {
const comment = await showPromptDialog(this, {
title: this.hass.localize(
`ui.panel.config.automation.editor.comment.${this.action.comment ? "edit" : "add"}`
),
inputLabel: this.hass.localize(
"ui.panel.config.automation.editor.comment.label"
),
inputType: "string",
defaultValue: this.action.comment,
confirmText: this.hass.localize("ui.common.submit"),
multiline: true,
});
if (comment !== null) {
const value = { ...this.action };
if (comment === "") {
delete value.comment;
} else {
value.comment = comment;
}
fireEvent(this, "value-changed", {
value,
});
if (this._selected && this.optionsInSidebar) {
this.openSidebar(value); // refresh sidebar
} else if (this._yamlMode) {
this._actionEditor?.yamlEditor?.setValue(value);
}
}
};
private _duplicateAction = () => {
fireEvent(this, "duplicate");
};
@@ -1089,7 +1026,6 @@ export default class HaAutomationActionRow extends LitElement {
rename: () => {
this._renameAction();
},
editComment: this._editCommentAction,
toggleYamlMode: () => {
this._toggleYamlMode();
this.openSidebar();
@@ -1185,9 +1121,6 @@ export default class HaAutomationActionRow extends LitElement {
case "rename":
this._renameAction();
break;
case "edit_comment":
this._editCommentAction();
break;
case "duplicate":
this._duplicateAction();
break;
@@ -18,7 +18,6 @@ import { getValueFromDynamic, isDynamic } from "../../../../data/automation";
import type { Action } from "../../../../data/script";
import { EDITOR_SAVE_FAB_TOAST_BOTTOM_OFFSET } from "../editor-toast";
import {
getAddAutomationElementTargetFromQuery,
PASTE_VALUE,
showAddAutomationElementDialog,
} from "../show-add-automation-element-dialog";
@@ -42,8 +41,6 @@ export default class HaAutomationAction extends AutomationSortableListMixin<Acti
@queryAll("ha-automation-action-row")
private _actionRowElements?: HaAutomationActionRow[];
private _openedAddDialogFromQuery = false;
protected get items(): Action[] {
return this.actions;
}
@@ -136,33 +133,6 @@ export default class HaAutomationAction extends AutomationSortableListMixin<Acti
protected updated(changedProps: PropertyValues<this>) {
super.updated(changedProps);
if (!this.hass) {
return;
}
const addActionTargetFromQuery = getAddAutomationElementTargetFromQuery(
this.hass.states,
this.hass.devices,
"action"
);
if (changedProps.has("actions") && addActionTargetFromQuery) {
this._openedAddDialogFromQuery = false;
}
if (
!this._openedAddDialogFromQuery &&
this.root &&
!this.disabled &&
this.actions.length === 0 &&
addActionTargetFromQuery
) {
this._openedAddDialogFromQuery = true;
queueMicrotask(() => this._addActionDialog());
} else if (this._openedAddDialogFromQuery && !addActionTargetFromQuery) {
this._openedAddDialogFromQuery = false;
}
if (
changedProps.has("actions") &&
(this.focusLastItemOnChange || this.focusItemIndexOnChange !== undefined)
@@ -112,11 +112,11 @@ export class HaDeviceAction extends LitElement {
.schema=${this._capabilities.extra_fields}
.disabled=${this.disabled}
.computeLabel=${localizeExtraFieldsComputeLabelCallback(
this.hass.localize,
this.hass,
this.action
)}
.computeHelper=${localizeExtraFieldsComputeHelperCallback(
this.hass.localize,
this.hass,
this.action
)}
@value-changed=${this._extraFieldsChanged}
@@ -149,7 +149,7 @@ export class HaDeviceAction extends LitElement {
private async _getCapabilities() {
this._capabilities = this.action.domain
? await fetchDeviceActionCapabilities(this.hass.callWS, this.action)
? await fetchDeviceActionCapabilities(this.hass, this.action)
: undefined;
}
@@ -186,10 +186,6 @@ export class HaDeviceAction extends LitElement {
}
static styles = css`
:host {
display: block;
margin-bottom: var(--ha-space-3);
}
ha-device-picker {
display: block;
margin-bottom: 24px;
@@ -33,7 +33,6 @@ import type {
import { computeRTL } from "../../../common/util/compute_rtl";
import { debounce } from "../../../common/util/debounce";
import { deepEqual } from "../../../common/util/deep-equal";
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
import "../../../components/entity/state-badge";
import "../../../components/ha-bottom-sheet";
import "../../../components/ha-button";
@@ -129,13 +128,7 @@ import "./add-automation-element/ha-automation-add-from-target";
import "./add-automation-element/ha-automation-add-items";
import "./add-automation-element/ha-automation-add-search";
import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog";
import {
ADD_AUTOMATION_ELEMENT_DEVICE_TARGET_PARAM,
ADD_AUTOMATION_ELEMENT_ENTITY_TARGET_PARAM,
ADD_AUTOMATION_ELEMENT_QUERY_PARAM,
getAddAutomationElementTargetFromQuery,
PASTE_VALUE,
} from "./show-add-automation-element-dialog";
import { PASTE_VALUE } from "./show-add-automation-element-dialog";
import { getTargetText } from "./target/get_target_text";
const TYPES = {
@@ -237,8 +230,6 @@ class DialogAddAutomationElement
@state() private _newTriggersAndConditions = false;
@state() private _openedFromQuery = false;
@state() private _conditionDescriptions: ConditionDescriptions = {};
@state()
@@ -304,29 +295,10 @@ class DialogAddAutomationElement
}
}
public showDialog(params: AddAutomationElementDialogParams): void {
public showDialog(params): void {
this._params = params;
this._resetVariables();
const queryTarget = getAddAutomationElementTargetFromQuery(
this.hass.states,
this.hass.devices,
params.type
);
this._openedFromQuery = !!queryTarget;
if (queryTarget) {
const searchParams = new URLSearchParams(mainWindow.location.search);
searchParams.delete(ADD_AUTOMATION_ELEMENT_QUERY_PARAM);
searchParams.delete(ADD_AUTOMATION_ELEMENT_ENTITY_TARGET_PARAM);
searchParams.delete(ADD_AUTOMATION_ELEMENT_DEVICE_TARGET_PARAM);
mainWindow.history.replaceState(
mainWindow.history.state,
"",
constructUrlCurrentPath(searchParams.toString())
);
}
this.addKeyboardShortcuts();
this._loadConfigEntries();
@@ -342,26 +314,16 @@ class DialogAddAutomationElement
(feature) => {
this._newTriggersAndConditions = feature.enabled;
this._tab = this._newTriggersAndConditions ? "targets" : "groups";
if (
queryTarget &&
this._newTriggersAndConditions &&
!this._selectedTarget
) {
this._selectedTarget = queryTarget;
this._getItemsByTarget();
}
}
);
if (!queryTarget) {
// add initial dialog view state to history
mainWindow.history.pushState(
{
dialogData: {},
},
""
);
}
// add initial dialog view state to history
mainWindow.history.pushState(
{
dialogData: {},
},
""
);
if (this._params?.type === "action") {
this.hass.loadBackendTranslation("services");
@@ -381,16 +343,6 @@ class DialogAddAutomationElement
// prevent view mode switch when resizing window
this._bottomSheetMode = this._narrow;
if (
queryTarget &&
this._newTriggersAndConditions &&
!this._selectedTarget
) {
this._selectedTarget = queryTarget;
this._tab = "targets";
this._getItemsByTarget();
}
}
public closeDialog(historyState?: any) {
@@ -455,7 +407,6 @@ class DialogAddAutomationElement
this._narrow = false;
this._targetItems = undefined;
this._loadItemsError = false;
this._openedFromQuery = false;
}
private _updateNarrow = () => {
@@ -940,9 +891,7 @@ class DialogAddAutomationElement
></ha-icon-button>
`
: nothing}
${this._narrow &&
(this._selectedGroup || this._selectedTarget) &&
!this._openedFromQuery
${this._narrow && (this._selectedGroup || this._selectedTarget)
? html`<ha-icon-button-prev
slot="navigationIcon"
@click=${this._back}
@@ -2294,14 +2243,7 @@ class DialogAddAutomationElement
if (targetId) {
return getTargetText(
{
entities: this.hass.entities,
devices: this.hass.devices,
areas: this.hass.areas,
floors: this.hass.floors,
},
this.hass.states,
this.hass.localize,
this.hass,
targetType as "floor" | "area" | "device" | "entity" | "label",
targetId,
this._getLabel
@@ -161,13 +161,7 @@ export class HaAutomationAddItems extends LitElement {
return html`<div class="selected-target">
${getTargetIcon(
{
entities: this.hass.entities,
devices: this.hass.devices,
areas: this.hass.areas,
floors: this.hass.floors,
},
this.hass.states,
this.hass,
target[0],
target[1],
this.configEntryLookup,
@@ -123,7 +123,6 @@ export default class HaAutomationConditionEditor extends LitElement {
ev.stopPropagation();
const value = {
...(this.condition.alias ? { alias: this.condition.alias } : {}),
...(this.condition.comment ? { comment: this.condition.comment } : {}),
...ev.detail.value,
};
fireEvent(this, "value-changed", { value });
@@ -4,8 +4,6 @@ import {
mdiAppleKeyboardCommand,
mdiArrowDown,
mdiArrowUp,
mdiCommentEditOutline,
mdiCommentTextOutline,
mdiContentCopy,
mdiContentCut,
mdiContentPaste,
@@ -35,7 +33,6 @@ import { fireEvent } from "../../../../common/dom/fire_event";
import { preventDefaultStopPropagation } from "../../../../common/dom/prevent_default_stop_propagation";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
import { truncateWithEllipsis } from "../../../../common/string/truncate-with-ellipsis";
import { handleStructError } from "../../../../common/structs/handle-errors";
import { copyToClipboard } from "../../../../common/util/copy-clipboard";
import { debounce } from "../../../../common/util/debounce";
@@ -43,7 +40,6 @@ import "../../../../components/automation/ha-automation-row";
import type { HaAutomationRow } from "../../../../components/automation/ha-automation-row";
import "../../../../components/automation/ha-automation-row-event-chip";
import "../../../../components/automation/ha-automation-row-live-test";
import type { LiveTestState } from "../../../../components/automation/ha-automation-row-live-test";
import "../../../../components/ha-card";
import "../../../../components/ha-condition-icon";
import "../../../../components/ha-dropdown";
@@ -153,7 +149,10 @@ export default class HaAutomationConditionRow extends LitElement {
@state() private _selected = false;
@state() private _liveTestResult: LiveTestState = "unknown";
@state() private _liveTestResult: {
state: "pass" | "fail" | "invalid" | "unknown";
message?: string;
} = { state: "unknown" };
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
@@ -201,11 +200,6 @@ export default class HaAutomationConditionRow extends LitElement {
const conditionTargetSpec =
this.conditionDescriptions[this.condition.condition]?.target;
const commentTooltipText = truncateWithEllipsis(
this.condition.comment?.trim() || "",
250
);
return html`
<ha-condition-icon
slot="leading-icon"
@@ -223,21 +217,6 @@ export default class HaAutomationConditionRow extends LitElement {
conditionTargetSpec
)
: nothing}
${this.condition.comment?.trim()
? html`
<ha-svg-icon
id="comment-icon"
.path=${mdiCommentTextOutline}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.comment.label"
)}
class="comment-indicator"
></ha-svg-icon>
<ha-tooltip for="comment-icon"
><p>${commentTooltipText}</p></ha-tooltip
>
`
: nothing}
</h3>
<ha-automation-row-event-chip
.show=${this._testing}
@@ -285,14 +264,6 @@ export default class HaAutomationConditionRow extends LitElement {
)
)}
</ha-dropdown-item>
<ha-dropdown-item value="edit_comment">
<ha-svg-icon slot="icon" .path=${mdiCommentEditOutline}></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
`ui.panel.config.automation.editor.comment.${this.condition.comment ? "edit" : "add"}`
)
)}
</ha-dropdown-item>
<wa-divider></wa-divider>
@@ -529,10 +500,11 @@ export default class HaAutomationConditionRow extends LitElement {
>${this._renderRow()}
<ha-automation-row-live-test
slot="icons"
.state=${this._liveTestResult}
.state=${this._liveTestResult.state}
.label=${this.hass.localize(
`ui.panel.config.automation.editor.conditions.live_test_state.${this._liveTestResult}`
`ui.panel.config.automation.editor.conditions.live_test_state.${this._liveTestResult.state}`
)}
.message=${this._liveTestResult.message}
></ha-automation-row-live-test
></ha-automation-row>`
: html`
@@ -619,7 +591,12 @@ export default class HaAutomationConditionRow extends LitElement {
}
private _resetSubscription() {
this._liveTestResult = "unknown";
this._liveTestResult = {
state: "unknown",
message: this.hass.localize(
"ui.panel.config.automation.editor.conditions.live_test_state.unknown"
),
};
if (this._conditionUnsub) {
this._conditionUnsub.then((unsub) => unsub());
this._conditionUnsub = undefined;
@@ -644,7 +621,12 @@ export default class HaAutomationConditionRow extends LitElement {
if (result.error) {
this._handleLiveTestError(result.error);
} else {
this._liveTestResult = result.result ? "pass" : "fail";
this._liveTestResult = {
state: result.result ? "pass" : "fail",
message: this.hass.localize(
`ui.panel.config.automation.editor.conditions.testing_${result.result ? "pass" : "error"}`
),
};
}
},
this.condition
@@ -661,7 +643,10 @@ export default class HaAutomationConditionRow extends LitElement {
private _handleLiveTestError(error: any) {
const invalid =
typeof error !== "string" && error.code === "invalid_format";
this._liveTestResult = invalid ? "invalid" : "unknown";
this._liveTestResult = {
state: invalid ? "invalid" : "unknown",
message: typeof error === "string" ? error : error.message,
};
}
private _onValueChange(event: CustomEvent) {
@@ -828,38 +813,6 @@ export default class HaAutomationConditionRow extends LitElement {
}
};
private _editCommentCondition = async (): Promise<void> => {
const comment = await showPromptDialog(this, {
title: this.hass.localize(
`ui.panel.config.automation.editor.comment.${this.condition.comment ? "edit" : "add"}`
),
inputLabel: this.hass.localize(
"ui.panel.config.automation.editor.comment.label"
),
inputType: "string",
defaultValue: this.condition.comment,
confirmText: this.hass.localize("ui.common.submit"),
multiline: true,
});
if (comment !== null) {
const value = { ...this.condition };
if (comment === "") {
delete value.comment;
} else {
value.comment = comment;
}
fireEvent(this, "value-changed", {
value,
});
if (this._selected && this.optionsInSidebar) {
this.openSidebar(value); // refresh sidebar
} else if (this._yamlMode) {
this.conditionEditor?.yamlEditor?.setValue(value);
}
}
};
private _duplicateCondition = () => {
fireEvent(this, "duplicate");
};
@@ -1001,7 +954,6 @@ export default class HaAutomationConditionRow extends LitElement {
rename: () => {
this._renameCondition();
},
editComment: this._editCommentCondition,
toggleYamlMode: () => {
this._toggleYamlMode();
this.openSidebar();
@@ -1073,9 +1025,6 @@ export default class HaAutomationConditionRow extends LitElement {
case "rename":
this._renameCondition();
break;
case "edit_comment":
this._editCommentCondition();
break;
case "duplicate":
this._duplicateCondition();
break;
@@ -27,7 +27,6 @@ import { subscribeLabFeature } from "../../../../data/labs";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { EDITOR_SAVE_FAB_TOAST_BOTTOM_OFFSET } from "../editor-toast";
import {
getAddAutomationElementTargetFromQuery,
PASTE_VALUE,
showAddAutomationElementDialog,
} from "../show-add-automation-element-dialog";
@@ -58,8 +57,6 @@ export default class HaAutomationCondition extends AutomationSortableListMixin<C
private _unsub?: Promise<UnsubscribeFunc>;
private _openedAddDialogFromQuery = false;
protected get items(): Condition[] {
return this.conditions;
}
@@ -121,33 +118,6 @@ export default class HaAutomationCondition extends AutomationSortableListMixin<C
}
protected updated(changedProperties: PropertyValues<this>) {
if (!this.hass) {
return;
}
const addConditionTargetFromQuery = getAddAutomationElementTargetFromQuery(
this.hass.states,
this.hass.devices,
"condition"
);
if (changedProperties.has("conditions") && addConditionTargetFromQuery) {
this._openedAddDialogFromQuery = false;
}
if (
!this._openedAddDialogFromQuery &&
this.root &&
!this.disabled &&
this.conditions.length === 0 &&
addConditionTargetFromQuery
) {
this._openedAddDialogFromQuery = true;
queueMicrotask(() => this._addConditionDialog());
} else if (this._openedAddDialogFromQuery && !addConditionTargetFromQuery) {
this._openedAddDialogFromQuery = false;
}
if (!changedProperties.has("conditions")) {
return;
}
@@ -113,11 +113,11 @@ export class HaDeviceCondition extends LitElement {
.schema=${this._capabilities.extra_fields}
.disabled=${this.disabled}
.computeLabel=${localizeExtraFieldsComputeLabelCallback(
this.hass.localize,
this.hass,
this.condition
)}
.computeHelper=${localizeExtraFieldsComputeHelperCallback(
this.hass.localize,
this.hass,
this.condition
)}
@value-changed=${this._extraFieldsChanged}
@@ -151,7 +151,7 @@ export class HaDeviceCondition extends LitElement {
const condition = this.condition;
this._capabilities = condition.domain
? await fetchDeviceConditionCapabilities(this.hass.callWS, condition)
? await fetchDeviceConditionCapabilities(this.hass, condition)
: undefined;
}
@@ -188,10 +188,6 @@ export class HaDeviceCondition extends LitElement {
}
static styles = css`
:host {
display: block;
margin-bottom: var(--ha-space-3);
}
ha-device-picker {
display: block;
margin-bottom: 24px;
@@ -1,5 +1,5 @@
import type { PropertyValues } from "lit";
import { css, html, LitElement } from "lit";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import {
@@ -22,7 +22,6 @@ import type { HomeAssistant } from "../../../../../types";
const numericStateConditionStruct = object({
alias: optional(string()),
comment: optional(string()),
condition: literal("numeric_state"),
entity_id: optional(string()),
attribute: optional(string()),
@@ -256,13 +255,6 @@ export default class HaNumericStateCondition extends LitElement {
);
}
};
static styles = css`
:host {
display: block;
margin-bottom: var(--ha-space-3);
}
`;
}
declare global {
@@ -1,8 +1,7 @@
import type { PropertyValues } from "lit";
import { css, html, LitElement } from "lit";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import {
array,
assert,
boolean,
literal,
@@ -11,9 +10,10 @@ import {
optional,
string,
union,
array,
} from "superstruct";
import { ensureArray } from "../../../../../common/array/ensure-array";
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
import { ensureArray } from "../../../../../common/array/ensure-array";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
@@ -25,7 +25,6 @@ import type { ConditionElement } from "../ha-automation-condition-row";
const stateConditionStruct = object({
alias: optional(string()),
comment: optional(string()),
condition: literal("state"),
entity_id: optional(string()),
attribute: optional(string()),
@@ -143,13 +142,6 @@ export class HaStateCondition extends LitElement implements ConditionElement {
);
}
};
static styles = css`
:host {
display: block;
margin-bottom: var(--ha-space-3);
}
`;
}
declare global {
@@ -1,79 +0,0 @@
import { consume, type ContextType } from "@lit/context";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-button";
import "../../../components/ha-settings-row";
import { internationalizationContext } from "../../../data/context";
@customElement("ha-automation-comment")
export class HaAutomationComment extends LitElement {
@property() public comment!: string;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
protected render() {
return html`
<ha-settings-row narrow>
<div class="heading" slot="heading">
<span class="title" id="comment-label">
${this._i18n.localize(
"ui.panel.config.automation.editor.comment.label"
)}
</span>
<ha-button
@click=${this._handleClick}
size="small"
appearance="plain"
>
${this._i18n.localize("ui.common.edit")}
</ha-button>
</div>
<p aria-labelledby="comment-label">${this.comment}</p>
</ha-settings-row>
`;
}
private _handleClick() {
fireEvent(this, "edit-comment");
}
static styles = css`
ha-settings-row {
margin-inline: calc(-1 * var(--ha-space-4));
}
ha-settings-row::part(heading) {
padding-inline-end: 0;
overflow: visible;
}
.heading {
display: flex;
justify-content: space-between;
align-items: center;
}
p {
margin: var(--ha-space-2) 0 0;
border: var(--ha-border-width-sm) solid
var(--ha-color-border-neutral-quiet);
padding: var(--ha-space-1) var(--ha-space-3);
border-radius: var(--ha-border-radius-lg);
background-color: var(--ha-color-fill-neutral-quiet-resting);
white-space: pre-wrap;
}
ha-button {
margin-inline-end: calc(-1 * var(--ha-space-3));
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-comment": HaAutomationComment;
}
interface HASSDomEvents {
"edit-comment": undefined;
}
}
@@ -77,7 +77,6 @@ import { showAssignCategoryDialog } from "../category/show-dialog-assign-categor
import { showAutomationModeDialog } from "./automation-mode-dialog/show-dialog-automation-mode";
import { showAutomationSaveDialog } from "./automation-save-dialog/show-dialog-automation-save";
import { showAutomationSaveTimeoutDialog } from "./automation-save-timeout-dialog/show-dialog-automation-save-timeout";
import { ADD_AUTOMATION_ELEMENT_QUERY_PARAM } from "./show-add-automation-element-dialog";
import "./blueprint-automation-editor";
import type { EditorDomainHooks } from "./ha-automation-script-editor-mixin";
import {
@@ -573,21 +572,11 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!this.hass) {
return;
}
const shouldResetNewAutomationConfigFromQuery =
changedProps.has("route") &&
this.route?.path === "/new" &&
new URLSearchParams(window.location.search).has(
ADD_AUTOMATION_ELEMENT_QUERY_PARAM
);
const oldAutomationId = changedProps.get("automationId");
if (
changedProps.has("automationId") &&
this.automationId &&
this.hass &&
// Only refresh config if we picked a new automation. If same ID, don't fetch it.
oldAutomationId !== this.automationId
) {
@@ -596,10 +585,10 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
}
if (
(changedProps.has("automationId") ||
shouldResetNewAutomationConfigFromQuery) &&
changedProps.has("automationId") &&
!this.automationId &&
!this.entityId
!this.entityId &&
this.hass
) {
const initData = getAutomationEditorInitData();
this.dirty = !!initData;
@@ -169,19 +169,15 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
private _filter = "";
@state()
private _filters: DataTableFilters = {};
@storage({
storage: "sessionStorage",
key: "automation-table-filters-full",
state: false,
state: true,
subscribe: false,
serializer: serializeFilters,
deserializer: deserializeFilters,
})
private _storageFilters: DataTableFilters = {};
private _fromUrl = false;
private _filters: DataTableFilters = {};
@state() private _expandedFilter?: string;
@@ -764,23 +760,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
`;
}
protected willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
const hasUrlFilter =
this._searchParms.has("blueprint") || this._searchParms.has("label");
if (!hasUrlFilter) {
this._filters = this._storageFilters;
}
if (this._searchParms.has("blueprint")) {
this._filterBlueprint();
}
if (this._searchParms.has("label")) {
this._filterLabel();
}
}
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("_entityReg")) {
@@ -788,6 +767,15 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
}
}
firstUpdated() {
if (this._searchParms.has("blueprint")) {
this._filterBlueprint();
}
if (this._searchParms.has("label")) {
this._filterLabel();
}
}
private _filterExpanded(ev) {
if (ev.detail.expanded) {
this._expandedFilter = ev.target.localName;
@@ -805,9 +793,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
items: undefined,
},
};
if (!this._fromUrl) {
this._storageFilters = this._filters;
}
this._applyFilters();
};
@@ -818,9 +803,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
private _filterChanged(ev) {
const type = ev.target.localName;
this._filters = { ...this._filters, [type]: ev.detail };
if (!this._fromUrl) {
this._storageFilters = this._filters;
}
this._applyFilters();
}
@@ -876,7 +858,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
if (!label) {
return;
}
this._fromUrl = true;
this._filters = {
...this._filters,
"ha-filter-labels": {
@@ -892,7 +873,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
if (!blueprint) {
return;
}
this._fromUrl = true;
const related = await findRelated(
this.hass,
"automation_blueprint",
@@ -910,9 +890,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
private _clearFilter() {
this._filters = {};
if (!this._fromUrl) {
this._storageFilters = {};
}
this._applyFilters();
}
@@ -3,8 +3,6 @@ import {
mdiAppleKeyboardCommand,
mdiArrowDown,
mdiArrowUp,
mdiCommentEditOutline,
mdiCommentTextOutline,
mdiDelete,
mdiDotsVertical,
mdiPlusCircleMultipleOutline,
@@ -19,7 +17,6 @@ import { fireEvent } from "../../../../common/dom/fire_event";
import { preventDefaultStopPropagation } from "../../../../common/dom/prevent_default_stop_propagation";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
import { truncateWithEllipsis } from "../../../../common/string/truncate-with-ellipsis";
import "../../../../components/automation/ha-automation-row";
import type { HaAutomationRow } from "../../../../components/automation/ha-automation-row";
import "../../../../components/ha-card";
@@ -40,11 +37,11 @@ import type { Action, Option } from "../../../../data/script";
import { showPromptDialog } from "../../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import { showEditorToast } from "../editor-toast";
import "../action/ha-automation-action";
import type HaAutomationAction from "../action/ha-automation-action";
import "../condition/ha-automation-condition";
import type HaAutomationCondition from "../condition/ha-automation-condition";
import { showEditorToast } from "../editor-toast";
import {
editorStyles,
indentStyle,
@@ -141,12 +138,8 @@ export default class HaAutomationOptionRow extends LitElement {
</div>
`;
}
private _renderRow() {
const commentTooltipText = truncateWithEllipsis(
this.option?.comment?.trim() || "",
250
);
private _renderRow() {
return html`
<h3 slot="header">
${this.option
@@ -157,21 +150,6 @@ export default class HaAutomationOptionRow extends LitElement {
: this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.default"
)}
${this.option?.comment?.trim()
? html`
<ha-svg-icon
id="comment-icon"
.path=${mdiCommentTextOutline}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.comment.label"
)}
class="comment-indicator"
></ha-svg-icon>
<ha-tooltip for="comment-icon"
><p>${commentTooltipText}</p></ha-tooltip
>
`
: nothing}
</h3>
<slot name="icons" slot="icons"></slot>
@@ -199,17 +177,6 @@ export default class HaAutomationOptionRow extends LitElement {
)
)}
</ha-dropdown-item>
<ha-dropdown-item value="edit_comment">
<ha-svg-icon
slot="icon"
.path=${mdiCommentEditOutline}
></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
`ui.panel.config.automation.editor.comment.${this.option?.comment ? "edit" : "add"}`
)
)}
</ha-dropdown-item>
<ha-dropdown-item value="duplicate" .disabled=${this.disabled}>
<ha-svg-icon
@@ -394,9 +361,6 @@ export default class HaAutomationOptionRow extends LitElement {
case "rename":
this._renameOption();
break;
case "edit_comment":
this._editCommentOption();
break;
case "delete":
this._removeOption();
break;
@@ -460,39 +424,6 @@ export default class HaAutomationOptionRow extends LitElement {
}
};
private _editCommentOption = async (): Promise<void> => {
if (!this.option) {
return;
}
const comment = await showPromptDialog(this, {
title: this.hass.localize(
`ui.panel.config.automation.editor.comment.${this.option.comment ? "edit" : "add"}`
),
inputLabel: this.hass.localize(
"ui.panel.config.automation.editor.comment.label"
),
inputType: "string",
defaultValue: this.option.comment,
confirmText: this.hass.localize("ui.common.submit"),
multiline: true,
});
if (comment !== null) {
const value: Option = { ...this.option };
if (comment === "") {
delete value.comment;
} else {
value.comment = comment;
}
fireEvent(this, "value-changed", {
value,
});
if (this._selected) {
this.openSidebar(value); // refresh sidebar
}
}
};
private _conditionChanged(ev: CustomEvent) {
ev.stopPropagation();
const conditions = ev.detail.value as Condition[];
@@ -524,8 +455,7 @@ export default class HaAutomationOptionRow extends LitElement {
this.openSidebar();
}
public openSidebar(option?: Option): void {
const sidebarOption = option ?? this.option;
public openSidebar(): void {
fireEvent(this, "open-sidebar", {
close: (focus?: boolean) => {
this._selected = false;
@@ -537,11 +467,9 @@ export default class HaAutomationOptionRow extends LitElement {
rename: () => {
this._renameOption();
},
editComment: this._editCommentOption,
delete: this._removeOption,
duplicate: this._duplicateOption,
defaultOption: !!this.defaultActions,
comment: sidebarOption?.comment,
} satisfies OptionSidebarConfig);
this._selected = true;
this._collapsed = false;
@@ -1,60 +1,23 @@
import type { HassServiceTarget } from "home-assistant-js-websocket";
import { fireEvent } from "../../../common/dom/fire_event";
import type { SingleHassServiceTarget } from "../../../data/target";
import type { HomeAssistant } from "../../../types";
export const PASTE_VALUE = "__paste__";
export const ADD_AUTOMATION_ELEMENT_QUERY_PARAM = "add_automation_element";
export const ADD_AUTOMATION_ELEMENT_ENTITY_TARGET_PARAM = "target_entity_id";
export const ADD_AUTOMATION_ELEMENT_DEVICE_TARGET_PARAM = "target_device_id";
/** Parameters for the add automation element dialog. */
export interface AddAutomationElementDialogParams {
type: "trigger" | "condition" | "action";
add: (key: string, target?: HassServiceTarget) => void;
clipboardItem: string | undefined;
clipboardPasteToastBottomOffset?: number;
}
/** Get the target from the query parameters. */
export const getAddAutomationElementTargetFromQuery = (
states: HomeAssistant["states"],
devices: HomeAssistant["devices"],
type: AddAutomationElementDialogParams["type"]
): SingleHassServiceTarget | undefined => {
const params = new URLSearchParams(window.location.search);
if (params.get(ADD_AUTOMATION_ELEMENT_QUERY_PARAM) !== type) {
return undefined;
}
const entityId = params.get(ADD_AUTOMATION_ELEMENT_ENTITY_TARGET_PARAM);
if (entityId && states[entityId]) {
return { entity_id: entityId };
}
const deviceId = params.get(ADD_AUTOMATION_ELEMENT_DEVICE_TARGET_PARAM);
if (deviceId && devices[deviceId]) {
return { device_id: deviceId };
}
return undefined;
};
const loadDialog = () => import("./add-automation-element-dialog");
/** Show the add automation element dialog. */
export const showAddAutomationElementDialog = (
element: HTMLElement,
dialogParams: AddAutomationElementDialogParams
): void => {
const params = new URLSearchParams(window.location.search);
fireEvent(element, "show-dialog", {
dialogTag: "add-automation-element-dialog",
dialogImport: loadDialog,
dialogParams,
addHistory:
params.get(ADD_AUTOMATION_ELEMENT_QUERY_PARAM) !== dialogParams.type,
});
};
@@ -3,7 +3,6 @@ import {
mdiAppleKeyboardCommand,
mdiCheckboxBlankOutline,
mdiCheckboxOutline,
mdiCommentEditOutline,
mdiContentCopy,
mdiContentCut,
mdiContentPaste,
@@ -27,8 +26,8 @@ import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
import "../../../../components/ha-dropdown-item";
import { ACTION_BUILDING_BLOCKS } from "../../../../data/action";
import type { ActionSidebarConfig } from "../../../../data/automation";
import type { DomainManifestLookup } from "../../../../data/integration";
import { domainToName } from "../../../../data/integration";
import type { DomainManifestLookup } from "../../../../data/integration";
import type {
NonConditionAction,
RepeatAction,
@@ -39,7 +38,6 @@ import { isMac } from "../../../../util/is_mac";
import type HaAutomationConditionEditor from "../action/ha-automation-action-editor";
import { getAutomationActionType } from "../action/ha-automation-action-row";
import { getRepeatType } from "../action/types/ha-automation-action-repeat";
import "../ha-automation-comment";
import { overflowStyles, sidebarEditorStyles } from "../styles";
import "./ha-automation-sidebar-card";
@@ -177,15 +175,6 @@ export default class HaAutomationSidebarAction extends LitElement {
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-dropdown-item>
<ha-dropdown-item slot="menu-items" value="edit_comment">
<ha-svg-icon slot="icon" .path=${mdiCommentEditOutline}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
`ui.panel.config.automation.editor.comment.${this.config.config.action.comment ? "edit" : "add"}`
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-dropdown-item>
<wa-divider slot="menu-items"></wa-divider>
<ha-dropdown-item
@@ -388,12 +377,6 @@ export default class HaAutomationSidebarAction extends LitElement {
@ui-mode-not-available=${this._handleUiModeNotAvailable}
></ha-automation-action-editor>`
)}
${this.config.config.action.comment?.trim() && !this.yamlMode
? html`<ha-automation-comment
@edit-comment=${this.config.editComment}
.comment=${this.config.config.action.comment}
></ha-automation-comment>`
: nothing}
</ha-automation-sidebar-card>`;
}
@@ -442,9 +425,6 @@ export default class HaAutomationSidebarAction extends LitElement {
case "rename":
this.config.rename();
break;
case "edit_comment":
this.config.editComment();
break;
case "run":
this.config.run();
break;
@@ -1,7 +1,6 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import {
mdiAppleKeyboardCommand,
mdiCommentEditOutline,
mdiContentCopy,
mdiContentCut,
mdiContentPaste,
@@ -35,7 +34,6 @@ import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import "../condition/ha-automation-condition-editor";
import type HaAutomationConditionEditor from "../condition/ha-automation-condition-editor";
import "../ha-automation-comment";
import { overflowStyles, sidebarEditorStyles } from "../styles";
import "./ha-automation-sidebar-card";
@@ -151,19 +149,6 @@ export default class HaAutomationSidebarCondition extends LitElement {
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-dropdown-item>
<ha-dropdown-item
slot="menu-items"
value="edit_comment"
.disabled=${this.disabled}
>
<ha-svg-icon slot="icon" .path=${mdiCommentEditOutline}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
`ui.panel.config.automation.editor.comment.${this.config.config.comment ? "edit" : "add"}`
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-dropdown-item>
<wa-divider slot="menu-items"></wa-divider>
@@ -347,12 +332,6 @@ export default class HaAutomationSidebarCondition extends LitElement {
sidebar
></ha-automation-condition-editor>`
)}
${this.config.config.comment?.trim() && !this.yamlMode
? html`<ha-automation-comment
@edit-comment=${this.config.editComment}
.comment=${this.config.config.comment}
></ha-automation-comment>`
: nothing}
<div class="testing-wrapper">
<div
class="testing ${classMap({
@@ -417,9 +396,6 @@ export default class HaAutomationSidebarCondition extends LitElement {
case "rename":
this.config.rename();
break;
case "edit_comment":
this.config.editComment();
break;
case "test":
this.config.test();
break;

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