Compare commits

..

12 Commits

Author SHA1 Message Date
Aidan Timson 2593dfed8d Type assertion and signature improvements for hui changed handlers (#52109)
* Type assertion and signature improvements

* Improve
2026-05-19 20:06:20 +02:00
pcan08 2d92f1fb3b Forget filter from url: remaining pages (#52061)
* refactor: use separate storage and display filters in backup page

Apply the two-lists pattern in backup page: _filters (@state, display only) +
_storageFilters (@storage sessionStorage, state: false). _storageFilters
is only updated when not in URL mode (_fromUrl flag). Init moved from
connectedCallback to willUpdate(!hasUpdated).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: use separate storage and display filters in scenes page

Apply the two-lists pattern in scenes page: _filters (@state, display only) +
_storageFilters (@storage sessionStorage, state: false, with
serializer/deserializer). _storageFilters is only updated when not in
URL mode (_fromUrl flag). Init moved from firstUpdated to
willUpdate(!hasUpdated). The existing updated() hook already calls
_applyFilters() when _entityReg changes, covering the reconnect case.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: use separate storage and display filters in automations page

Apply the two-lists pattern in automations page: _filters (@state, display only) +
_storageFilters (@storage sessionStorage, state: false, with
serializer/deserializer). _storageFilters is only updated when not in
URL mode (_fromUrl flag). _fromUrl is set before the await in the async
_filterBlueprint() to prevent any user change during the fetch from
persisting. Init moved from firstUpdated to willUpdate(!hasUpdated).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: use separate storage and display filters in scripts page

Apply the two-lists pattern in scripts page: _filters (@state, display only) +
_storageFilters (@storage sessionStorage, state: false, with
serializer/deserializer). _storageFilters is only updated when not in
URL mode (_fromUrl flag). _fromUrl is set before the await in the async
_filterBlueprint() to prevent any user change during the fetch from
persisting. Init moved from firstUpdated to willUpdate(!hasUpdated).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: don't mix URL filters with storage filters in automation,script and scene pages

When URL params are present, _filters starts empty so URL methods build
from scratch. Previously, _filters was pre-populated from _storageFilters
and the spread in _filterLabel()/_filterBlueprint() would merge storage
filters into the URL-injected ones.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Update src/panels/config/backup/ha-config-backup-backups.ts

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2026-05-19 17:47:47 +00:00
pcan08 8cff4c6bd2 Helpers page: forget filter from url (#51989)
* fix(helpers): clear URL-injected filters on leaving helpers dashboard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(helpers): restore previous filters after URL-injected navigation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor: use separate storage and display filters

Apply the same pattern as devices and entities pages: split _filters into
a display-only @state and a _storageFilters persisted to sessionStorage.
_storageFilters is only updated when not in URL mode (_fromUrl flag), so
URL-injected filters never persist to storage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: reapply filters when helper entities load on reconnect

_applyFilters() was never called when _filters was restored from
sessionStorage, leaving _filteredHelperEntityIds undefined and the
table appearing empty. Call it whenever _helperEntities updates and
active filters are present.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-19 19:43:31 +02:00
renovate[bot] 5aa8455861 Update tsparticles to v4.0.2 (#52110) 2026-05-19 14:08:32 +00:00
renovate[bot] 4d142734d8 Migrate Renovate config (#52105) 2026-05-19 14:07:06 +00:00
Aidan Timson eaecc76f36 Add to automation/script with triggers/conditions/actions (#51871)
* Setup default add to actions

* Setup default add to actions

* Move event into external only

* Split into sections

* Padding

* Refactor to single type and adapt app interface to frontend style and vice versa

* Refactor to single type and adapt app interface to frontend style and vice versa

* Condition action and navigation actions

* Open dialogs with trigger, condition, action dialogs

* Add divider before add to

* Move add to to the top

* Action

* Triggers and conditions labs feature check

* Suggestion

* Keep query state

* Change to automation_trigger

* Use typed key instead of finding with icon

* Apply suggestions from code review

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>

* Finish

* Reset state

* Fix navigation resets

* stated

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>

* Split

* Add import, sort imports

---------

Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2026-05-19 14:57:44 +02:00
Aidan Timson 7dc0033c03 Improve typing on value-changed handlers in card features and state controls (#52107) 2026-05-19 14:28:52 +02:00
Paul Bottein 601e6d0542 Distinguish unknown from unavailable entity state (#52089) 2026-05-19 15:24:50 +03:00
Aidan Timson c7ca3dd837 Typing assertion and generic improvements on area controls and conditions (#52106) 2026-05-19 15:11:31 +03:00
renovate[bot] f75a376add Update dependency lint-staged to v17.0.5 (#52104) 2026-05-19 12:54:34 +01:00
Aidan Timson a541204ffb Match python version with core version (#52102) 2026-05-19 13:27:31 +02:00
Aidan Timson cbbce90eae Remove YARN_VERSION from netlify.toml (inherit packageManager) (#52101) 2026-05-19 13:26:33 +02:00
136 changed files with 1181 additions and 1234 deletions
-1
View File
@@ -1,3 +1,2 @@
[build.environment]
YARN_VERSION = "1.22.11"
NODE_OPTIONS = "--max_old_space_size=6144"
+3 -3
View File
@@ -74,8 +74,8 @@
"@replit/codemirror-indentation-markers": "6.5.3",
"@swc/helpers": "0.5.21",
"@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "4.0.1",
"@tsparticles/preset-links": "4.0.1",
"@tsparticles/engine": "4.0.2",
"@tsparticles/preset-links": "4.0.2",
"@vibrant/color": "4.0.4",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
"@webcomponents/webcomponentsjs": "2.8.0",
@@ -187,7 +187,7 @@
"jsdom": "29.1.1",
"jszip": "3.10.1",
"license-checker-rseidelsohn": "4.4.2",
"lint-staged": "17.0.4",
"lint-staged": "17.0.5",
"lit-analyzer": "2.0.3",
"lodash.merge": "4.6.2",
"lodash.template": "4.18.1",
+39
View File
@@ -18,7 +18,46 @@
"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: T[]) {
export function getAllCombinations<T>(arr: readonly T[]): T[][] {
return arr.reduce<T[][]>(
(combinations, element) =>
combinations.concat(
@@ -1,5 +1,5 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { isUnavailableState } from "../../data/entity/entity";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
import type { HomeAssistant } from "../../types";
interface EntityUnitStubConfig {
@@ -21,32 +21,24 @@ export const computeEntityUnitDisplay = (
stateObj: HassEntity | undefined,
config: EntityUnitStubConfig
): string => {
let unit;
if (
stateObj &&
!isUnavailableState(stateObj.state) &&
(config.attribute || stateObj.attributes.device_class !== "duration")
!stateObj ||
stateObj.state === UNAVAILABLE ||
stateObj.state === UNKNOWN ||
(!config.attribute && stateObj.attributes.device_class === "duration")
) {
// 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 ?? "";
return "";
}
return "";
// 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 ?? "";
};
+2 -2
View File
@@ -1,5 +1,5 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { UNAVAILABLE_STATES } from "../../data/entity/entity";
import { UNAVAILABLE, UNKNOWN } 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_STATES);
result.push(UNAVAILABLE, UNKNOWN);
}
if (!attribute && domain in FIXED_DOMAIN_STATES) {
+11 -5
View File
@@ -1,5 +1,5 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { isUnavailableState, UNAVAILABLE } from "../../data/entity/entity";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
import type { HomeAssistant } from "../../types";
import { computeStateDomain } from "./compute_state_domain";
@@ -8,14 +8,20 @@ export const computeGroupEntitiesState = (states: HassEntity[]): string => {
return UNAVAILABLE;
}
const validState = states.some(
(stateObj) => !isUnavailableState(stateObj.state)
const allUnavailable = states.every(
(stateObj) => stateObj.state === UNAVAILABLE
);
if (!validState) {
if (allUnavailable) {
return UNAVAILABLE;
}
const hasValidState = states.some(
(stateObj) => stateObj.state !== UNAVAILABLE && stateObj.state !== UNKNOWN
);
if (!hasValidState) {
return UNKNOWN;
}
// Use the first state to determine the domain
// This assumes all states in the group have the same domain
const domain = computeStateDomain(states[0]);
+2 -2
View File
@@ -1,5 +1,5 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { isUnavailableState, OFF, UNAVAILABLE } from "../../data/entity/entity";
import { OFF, UNAVAILABLE, UNKNOWN } 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 (isUnavailableState(compareState)) {
if (compareState === UNAVAILABLE || compareState === UNKNOWN) {
return false;
}
@@ -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;
+3 -6
View File
@@ -6,11 +6,7 @@ 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,
isUnavailableState,
} from "../../data/entity/entity";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
import { forwardHaptic } from "../../data/haptics";
import type { HomeAssistant } from "../../types";
import "../ha-formfield";
@@ -20,7 +16,8 @@ import "../ha-switch";
const isOn = (stateObj?: HassEntity) =>
stateObj !== undefined &&
!STATES_OFF.includes(stateObj.state) &&
!isUnavailableState(stateObj.state);
stateObj.state !== UNAVAILABLE &&
stateObj.state !== UNKNOWN;
/**
* @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 { isUnavailableState, UNAVAILABLE } from "../../data/entity/entity";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity/entity";
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
import { timerTimeRemaining } from "../../data/timer";
import type { HomeAssistant } from "../../types";
@@ -170,7 +170,8 @@ export class HaStateLabelBadge extends LitElement {
}
// eslint-disable-next-line: disable=no-fallthrough
default:
return isUnavailableState(entityState.state)
return entityState.state === UNAVAILABLE ||
entityState.state === UNKNOWN
? "—"
: this.hass!.formatEntityStateToParts(entityState).find(
(part) => part.type === "value"
@@ -209,7 +210,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 (isUnavailableState(entityState.state)) {
if (entityState.state === UNAVAILABLE || entityState.state === UNKNOWN) {
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: readonly AreaControlDomain[] = [
const AREA_CONTROL_DOMAINS = [
"light",
"fan",
"switch",
@@ -43,7 +43,7 @@ const AREA_CONTROL_DOMAINS: readonly AreaControlDomain[] = [
"cover-door",
"cover-window",
"cover-damper",
] as const;
] as const satisfies readonly AreaControlDomain[];
@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 as unknown as AreaControlDomain[],
AREA_CONTROL_DOMAINS,
areaId,
excludeEntities,
this.hass
+9 -4
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 { isUnavailableState, OFF } from "../data/entity/entity";
import { OFF, UNAVAILABLE, UNKNOWN } from "../data/entity/entity";
import type { HomeAssistant } from "../types";
@customElement("ha-climate-state")
@@ -14,9 +14,11 @@ 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">
${!isUnavailableState(this.stateObj.state)
${!noValue
? html`<span class="state-label">
${this._localizeState()}
${this.stateObj.attributes.preset_mode &&
@@ -32,7 +34,7 @@ class HaClimateState extends LitElement {
: this._localizeState()}
</div>
${currentStatus && !isUnavailableState(this.stateObj.state)
${currentStatus && !noValue
? html`
<div class="current">
${this.hass.localize("ui.card.climate.currently")}:
@@ -119,7 +121,10 @@ class HaClimateState extends LitElement {
}
private _localizeState(): string {
if (isUnavailableState(this.stateObj.state)) {
if (
this.stateObj.state === UNAVAILABLE ||
this.stateObj.state === UNKNOWN
) {
return this.hass.localize(`state.default.${this.stateObj.state}`);
}
+11 -4
View File
@@ -6,11 +6,18 @@ import type { HaIconButton } from "./ha-icon-button";
/**
* Event type for the ha-dropdown component when an item is selected.
* @param T - The type of the value of the selected item.
* @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`.
*/
export type HaDropdownSelectEvent<T = string> = CustomEvent<{
item: Omit<HaDropdownItem, "value"> & { value: T };
}>;
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 };
}>;
/**
* Home Assistant dropdown component
+9 -4
View File
@@ -1,7 +1,7 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { isUnavailableState, OFF } from "../data/entity/entity";
import { OFF, UNAVAILABLE, UNKNOWN } from "../data/entity/entity";
import type { HumidifierEntity } from "../data/humidifier";
import type { HomeAssistant } from "../types";
@@ -13,9 +13,11 @@ 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">
${!isUnavailableState(this.stateObj.state)
${!noValue
? html`<span class="state-label">
${this._localizeState()}
${this.stateObj.attributes.mode
@@ -30,7 +32,7 @@ class HaHumidifierState extends LitElement {
: this._localizeState()}
</div>
${currentStatus && !isUnavailableState(this.stateObj.state)
${currentStatus && !noValue
? html`<div class="current">
${this.hass.localize("ui.card.humidifier.currently")}:
<div class="unit">${currentStatus}</div>
@@ -69,7 +71,10 @@ class HaHumidifierState extends LitElement {
}
private _localizeState(): string {
if (isUnavailableState(this.stateObj.state)) {
if (
this.stateObj.state === UNAVAILABLE ||
this.stateObj.state === UNKNOWN
) {
return this.hass.localize(`state.default.${this.stateObj.state}`);
}
@@ -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 } from "../../common/dom/fire_event";
import { fireEvent, type HASSDomEvent } 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: CustomEvent) {
private _valueChanged(ev: HASSDomEvent<HASSDomEvents["value-changed"]>) {
ev.stopPropagation();
fireEvent(this, "value-changed", {
value: Number((ev.detail as any).value),
value: Number(ev.detail.value),
});
}
}
-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}
>
+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 { isUnavailableState } from "../../data/entity/entity";
import { UNAVAILABLE } 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 &&
isUnavailableState(this.hass.states[this.entityId]?.state)
this.hass.states[this.entityId]?.state === UNAVAILABLE
) {
this._setError({
message: this.hass.localize(
+1 -1
View File
@@ -62,7 +62,7 @@ export const AREA_CONTROLS_BUTTONS: Record<
};
export const getAreaControlEntities = (
controls: AreaControlDomain[],
controls: readonly 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 -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 { isUnavailableState } from "./entity/entity";
import { UNAVAILABLE } 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" &&
!isUnavailableState(hass.states[eid].state) &&
hass.states[eid].state !== UNAVAILABLE &&
hass.entities[eid]?.hidden !== true
)
.sort()
-1
View File
@@ -12,7 +12,6 @@ import {
export interface DeviceAutomation {
alias?: string;
comment?: string;
device_id: string;
domain: string;
entity_id?: string;
-2
View File
@@ -6,10 +6,8 @@ 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;
-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[];
}
+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 { isUnavailableState } from "./entity/entity";
import { UNAVAILABLE } from "./entity/entity";
export interface TodoList {
entity_id: string;
@@ -49,7 +49,7 @@ export const getTodoLists = (
.filter(
(entityId) =>
computeDomain(entityId) === "todo" &&
!isUnavailableState(hass.states[entityId].state) &&
hass.states[entityId].state !== UNAVAILABLE &&
(includeHidden || hass.entities[entityId]?.hidden !== true)
)
.map((entityId) => ({
@@ -18,7 +18,7 @@ import "../../../components/ha-slider";
import "../../../components/ha-time-input";
import "../../../components/input/ha-input";
import { isTiltOnly } from "../../../data/cover";
import { isUnavailableState, UNAVAILABLE } from "../../../data/entity/entity";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
import type { ImageEntity } from "../../../data/image";
import { computeImageUrl } from "../../../data/image";
import "../../../panels/lovelace/components/hui-timestamp-display";
@@ -108,14 +108,13 @@ 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=${isUnavailableState(stateObj.state)}
>
<ha-button appearance="plain" size="small" .disabled=${disabled}>
${this.hass.localize("ui.card.button.press")}
</ha-button>
`;
@@ -151,19 +150,15 @@ class EntityPreviewRow extends LitElement {
return html`
<ha-date-input
.locale=${this.hass.locale}
.disabled=${isUnavailableState(stateObj.state)}
.value=${isUnavailableState(stateObj.state)
? undefined
: stateObj.state}
.disabled=${disabled}
.value=${noValue ? undefined : stateObj.state}
>
</ha-date-input>
`;
}
if (domain === "datetime") {
const dateObj = isUnavailableState(stateObj.state)
? undefined
: new Date(stateObj.state);
const dateObj = noValue ? 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`
@@ -172,12 +167,12 @@ class EntityPreviewRow extends LitElement {
.label=${computeStateName(stateObj)}
.locale=${this.hass.locale}
.value=${date}
.disabled=${isUnavailableState(stateObj.state)}
.disabled=${disabled}
>
</ha-date-input>
<ha-time-input
.value=${time}
.disabled=${isUnavailableState(stateObj.state)}
.disabled=${disabled}
.locale=${this.hass.locale}
></ha-time-input>
</div>
@@ -187,7 +182,7 @@ class EntityPreviewRow extends LitElement {
if (domain === "event") {
return html`
<div class="when">
${isUnavailableState(stateObj.state)
${noValue
? this.hass.formatEntityState(stateObj)
: html`<hui-timestamp-display
.hass=${this.hass}
@@ -196,7 +191,7 @@ class EntityPreviewRow extends LitElement {
></hui-timestamp-display>`}
</div>
<div class="what">
${isUnavailableState(stateObj.state)
${noValue
? nothing
: this.hass.formatEntityAttributeValue(stateObj, "event_type")}
</div>
@@ -206,9 +201,7 @@ class EntityPreviewRow extends LitElement {
const toggleDomains = ["fan", "light", "remote", "siren", "switch"];
if (toggleDomains.includes(domain)) {
const showToggle =
stateObj.state === "on" ||
stateObj.state === "off" ||
isUnavailableState(stateObj.state);
stateObj.state === "on" || stateObj.state === "off" || noValue;
return html`
${showToggle
? html`
@@ -241,7 +234,7 @@ class EntityPreviewRow extends LitElement {
if (domain === "lock") {
return html`
<ha-button
.disabled=${isUnavailableState(stateObj.state)}
.disabled=${disabled}
class="text-content"
appearance="plain"
size="small"
@@ -266,7 +259,7 @@ class EntityPreviewRow extends LitElement {
<div class="numberflex">
<ha-slider
labeled
.disabled=${stateObj.state === UNAVAILABLE}
.disabled=${disabled}
.step=${Number(stateObj.attributes.step)}
.min=${Number(stateObj.attributes.min)}
.max=${Number(stateObj.attributes.max)}
@@ -280,7 +273,7 @@ class EntityPreviewRow extends LitElement {
: html`<div class="numberflex numberstate">
<ha-input
auto-validate
.disabled=${stateObj.state === UNAVAILABLE}
.disabled=${disabled}
pattern="[0-9]+([\\.][0-9]+)?"
.step=${Number(stateObj.attributes.step)}
.min=${Number(stateObj.attributes.min)}
@@ -303,7 +296,7 @@ class EntityPreviewRow extends LitElement {
<ha-select
.label=${computeStateName(stateObj)}
.value=${stateObj.state}
.disabled=${stateObj.state === UNAVAILABLE}
.disabled=${disabled}
.options=${stateObj.attributes.options?.map((option) => ({
value: option,
label: this.hass!.formatEntityState(stateObj, option),
@@ -317,7 +310,7 @@ class EntityPreviewRow extends LitElement {
const showSensor =
SENSOR_TIMESTAMP_DEVICE_CLASSES.includes(
stateObj.attributes.device_class
) && !isUnavailableState(stateObj.state);
) && !noValue;
return html`
${showSensor
? html`
@@ -339,7 +332,7 @@ class EntityPreviewRow extends LitElement {
return html`
<ha-input
.label=${computeStateName(stateObj)}
.disabled=${isUnavailableState(stateObj.state)}
.disabled=${disabled}
.value=${stateObj.state}
.minlength=${stateObj.attributes.min}
.maxlength=${stateObj.attributes.max}
@@ -354,11 +347,9 @@ class EntityPreviewRow extends LitElement {
if (domain === "time") {
return html`
<ha-time-input
.value=${isUnavailableState(stateObj.state)
? undefined
: stateObj.state}
.value=${noValue ? undefined : stateObj.state}
.locale=${this.hass.locale}
.disabled=${isUnavailableState(stateObj.state)}
.disabled=${disabled}
></ha-time-input>
`;
}
@@ -366,7 +357,7 @@ class EntityPreviewRow extends LitElement {
if (domain === "weather") {
return html`
<div>
${isUnavailableState(stateObj.state) ||
${noValue ||
stateObj.attributes.temperature === undefined ||
stateObj.attributes.temperature === null
? this.hass.formatEntityState(stateObj)
+3 -16
View File
@@ -9,7 +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 "../../components/input/ha-input";
import type { HaInput } from "../../components/input/ha-input";
import type { HomeAssistant } from "../../types";
@@ -29,7 +28,7 @@ class DialogBox extends LitElement {
@state() private _validInput = true;
@query("ha-input, ha-textarea") private _textField?: HaInput;
@query("ha-input") private _textField?: HaInput;
private _closePromise?: Promise<void>;
@@ -110,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
@@ -132,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
+116
View File
@@ -0,0 +1,116 @@
import { navigate } from "../../common/navigate";
import type { LocalizeFunc } from "../../common/translations/localize";
import { createSearchParam } from "../../common/url/search-params";
import {
ADD_AUTOMATION_ELEMENT_QUERY_PARAM,
ADD_AUTOMATION_ELEMENT_TARGET_PARAM,
} from "../../panels/config/automation/show-add-automation-element-dialog";
import type { HomeAssistant, TranslationDict } from "../../types";
type DefaultActionKey =
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: DefaultActionKey;
}
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: DefaultActionKey;
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}`,
{
entity:
states[entityId] !== undefined
? formatEntityName(states[entityId], undefined)
: entityId,
}
),
icon: def.icon,
})
);
export function defaultActionHandler(
key: (typeof DEFAULT_ACTION_DEFS)[number]["translation_key"],
entityId: string
): Promise<boolean> {
const params = (addElement: string) =>
`?${createSearchParam({
[ADD_AUTOMATION_ELEMENT_QUERY_PARAM]: addElement,
[ADD_AUTOMATION_ELEMENT_TARGET_PARAM]: entityId,
})}`;
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 { isUnavailableState } from "../../../data/entity/entity";
import { UNAVAILABLE, UNKNOWN } 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,7 +24,8 @@ export class HaMoreInfoStateHeader extends LitElement {
private _localizeState(): TemplateResult | string {
if (
this.stateObj.attributes.device_class === SENSOR_DEVICE_CLASS_TIMESTAMP &&
!isUnavailableState(this.stateObj.state)
this.stateObj.state !== UNAVAILABLE &&
this.stateObj.state !== UNKNOWN
) {
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 { isUnavailableState } from "../../../data/entity/entity";
import { UNAVAILABLE } 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=${isUnavailableState(this.stateObj!.state)}
.disabled=${this.stateObj!.state === UNAVAILABLE}
>
${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 { isUnavailableState } from "../../../data/entity/entity";
import { UNAVAILABLE } 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 = isUnavailableState(this.stateObj.state);
const disabled = this.stateObj.state === UNAVAILABLE;
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 { isUnavailableState, UNAVAILABLE } from "../../../data/entity/entity";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
@customElement("more-info-date")
@@ -21,10 +21,9 @@ class MoreInfoDate extends LitElement {
return html`
<ha-date-input
.locale=${this.hass.locale}
.value=${isUnavailableState(this.stateObj.state)
.value=${this.stateObj.state === UNKNOWN
? 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 { isUnavailableState, UNAVAILABLE } from "../../../data/entity/entity";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
@customElement("more-info-datetime")
@@ -19,23 +19,22 @@ class MoreInfoDatetime extends LitElement {
return nothing;
}
const dateObj = isUnavailableState(this.stateObj.state)
? undefined
: new Date(this.stateObj.state);
const dateObj =
this.stateObj.state === UNKNOWN
? 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>`;
@@ -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 { isUnavailableState } from "../../../data/entity/entity";
import { UNAVAILABLE } 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=${isUnavailableState(stateObj.state) || !this._canRun()}
.disabled=${stateObj.state === UNAVAILABLE || !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 { isUnavailableState, UNAVAILABLE } from "../../../data/entity/entity";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
import { setTimeValue } from "../../../data/time";
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
@@ -20,11 +20,10 @@ class MoreInfoTime extends LitElement {
return html`
<ha-time-input
.value=${isUnavailableState(this.stateObj.state)
.value=${this.stateObj.state === UNKNOWN
? 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 { isUnavailableState } from "../../../data/entity/entity";
import { UNAVAILABLE, UNKNOWN } 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,7 +176,8 @@ class MoreInfoUpdate extends LitElement {
if (
!this.hass ||
!this.stateObj ||
isUnavailableState(this.stateObj.state)
this.stateObj.state === UNAVAILABLE ||
this.stateObj.state === UNKNOWN
) {
return nothing;
}
+123 -55
View File
@@ -5,14 +5,18 @@ import "../../components/ha-icon";
import "../../components/ha-spinner";
import "../../components/item/ha-list-item-button";
import "../../components/list/ha-list-base";
import type {
ExternalEntityAddToAction,
ExternalEntityAddToActions,
} from "../../external_app/external_messaging";
import type { HaListItemButton } from "../../components/item/ha-list-item-button";
import { showToast } from "../../util/toast";
import type { HASSDomCurrentTargetEvent } from "../../common/dom/fire_event";
import { fireEvent } from "../../common/dom/fire_event";
import type { HomeAssistant } from "../../types";
import {
type EntityAddToAction,
type EntityAddToActions,
defaultActionHandler,
getDefaultAddToActions,
} from "./add-to";
@customElement("ha-more-info-add-to")
export class HaMoreInfoAddTo extends LitElement {
@@ -20,54 +24,112 @@ export class HaMoreInfoAddTo extends LitElement {
@property({ attribute: false }) public entityId!: string;
@state() private _externalActions?: ExternalEntityAddToActions = {
actions: [],
};
@state() private _defaultActions: EntityAddToActions = [];
@state() private _externalActions: EntityAddToActions = [];
@state() private _loading = true;
private async _loadExternalActions() {
private async _loadActions() {
this._defaultActions = getDefaultAddToActions(
this.hass.states,
this.hass.localize,
this.hass.formatEntityName,
this.entityId
);
this._externalActions = [];
if (this.hass.auth.external?.config.hasEntityAddTo) {
this._externalActions =
await this.hass.auth.external?.sendMessage<"entity/add_to/get_actions">(
{
type: "entity/add_to/get_actions",
payload: { entity_id: this.entityId },
}
);
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);
}
}
}
private async _actionSelected(ev: CustomEvent) {
const action = (ev.currentTarget as any)
.action as ExternalEntityAddToAction;
private async _actionSelected(
ev: HASSDomCurrentTargetEvent<
HaListItemButton & {
action: EntityAddToAction;
}
>
) {
const action = ev.currentTarget.action;
if (!action.enabled) {
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 === "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;
}
if (action.type !== "default") {
return;
}
defaultActionHandler(action.key, 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._loadExternalActions();
await this._loadActions();
this._loading = false;
}
@@ -80,7 +142,7 @@ export class HaMoreInfoAddTo extends LitElement {
`;
}
if (!this._externalActions?.actions.length) {
if (!this._defaultActions.length && !this._externalActions.length) {
return html`
<ha-alert alert-type="info">
${this.hass.localize(
@@ -92,30 +154,27 @@ export class HaMoreInfoAddTo extends LitElement {
return html`
<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>
`
)}
${this._renderActionItems(this._defaultActions)}
</ha-list-base>
${this._externalActions.length
? html`
<h2 class="section-title">
${this.hass.localize(
"ui.dialogs.more_info_control.add_to.app_actions"
)}
</h2>
<ha-list-base>
${this._renderActionItems(this._externalActions)}
</ha-list-base>
`
: nothing}
`;
}
static styles = css`
:host {
display: block;
padding: var(--ha-space-2) var(--ha-space-6) var(--ha-space-6)
var(--ha-space-6);
padding: var(--ha-space-3) 0 var(--ha-space-4);
}
.loading {
@@ -125,6 +184,15 @@ 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;
+45 -19
View File
@@ -1,3 +1,4 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import {
mdiBackupRestore,
mdiChartBoxOutline,
@@ -17,13 +18,13 @@ import {
} from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { css, html, LitElement, 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 type { HASSDomEvent } from "../../common/dom/fire_event";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import type { HASSDomEvent } from "../../common/dom/fire_event";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { computeAreaName } from "../../common/entity/compute_area_name";
@@ -57,10 +58,12 @@ 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,
@@ -70,12 +73,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";
@@ -117,7 +120,9 @@ declare global {
const DEFAULT_VIEW: MoreInfoView = "info";
@customElement("ha-more-info-dialog")
export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
export class MoreInfoDialog extends SubscribeMixin(
ScrollableFadeMixin(LitElement)
) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean, reflect: true }) public large = false;
@@ -156,6 +161,8 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
@state() private _sensorNumericDeviceClasses?: string[] = [];
@state() private _newTriggersAndConditions = false;
protected scrollFadeThreshold = 24;
protected get scrollableElement(): HTMLElement | null {
@@ -254,7 +261,24 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
}
private _shouldShowAddEntityTo(): boolean {
return !!this.hass.auth.external?.config.hasEntityAddTo;
// When 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;
}
),
];
}
private _getDeviceId(): string | null {
@@ -680,6 +704,21 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
.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_entity_to"
)}
</ha-dropdown-item>
<wa-divider></wa-divider>
`
: nothing}
${supportsFavorites
? html`
<ha-dropdown-item value="toggle_edit">
@@ -769,19 +808,6 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
"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()
@@ -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,
@@ -296,13 +294,6 @@ export default class HaAutomationActionRow extends LitElement {
?.target
: undefined;
const trimmedComment = this.action.comment?.trim() || "";
const commentTooltipText = !trimmedComment
? ""
: trimmedComment.length > 250
? `${trimmedComment.substring(0, 250)}...`
: trimmedComment;
return html`
${type === "service" && "action" in this.action && this.action.action
? html`
@@ -338,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
@@ -408,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
@@ -942,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");
};
@@ -1090,7 +1026,6 @@ export default class HaAutomationActionRow extends LitElement {
rename: () => {
this._renameAction();
},
editComment: this._editCommentAction,
toggleYamlMode: () => {
this._toggleYamlMode();
this.openSidebar();
@@ -1186,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,6 +18,7 @@ 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";
@@ -41,6 +42,8 @@ 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;
}
@@ -133,6 +136,32 @@ 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,
"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)
@@ -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,6 +33,7 @@ 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";
@@ -128,7 +129,12 @@ 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 { PASTE_VALUE } from "./show-add-automation-element-dialog";
import {
ADD_AUTOMATION_ELEMENT_QUERY_PARAM,
ADD_AUTOMATION_ELEMENT_TARGET_PARAM,
getAddAutomationElementTargetFromQuery,
PASTE_VALUE,
} from "./show-add-automation-element-dialog";
import { getTargetText } from "./target/get_target_text";
const TYPES = {
@@ -230,6 +236,8 @@ class DialogAddAutomationElement
@state() private _newTriggersAndConditions = false;
@state() private _openedFromQuery = false;
@state() private _conditionDescriptions: ConditionDescriptions = {};
@state()
@@ -295,10 +303,27 @@ class DialogAddAutomationElement
}
}
public showDialog(params): void {
public showDialog(params: AddAutomationElementDialogParams): void {
this._params = params;
this._resetVariables();
const queryEntityId = getAddAutomationElementTargetFromQuery(
this.hass.states,
params.type
);
this._openedFromQuery = !!queryEntityId;
if (queryEntityId) {
const searchParams = new URLSearchParams(mainWindow.location.search);
searchParams.delete(ADD_AUTOMATION_ELEMENT_QUERY_PARAM);
searchParams.delete(ADD_AUTOMATION_ELEMENT_TARGET_PARAM);
mainWindow.history.replaceState(
mainWindow.history.state,
"",
constructUrlCurrentPath(searchParams.toString())
);
}
this.addKeyboardShortcuts();
this._loadConfigEntries();
@@ -314,16 +339,26 @@ class DialogAddAutomationElement
(feature) => {
this._newTriggersAndConditions = feature.enabled;
this._tab = this._newTriggersAndConditions ? "targets" : "groups";
if (
queryEntityId &&
this._newTriggersAndConditions &&
!this._selectedTarget
) {
this._selectedTarget = { entity_id: queryEntityId };
this._getItemsByTarget();
}
}
);
// add initial dialog view state to history
mainWindow.history.pushState(
{
dialogData: {},
},
""
);
if (!queryEntityId) {
// add initial dialog view state to history
mainWindow.history.pushState(
{
dialogData: {},
},
""
);
}
if (this._params?.type === "action") {
this.hass.loadBackendTranslation("services");
@@ -343,6 +378,16 @@ class DialogAddAutomationElement
// prevent view mode switch when resizing window
this._bottomSheetMode = this._narrow;
if (
queryEntityId &&
this._newTriggersAndConditions &&
!this._selectedTarget
) {
this._selectedTarget = { entity_id: queryEntityId };
this._tab = "targets";
this._getItemsByTarget();
}
}
public closeDialog(historyState?: any) {
@@ -407,6 +452,7 @@ class DialogAddAutomationElement
this._narrow = false;
this._targetItems = undefined;
this._loadItemsError = false;
this._openedFromQuery = false;
}
private _updateNarrow = () => {
@@ -891,7 +937,9 @@ class DialogAddAutomationElement
></ha-icon-button>
`
: nothing}
${this._narrow && (this._selectedGroup || this._selectedTarget)
${this._narrow &&
(this._selectedGroup || this._selectedTarget) &&
!this._openedFromQuery
? html`<ha-icon-button-prev
slot="navigationIcon"
@click=${this._back}
@@ -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,
@@ -202,13 +200,6 @@ export default class HaAutomationConditionRow extends LitElement {
const conditionTargetSpec =
this.conditionDescriptions[this.condition.condition]?.target;
const trimmedComment = this.condition.comment?.trim() || "";
const commentTooltipText = !trimmedComment
? ""
: trimmedComment.length > 250
? `${trimmedComment.substring(0, 250)}...`
: trimmedComment;
return html`
<ha-condition-icon
slot="leading-icon"
@@ -226,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}
@@ -288,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>
@@ -845,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");
};
@@ -1018,7 +954,6 @@ export default class HaAutomationConditionRow extends LitElement {
rename: () => {
this._renameCondition();
},
editComment: this._editCommentCondition,
toggleYamlMode: () => {
this._toggleYamlMode();
this.openSidebar();
@@ -1090,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,6 +27,7 @@ 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";
@@ -57,6 +58,8 @@ export default class HaAutomationCondition extends AutomationSortableListMixin<C
private _unsub?: Promise<UnsubscribeFunc>;
private _openedAddDialogFromQuery = false;
protected get items(): Condition[] {
return this.conditions;
}
@@ -118,6 +121,32 @@ export default class HaAutomationCondition extends AutomationSortableListMixin<C
}
protected updated(changedProperties: PropertyValues<this>) {
if (!this.hass) {
return;
}
const addConditionTargetFromQuery = getAddAutomationElementTargetFromQuery(
this.hass.states,
"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;
}
@@ -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,80 +0,0 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
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;
}
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,6 +77,7 @@ 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 {
@@ -572,11 +573,21 @@ 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
) {
@@ -585,10 +596,10 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
}
if (
changedProps.has("automationId") &&
(changedProps.has("automationId") ||
shouldResetNewAutomationConfigFromQuery) &&
!this.automationId &&
!this.entityId &&
this.hass
!this.entityId
) {
const initData = getAutomationEditorInitData();
this.dirty = !!initData;
@@ -169,15 +169,19 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
private _filter = "";
@state()
private _filters: DataTableFilters = {};
@storage({
storage: "sessionStorage",
key: "automation-table-filters-full",
state: true,
state: false,
subscribe: false,
serializer: serializeFilters,
deserializer: deserializeFilters,
})
private _filters: DataTableFilters = {};
private _storageFilters: DataTableFilters = {};
private _fromUrl = false;
@state() private _expandedFilter?: string;
@@ -760,6 +764,23 @@ 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")) {
@@ -767,15 +788,6 @@ 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;
@@ -793,6 +805,9 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
items: undefined,
},
};
if (!this._fromUrl) {
this._storageFilters = this._filters;
}
this._applyFilters();
};
@@ -803,6 +818,9 @@ 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();
}
@@ -858,6 +876,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
if (!label) {
return;
}
this._fromUrl = true;
this._filters = {
...this._filters,
"ha-filter-labels": {
@@ -873,6 +892,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
if (!blueprint) {
return;
}
this._fromUrl = true;
const related = await findRelated(
this.hass,
"automation_blueprint",
@@ -890,6 +910,9 @@ 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,
@@ -39,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,
@@ -140,14 +138,8 @@ export default class HaAutomationOptionRow extends LitElement {
</div>
`;
}
private _renderRow() {
const trimmedComment = this.option?.comment?.trim() || "";
const commentTooltipText = !trimmedComment
? ""
: trimmedComment.length > 250
? `${trimmedComment.substring(0, 250)}...`
: trimmedComment;
private _renderRow() {
return html`
<h3 slot="header">
${this.option
@@ -158,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>
@@ -200,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
@@ -395,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;
@@ -461,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[];
@@ -525,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;
@@ -538,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,8 +1,12 @@
import type { HassServiceTarget } from "home-assistant-js-websocket";
import { fireEvent } from "../../../common/dom/fire_event";
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_TARGET_PARAM = "target_entity_id";
export interface AddAutomationElementDialogParams {
type: "trigger" | "condition" | "action";
add: (key: string, target?: HassServiceTarget) => void;
@@ -11,13 +15,30 @@ export interface AddAutomationElementDialogParams {
}
const loadDialog = () => import("./add-automation-element-dialog");
export const getAddAutomationElementTargetFromQuery = (
states: HomeAssistant["states"],
type: AddAutomationElementDialogParams["type"]
): string | undefined => {
const params = new URLSearchParams(window.location.search);
const entityId = params.get(ADD_AUTOMATION_ELEMENT_TARGET_PARAM);
return params.get(ADD_AUTOMATION_ELEMENT_QUERY_PARAM) === type &&
entityId &&
states[entityId]
? entityId
: undefined;
};
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;
@@ -1,7 +1,6 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import {
mdiAppleKeyboardCommand,
mdiCommentEditOutline,
mdiDelete,
mdiPlusCircleMultipleOutline,
mdiRenameBox,
@@ -15,7 +14,6 @@ import type { OptionSidebarConfig } from "../../../../data/automation";
import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import type HaAutomationConditionEditor from "../action/ha-automation-action-editor";
import "../ha-automation-comment";
import { overflowStyles, sidebarEditorStyles } from "../styles";
import "./ha-automation-sidebar-card";
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
@@ -74,22 +72,6 @@ export default class HaAutomationSidebarOption extends LitElement {
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-dropdown-item>
<ha-dropdown-item
slot="menu-items"
value="edit_comment"
.disabled=${!!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.comment ? "edit" : "add"}`
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-dropdown-item>
<ha-dropdown-item
slot="menu-items"
@@ -144,12 +126,6 @@ export default class HaAutomationSidebarOption extends LitElement {
`}
<div class="description">${description}</div>
${!this.config.defaultOption && this.config.comment?.trim()
? html`<ha-automation-comment
@edit-comment=${this.config.editComment}
.comment=${this.config.comment}
></ha-automation-comment>`
: nothing}
</ha-automation-sidebar-card>`;
}
@@ -164,9 +140,6 @@ export default class HaAutomationSidebarOption extends LitElement {
case "rename":
this.config.rename();
break;
case "edit_comment":
this.config.editComment();
break;
case "duplicate":
this.config.duplicate();
break;
@@ -1,24 +1,18 @@
import {
mdiAppleKeyboardCommand,
mdiCommentEditOutline,
mdiDelete,
mdiPlaylistEdit,
} from "@mdi/js";
import { mdiAppleKeyboardCommand, mdiDelete, mdiPlaylistEdit } from "@mdi/js";
import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { keyed } from "lit/directives/keyed";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
import "../../../../components/ha-dropdown-item";
import type { ScriptFieldSidebarConfig } from "../../../../data/automation";
import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import "../../script/ha-script-field-editor";
import type HaAutomationConditionEditor from "../action/ha-automation-action-editor";
import "../ha-automation-comment";
import { overflowStyles, sidebarEditorStyles } from "../styles";
import "./ha-automation-sidebar-card";
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
@customElement("ha-automation-sidebar-script-field")
export default class HaAutomationSidebarScriptField extends LitElement {
@@ -68,15 +62,6 @@ export default class HaAutomationSidebarScriptField extends LitElement {
@wa-select=${this._handleDropdownSelect}
>
<span slot="title">${title}</span>
<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.field.description ? "edit" : "add"}`
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-dropdown-item>
<ha-dropdown-item
slot="menu-items"
value="toggle_yaml_mode"
@@ -136,12 +121,6 @@ export default class HaAutomationSidebarScriptField extends LitElement {
@yaml-changed=${this._yamlChangedSidebar}
></ha-script-field-editor>`
)}
${this.config.config.field.description?.trim() && !this.yamlMode
? html`<ha-automation-comment
@edit-comment=${this.config.editComment}
.comment=${this.config.config.field.description}
></ha-automation-comment>`
: nothing}
</ha-automation-sidebar-card>`;
}
@@ -189,9 +168,6 @@ export default class HaAutomationSidebarScriptField extends LitElement {
case "toggle_yaml_mode":
this._toggleYamlMode();
break;
case "edit_comment":
this.config.editComment();
break;
case "delete":
this.config.delete();
break;
@@ -1,7 +1,6 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import {
mdiAppleKeyboardCommand,
mdiCommentEditOutline,
mdiContentCopy,
mdiContentCut,
mdiContentPaste,
@@ -19,12 +18,9 @@ import { customElement, property, query, state } from "lit/decorators";
import { keyed } from "lit/directives/keyed";
import { fireEvent } from "../../../../common/dom/fire_event";
import { handleStructError } from "../../../../common/structs/handle-errors";
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
import "../../../../components/ha-dropdown-item";
import type {
LegacyTrigger,
Trigger,
TriggerList,
TriggerSidebarConfig,
} from "../../../../data/automation";
import {
@@ -34,11 +30,11 @@ import {
} from "../../../../data/trigger";
import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import "../ha-automation-comment";
import { overflowStyles, sidebarEditorStyles } from "../styles";
import "../trigger/ha-automation-trigger-editor";
import type HaAutomationTriggerEditor from "../trigger/ha-automation-trigger-editor";
import "./ha-automation-sidebar-card";
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
@customElement("ha-automation-sidebar-trigger")
export default class HaAutomationSidebarTrigger extends LitElement {
@@ -129,24 +125,7 @@ export default class HaAutomationSidebarTrigger extends LitElement {
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-dropdown-item>
${type !== "list"
? html`<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 as Exclude<Trigger, TriggerList>).comment ? "edit" : "add"}`
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-dropdown-item>`
: nothing}
${!this.yamlMode &&
!("id" in this.config.config) &&
!this._requestShowId
@@ -342,14 +321,6 @@ export default class HaAutomationSidebarTrigger extends LitElement {
sidebar
></ha-automation-trigger-editor>`
)}
${!isTriggerList(this.config.config) &&
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}
</ha-automation-sidebar-card>
`;
}
@@ -401,9 +372,6 @@ export default class HaAutomationSidebarTrigger extends LitElement {
case "rename":
this.config.rename();
break;
case "edit_comment":
this.config.editComment();
break;
case "show_id":
this._showTriggerId();
break;
-1
View File
@@ -4,7 +4,6 @@ export const baseTriggerStruct = object({
trigger: string(),
id: optional(string()),
enabled: optional(boolean()),
comment: optional(string()),
});
export const forDictStruct = object({
-12
View File
@@ -52,18 +52,6 @@ export const rowStyles = css`
ha-automation-row-event-chip.event-chip {
position: absolute;
}
.comment-indicator {
color: var(--ha-color-on-neutral-normal);
}
.comment-indicator + ha-tooltip::part(body) {
cursor: default;
max-width: 300px;
}
.comment-indicator + ha-tooltip p {
white-space: pre;
margin: 0;
}
`;
export const editorStyles = css`
@@ -141,7 +141,6 @@ export default class HaAutomationTriggerEditor extends LitElement {
ev.stopPropagation();
const value = {
...(this.trigger.alias ? { alias: this.trigger.alias } : {}),
...(this.trigger.comment ? { comment: this.trigger.comment } : {}),
...ev.detail.value,
};
fireEvent(this, "value-changed", { value });
@@ -4,8 +4,6 @@ import {
mdiAppleKeyboardCommand,
mdiArrowDown,
mdiArrowUp,
mdiCommentEditOutline,
mdiCommentTextOutline,
mdiContentCopy,
mdiContentCut,
mdiContentPaste,
@@ -223,16 +221,6 @@ export default class HaAutomationTriggerRow extends LitElement {
?.target
: undefined;
const trimmedComment =
(type !== "list" &&
(this.trigger as Exclude<Trigger, TriggerList>).comment?.trim()) ||
"";
const commentTooltipText = !trimmedComment
? ""
: trimmedComment.length > 250
? `${trimmedComment.substring(0, 250)}...`
: trimmedComment;
return html`
${type === "list"
? html`<ha-svg-icon
@@ -254,22 +242,6 @@ export default class HaAutomationTriggerRow extends LitElement {
triggerTargetSpec
)
: nothing}
${type !== "list" &&
(this.trigger as Exclude<Trigger, TriggerList>).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._triggered}
@@ -310,19 +282,7 @@ export default class HaAutomationTriggerRow extends LitElement {
)
)}
</ha-dropdown-item>
${type !== "list"
? html`<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.trigger as Exclude<Trigger, TriggerList>).comment ? "edit" : "add"}`
)
)}
</ha-dropdown-item>`
: nothing}
<wa-divider></wa-divider>
<ha-dropdown-item value="duplicate" .disabled=${this.disabled}>
@@ -699,7 +659,6 @@ export default class HaAutomationTriggerRow extends LitElement {
rename: () => {
this._renameTrigger();
},
editComment: this._editCommentTrigger,
toggleYamlMode: () => {
this._toggleYamlMode();
this.openSidebar();
@@ -843,40 +802,6 @@ export default class HaAutomationTriggerRow extends LitElement {
}
};
private _editCommentTrigger = async (): Promise<void> => {
if (isTriggerList(this.trigger)) return;
const trigger = this.trigger;
const comment = await showPromptDialog(this, {
title: this.hass.localize(
`ui.panel.config.automation.editor.comment.${trigger.comment ? "edit" : "add"}`
),
inputLabel: this.hass.localize(
"ui.panel.config.automation.editor.comment.label"
),
inputType: "string",
defaultValue: trigger.comment,
confirmText: this.hass.localize("ui.common.submit"),
multiline: true,
});
if (comment !== null) {
const value = { ...trigger };
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.triggerEditor?.yamlEditor?.setValue(value);
}
}
};
private _duplicateTrigger = () => {
fireEvent(this, "duplicate");
};
@@ -988,9 +913,6 @@ export default class HaAutomationTriggerRow extends LitElement {
case "rename":
this._renameTrigger();
break;
case "edit_comment":
this._editCommentTrigger();
break;
case "duplicate":
this._duplicateTrigger();
break;
@@ -25,6 +25,7 @@ import { isTriggerList, subscribeTriggers } from "../../../../data/trigger";
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";
@@ -52,6 +53,8 @@ export default class HaAutomationTrigger extends AutomationSortableListMixin<Tri
private _unsub?: Promise<UnsubscribeFunc>;
private _openedAddDialogFromQuery = false;
protected get items(): Trigger[] {
return this.triggers;
}
@@ -235,6 +238,32 @@ export default class HaAutomationTrigger extends AutomationSortableListMixin<Tri
protected updated(changedProps: PropertyValues<this>) {
super.updated(changedProps);
if (!this.hass) {
return;
}
const addTriggerTargetFromQuery = getAddAutomationElementTargetFromQuery(
this.hass.states,
"trigger"
);
if (changedProps.has("triggers") && addTriggerTargetFromQuery) {
this._openedAddDialogFromQuery = false;
}
if (
!this._openedAddDialogFromQuery &&
this.root &&
!this.disabled &&
this.triggers.length === 0 &&
addTriggerTargetFromQuery
) {
this._openedAddDialogFromQuery = true;
queueMicrotask(() => this._addTriggerDialog());
} else if (this._openedAddDialogFromQuery && !addTriggerTargetFromQuery) {
this._openedAddDialogFromQuery = false;
}
if (
changedProps.has("triggers") &&
(this.focusLastItemOnChange || this.focusItemIndexOnChange !== undefined)
@@ -212,10 +212,6 @@ export class HaDeviceTrigger extends LitElement {
}
static styles = css`
:host {
display: block;
margin-bottom: var(--ha-space-3);
}
ha-device-picker {
display: block;
margin-bottom: 24px;
@@ -1,8 +1,7 @@
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 { ensureArray } from "../../../../../common/array/ensure-array";
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { hasTemplate } from "../../../../../common/string/has-template";
@@ -11,6 +10,7 @@ import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import type { NumericStateTrigger } from "../../../../../data/automation";
import type { HomeAssistant } from "../../../../../types";
import { ensureArray } from "../../../../../common/array/ensure-array";
@customElement("ha-automation-trigger-numeric_state")
export class HaNumericStateTrigger extends LitElement {
@@ -333,13 +333,6 @@ export class HaNumericStateTrigger extends LitElement {
);
}
};
static styles = css`
:host {
display: block;
margin-bottom: var(--ha-space-3);
}
`;
}
declare global {
@@ -29,7 +29,6 @@ const DEFAULT_KEYS: (keyof PlatformTrigger)[] = [
"trigger",
"target",
"alias",
"comment",
"id",
"variables",
"enabled",
@@ -1,7 +1,6 @@
import type { PropertyValues } from "lit";
import { css, html, LitElement } from "lit";
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import {
array,
assert,
@@ -14,21 +13,22 @@ import {
string,
union,
} from "superstruct";
import memoizeOne from "memoize-one";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import { ensureArray } from "../../../../../common/array/ensure-array";
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { hasTemplate } from "../../../../../common/string/has-template";
import type { LocalizeFunc } from "../../../../../common/translations/localize";
import type { StateTrigger } from "../../../../../data/automation";
import { ANY_STATE_VALUE } from "../../../../../components/entity/const";
import type { HomeAssistant } from "../../../../../types";
import { baseTriggerStruct, forDictStruct } from "../../structs";
import type { TriggerElement } from "../ha-automation-trigger-row";
import "../../../../../components/ha-form/ha-form";
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
import type {
HaFormSchema,
SchemaUnion,
} from "../../../../../components/ha-form/types";
import type { StateTrigger } from "../../../../../data/automation";
import type { HomeAssistant } from "../../../../../types";
import { baseTriggerStruct, forDictStruct } from "../../structs";
import type { TriggerElement } from "../ha-automation-trigger-row";
const stateTriggerStruct = assign(
baseTriggerStruct,
@@ -303,13 +303,6 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
? "ui.components.entity.entity-picker.entity"
: `ui.panel.config.automation.editor.triggers.type.state.${schema.name}`
);
static styles = css`
:host {
display: block;
margin-bottom: var(--ha-space-3);
}
`;
}
declare global {
@@ -7,7 +7,7 @@ import {
mdiPlus,
mdiUpload,
} from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
@@ -104,13 +104,17 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
@state() private _selected: string[] = [];
@state()
private _filters: DataTableFiltersValues = {};
@storage({
storage: "sessionStorage",
key: "backups-table-filters",
state: true,
state: false,
subscribe: false,
})
private _filters: DataTableFiltersValues = {};
private _storageFilters: DataTableFiltersValues = {};
private _fromUrl = false;
@storage({ key: "backups-table-grouping", state: false, subscribe: false })
private _activeGrouping?: string = "formatted_type";
@@ -131,11 +135,18 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
private _overflowBackup?: BackupRow;
protected willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
this._filters = this._storageFilters;
this._setFiltersFromUrl();
}
}
public connectedCallback() {
super.connectedCallback();
window.addEventListener("location-changed", this._locationChanged);
window.addEventListener("popstate", this._popState);
this._setFiltersFromUrl();
}
disconnectedCallback(): void {
@@ -568,14 +579,23 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
private _typeFilterChanged(ev) {
this._filters = { ...this._filters, [TYPE_FILTER]: ev.detail.value };
if (!this._fromUrl) {
this._storageFilters = this._filters;
}
}
private _locationsFilterChanged(ev) {
this._filters = { ...this._filters, [LOCATIONS_FILTER]: ev.detail.value };
if (!this._fromUrl) {
this._storageFilters = this._filters;
}
}
private _clearFilter() {
this._filters = {};
if (!this._fromUrl) {
this._storageFilters = {};
}
}
private _setFiltersFromUrl() {
@@ -586,6 +606,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) {
return;
}
this._fromUrl = true;
this._filters = {
[TYPE_FILTER]: type === "all" ? [] : [type],
};
@@ -7,7 +7,7 @@ import type { DataTableColumnData } from "../../../components/data-table/ha-data
import { slugify } from "../../../common/string/slugify";
import { relativeTime } from "../../../common/datetime/relative_time";
import { formatShortDateTimeWithConditionalYear } from "../../../common/datetime/format_date_time";
import { isUnavailableState } from "../../../data/entity/entity";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
import "../../../components/ha-tooltip";
import "../../../components/ha-svg-icon";
@@ -146,7 +146,11 @@ export const renderRelativeTimeColumn = (
localize: LocalizeFunc,
hass: HomeAssistant
) => {
if (!valueRelativeTime || isUnavailableState(valueRelativeTime)) {
if (
!valueRelativeTime ||
valueRelativeTime === UNAVAILABLE ||
valueRelativeTime === UNKNOWN
) {
return localize("ui.components.relative_time.never");
}
const date = new Date(valueRelativeTime);
+18 -2
View File
@@ -241,13 +241,16 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
@state() private _searchParms = new URLSearchParams(window.location.search);
@state()
private _filters: DataTableFiltersValues = {};
@storage({
storage: "sessionStorage",
key: "helpers-table-filters",
state: true,
state: false,
subscribe: false,
})
private _filters: DataTableFiltersValues = {};
private _storageFilters: DataTableFiltersValues = {};
@state() private _filteredItems: DataTableFiltersItems = {};
@@ -819,6 +822,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
this._filters = { ...this._filters, [type]: ev.detail.value };
this._filteredItems = { ...this._filteredItems, [type]: ev.detail.items };
if (!this._fromUrl) {
this._storageFilters = this._filters;
}
this._applyFilters();
}
@@ -929,6 +935,8 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
this._filteredHelperEntityIds = items ? [...items] : undefined;
}
private _fromUrl = false;
public connectedCallback() {
super.connectedCallback();
window.addEventListener("location-changed", this._locationChanged);
@@ -965,6 +973,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
return;
}
this._fromUrl = true;
this._filter = history.state?.filter || "";
this._filters = {
@@ -978,6 +987,9 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
private _clearFilter() {
this._filters = {};
this._filteredItems = {};
if (!this._fromUrl) {
this._storageFilters = {};
}
this._applyFilters();
}
@@ -1206,6 +1218,7 @@ ${rejected
super.willUpdate(changedProps);
if (!this.hasUpdated) {
this._filters = this._storageFilters;
this._setFiltersFromUrl();
}
@@ -1254,6 +1267,9 @@ ${rejected
!this._helperEntities.every((val, idx) => newHelpers[idx] === val)
) {
this._helperEntities = newHelpers;
if (Object.keys(this._filters).length > 0) {
this._applyFilters();
}
}
}
+26 -8
View File
@@ -160,15 +160,19 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
private _filter = "";
@state()
private _filters: DataTableFilters = {};
@storage({
storage: "sessionStorage",
key: "scene-table-filters-full",
state: true,
state: false,
subscribe: false,
serializer: serializeFilters,
deserializer: deserializeFilters,
})
private _filters: DataTableFilters = {};
private _storageFilters: DataTableFilters = {};
private _fromUrl = false;
@state() private _expandedFilter?: string;
@@ -403,6 +407,16 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
}
);
protected willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
if (!this._searchParms.has("label")) {
this._filters = this._storageFilters;
}
this._filterLabel();
}
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("_entityReg")) {
@@ -704,12 +718,18 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
items: undefined,
},
};
if (!this._fromUrl) {
this._storageFilters = this._filters;
}
this._applyFilters();
};
private _filterChanged(ev) {
const type = ev.target.localName;
this._filters = { ...this._filters, [type]: ev.detail };
if (!this._fromUrl) {
this._storageFilters = this._filters;
}
this._applyFilters();
}
@@ -759,13 +779,10 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
private _clearFilter() {
this._filters = {};
this._applyFilters();
}
firstUpdated() {
if (this._searchParms.has("label")) {
this._filterLabel();
if (!this._fromUrl) {
this._storageFilters = {};
}
this._applyFilters();
}
private _filterLabel() {
@@ -773,6 +790,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
if (!label) {
return;
}
this._fromUrl = true;
this._filters = {
...this._filters,
"ha-filter-labels": {
@@ -42,6 +42,10 @@ export default class HaScriptFieldEditor extends LitElement {
name: "key",
selector: { text: {} },
},
{
name: "description",
selector: { text: {} },
},
{
name: "required",
selector: { boolean: {} },
@@ -1,7 +1,5 @@
import {
mdiAppleKeyboardCommand,
mdiCommentEditOutline,
mdiCommentTextOutline,
mdiDelete,
mdiDotsVertical,
mdiPlaylistEdit,
@@ -23,7 +21,6 @@ import "../../../components/ha-dropdown-item";
import type { ScriptFieldSidebarConfig } from "../../../data/automation";
import type { Field } from "../../../data/script";
import { SELECTOR_SELECTOR_BUILDING_BLOCKS } from "../../../data/selector/selector_selector";
import { showPromptDialog } from "../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../types";
import { isMac } from "../../../util/is_mac";
import { showEditorToast } from "../automation/editor-toast";
@@ -69,14 +66,6 @@ export default class HaScriptFieldRow extends LitElement {
protected render() {
const hasSelector =
this.field.selector && typeof this.field.selector === "object";
const trimmedComment = this.field.description?.trim() || "";
const commentTooltipText = !trimmedComment
? ""
: trimmedComment.length > 250
? `${trimmedComment.substring(0, 250)}...`
: trimmedComment;
return html`
<ha-card outlined>
<ha-automation-row
@@ -101,16 +90,6 @@ export default class HaScriptFieldRow extends LitElement {
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-dropdown-item value="edit_comment">
<ha-svg-icon
slot="icon"
.path=${mdiCommentEditOutline}
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.automation.editor.comment.${this.field.description ? "edit" : "add"}`
)}
</ha-dropdown-item>
<ha-dropdown-item value="toggle_yaml_mode">
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
<div class="overflow-label">
@@ -153,24 +132,7 @@ export default class HaScriptFieldRow extends LitElement {
</ha-dropdown-item>
</ha-dropdown>
<h3 slot="header">
${this.field.name ?? this.key}
${this.field.description?.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>
<h3 slot="header">${this.field.name ?? this.key}</h3>
<slot name="icons" slot="icons"></slot>
</ha-automation-row>
@@ -362,46 +324,11 @@ export default class HaScriptFieldRow extends LitElement {
});
}
private _editComment = async (): Promise<void> => {
const comment = await showPromptDialog(this, {
title: this.hass.localize(
`ui.panel.config.automation.editor.comment.${this.field.description ? "edit" : "add"}`
),
inputLabel: this.hass.localize(
"ui.panel.config.automation.editor.comment.label"
),
inputType: "string",
defaultValue: this.field.description,
confirmText: this.hass.localize("ui.common.submit"),
multiline: true,
});
if (comment !== null) {
const value = { ...this.field };
if (comment === "") {
delete value.description;
} else {
value.description = comment;
}
fireEvent(this, "value-changed", {
value,
});
if (this._selected) {
this.openSidebar(false, value); // refresh sidebar
}
}
};
public openSidebar(
selectorEditor = false,
fieldValue?: HaScriptFieldRow["field"]
): void {
public openSidebar(selectorEditor = false): void {
if (!selectorEditor) {
this._selected = true;
}
const field = fieldValue ?? this.field;
fireEvent(this, "open-sidebar", {
save: (value) => {
fireEvent(this, "value-changed", { value });
@@ -426,13 +353,12 @@ export default class HaScriptFieldRow extends LitElement {
},
delete: this._onDelete,
config: {
field,
field: this.field,
selector: selectorEditor,
key: this.key,
excludeKeys: this.excludeKeys,
},
yamlMode: this._yamlMode,
editComment: this._editComment,
} satisfies ScriptFieldSidebarConfig);
if (this.narrow) {
@@ -493,9 +419,6 @@ export default class HaScriptFieldRow extends LitElement {
case "delete":
this._onDelete();
break;
case "edit_comment":
this._editComment();
break;
}
}
+34 -11
View File
@@ -164,15 +164,19 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
private _filter = "";
@state()
private _filters: DataTableFilters = {};
@storage({
storage: "sessionStorage",
key: "script-table-filters-full",
state: true,
state: false,
subscribe: false,
serializer: serializeFilters,
deserializer: deserializeFilters,
})
private _filters: DataTableFilters = {};
private _storageFilters: DataTableFilters = {};
private _fromUrl = false;
@state() private _expandedFilter?: string;
@@ -707,17 +711,26 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
items: undefined,
},
};
if (!this._fromUrl) {
this._storageFilters = this._filters;
}
this._applyFilters();
};
private _filterChanged(ev) {
const type = ev.target.localName;
this._filters = { ...this._filters, [type]: ev.detail };
if (!this._fromUrl) {
this._storageFilters = this._filters;
}
this._applyFilters();
}
private _clearFilter() {
this._filters = {};
if (!this._fromUrl) {
this._storageFilters = {};
}
this._applyFilters();
}
@@ -766,6 +779,23 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
this._filteredEntityIds = filteredEntityIds;
}
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")) {
@@ -773,20 +803,12 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
}
}
firstUpdated() {
if (this._searchParms.has("blueprint")) {
this._filterBlueprint();
}
if (this._searchParms.has("label")) {
this._filterLabel();
}
}
private _filterLabel() {
const label = this._searchParms.get("label");
if (!label) {
return;
}
this._fromUrl = true;
this._filters = {
...this._filters,
"ha-filter-labels": {
@@ -802,6 +824,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
if (!blueprint) {
return;
}
this._fromUrl = true;
const related = await findRelated(this.hass, "script_blueprint", blueprint);
this._filters = {
...this._filters,
@@ -5,6 +5,7 @@ import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { computeDomain } from "../../../common/entity/compute_domain";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
@@ -101,9 +102,11 @@ class HuiAlarmModeCardFeature
return supportedModes.find((mode) => mode === stateObj.state);
});
private async _valueChanged(ev: CustomEvent) {
private async _valueChanged(
ev: HASSDomEvent<HASSDomEvents["value-changed"]>
) {
if (!this._stateObj) return;
const mode = (ev.detail as any).value as AlarmMode;
const mode = ev.detail.value as AlarmMode;
if (mode === this._stateObj.state) return;
@@ -2,6 +2,7 @@ import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { computeCssColor } from "../../../common/color/compute-color";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeAttributeNameDisplay } from "../../../common/entity/compute_attribute_display";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateActive } from "../../../common/entity/state_active";
@@ -115,9 +116,9 @@ class HuiCoverPositionCardFeature
`;
}
private _valueChanged(ev: CustomEvent) {
const value = (ev.detail as any).value;
if (isNaN(value)) return;
private _valueChanged(ev: HASSDomEvent<HASSDomEvents["value-changed"]>) {
const { value } = ev.detail;
if (typeof value !== "number" || isNaN(value)) return;
this.hass!.callService("cover", "set_cover_position", {
entity_id: this._stateObj!.entity_id,
@@ -2,6 +2,7 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { computeCssColor } from "../../../common/color/compute-color";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeAttributeNameDisplay } from "../../../common/entity/compute_attribute_display";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateColorCss } from "../../../common/entity/state_color";
@@ -118,9 +119,9 @@ class HuiCoverTiltPositionCardFeature
`;
}
private _valueChanged(ev: CustomEvent) {
const value = (ev.detail as any).value;
if (isNaN(value)) return;
private _valueChanged(ev: HASSDomEvent<HASSDomEvents["value-changed"]>) {
const { value } = ev.detail;
if (typeof value !== "number" || isNaN(value)) return;
this.hass!.callService("cover", "set_cover_tilt_position", {
entity_id: this._stateObj!.entity_id,
@@ -4,6 +4,7 @@ import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { computeDomain } from "../../../common/entity/compute_domain";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { stateColorCss } from "../../../common/entity/state_color";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-control-select";
@@ -79,8 +80,10 @@ class HuiFanOscillateCardFeature
}
}
private async _valueChanged(ev: CustomEvent) {
const shouldOscillate = (ev.detail as any).value === "yes";
private async _valueChanged(
ev: HASSDomEvent<HASSDomEvents["value-changed"]>
) {
const shouldOscillate = ev.detail.value === "yes";
if (shouldOscillate === this._stateObj!.attributes.oscillating) return;
@@ -1,6 +1,7 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeAttributeNameDisplay } from "../../../common/entity/compute_attribute_display";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateActive } from "../../../common/entity/state_active";
import { supportsFeature } from "../../../common/entity/supports-feature";
@@ -144,8 +145,8 @@ class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature {
`;
}
private _speedValueChanged(ev: CustomEvent) {
const speed = (ev.detail as any).value as FanSpeed;
private _speedValueChanged(ev: HASSDomEvent<HASSDomEvents["value-changed"]>) {
const speed = ev.detail.value as FanSpeed;
const percentage = fanSpeedToPercentage(this._stateObj!, speed);
@@ -155,9 +156,9 @@ class HuiFanSpeedCardFeature extends LitElement implements LovelaceCardFeature {
});
}
private _valueChanged(ev: CustomEvent) {
const value = (ev.detail as any).value;
if (isNaN(value)) return;
private _valueChanged(ev: HASSDomEvent<HASSDomEvents["value-changed"]>) {
const { value } = ev.detail;
if (typeof value !== "number" || isNaN(value)) return;
this.hass!.callService("fan", "set_percentage", {
entity_id: this._stateObj!.entity_id,
@@ -4,6 +4,7 @@ import { LitElement, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { computeDomain } from "../../../common/entity/compute_domain";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-control-select";
import type { ControlSelectOption } from "../../../components/ha-control-select";
@@ -81,8 +82,10 @@ class HuiHumidifierToggleCardFeature
}
}
private async _valueChanged(ev: CustomEvent) {
const newState = (ev.detail as any).value as HumidifierState;
private async _valueChanged(
ev: HASSDomEvent<HASSDomEvents["value-changed"]>
) {
const newState = ev.detail.value as HumidifierState;
if (newState === this._stateObj!.state) return;
@@ -26,7 +26,7 @@ import { stateActive } from "../../../common/entity/state_active";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-control-button";
import "../../../components/ha-control-button-group";
import { isUnavailableState } from "../../../data/entity/entity";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type {
ControlButton,
MediaPlayerEntity,
@@ -233,7 +233,7 @@ class HuiMediaPlayerPlaybackCardFeature
case "turn_on":
if (
(!active || assumedState) &&
!isUnavailableState(stateObj.state) &&
stateObj.state !== UNAVAILABLE &&
supportsFeature(stateObj, MediaPlayerEntityFeature.TURN_ON)
) {
buttons.push({
@@ -4,7 +4,7 @@ import { computeDomain } from "../../../common/entity/compute_domain";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { clamp } from "../../../common/number/clamp";
import "../../../components/ha-control-number-buttons";
import { isUnavailableState } from "../../../data/entity/entity";
import { UNAVAILABLE } from "../../../data/entity/entity";
import {
MediaPlayerEntityFeature,
type MediaPlayerEntity,
@@ -89,7 +89,7 @@ class HuiMediaPlayerVolumeButtonsCardFeature
}
const stateObj = this._stateObj;
const disabled = isUnavailableState(stateObj.state);
const disabled = stateObj.state === UNAVAILABLE;
const position =
stateObj.attributes.volume_level != null
@@ -4,7 +4,7 @@ import { computeDomain } from "../../../common/entity/compute_domain";
import { stateActive } from "../../../common/entity/state_active";
import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-control-slider";
import { isUnavailableState } from "../../../data/entity/entity";
import { UNAVAILABLE } from "../../../data/entity/entity";
import {
MediaPlayerEntityFeature,
type MediaPlayerEntity,
@@ -88,7 +88,7 @@ class HuiMediaPlayerVolumeSliderCardFeature
}
const stateObj = this._stateObj;
const disabled = isUnavailableState(stateObj.state);
const disabled = stateObj.state === UNAVAILABLE;
const position =
stateObj.attributes.volume_level != null
@@ -2,6 +2,7 @@ import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../common/entity/compute_domain";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-control-slider";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type { HumidifierEntity } from "../../../data/humidifier";
@@ -84,9 +85,9 @@ class HuiTargetHumidityCardFeature
return this._stateObj!.attributes.max_humidity ?? 100;
}
private _valueChanged(ev: CustomEvent) {
const value = (ev.detail as any).value;
if (isNaN(value)) return;
private _valueChanged(ev: HASSDomEvent<HASSDomEvents["value-changed"]>) {
const { value } = ev.detail;
if (typeof value !== "number" || isNaN(value)) return;
this._targetHumidity = value;
this._callService();
}
@@ -3,6 +3,7 @@ import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { UNIT_F } from "../../../common/const";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { stateColorCss } from "../../../common/entity/state_color";
@@ -121,10 +122,13 @@ class HuiTargetTemperatureCardFeature
return this._stateObj!.attributes.max_temp;
}
private async _valueChanged(ev: CustomEvent) {
const value = (ev.detail as any).value;
if (isNaN(value)) return;
const target = (ev.currentTarget as any).target ?? "value";
private async _valueChanged(
ev: HASSDomEvent<HASSDomEvents["value-changed"]>
) {
const { value } = ev.detail;
if (typeof value !== "number" || isNaN(value)) return;
const target =
(ev.currentTarget as HTMLElement & { target?: Target }).target ?? "value";
this._targetTemperature = {
...this._targetTemperature,
@@ -2,6 +2,7 @@ import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { computeCssColor } from "../../../common/color/compute-color";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeAttributeNameDisplay } from "../../../common/entity/compute_attribute_display";
import { computeDomain } from "../../../common/entity/compute_domain";
import { stateActive } from "../../../common/entity/state_active";
@@ -119,9 +120,9 @@ class HuiValvePositionCardFeature
`;
}
private _valueChanged(ev: CustomEvent) {
const value = (ev.detail as any).value;
if (isNaN(value)) return;
private _valueChanged(ev: HASSDomEvent<HASSDomEvents["value-changed"]>) {
const { value } = ev.detail;
if (typeof value !== "number" || isNaN(value)) return;
this.hass!.callService("valve", "set_valve_position", {
entity_id: this._stateObj!.entity_id,
+9 -4
View File
@@ -32,7 +32,7 @@ import "../../../components/tile/ha-tile-badge";
import "../../../components/tile/ha-tile-container";
import "../../../components/tile/ha-tile-icon";
import "../../../components/tile/ha-tile-info";
import { isUnavailableState } from "../../../data/entity/entity";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import type { HomeAssistant } from "../../../types";
import "../card-features/hui-card-features";
@@ -419,7 +419,9 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
const stateObj = this.hass.states[area.temperature_entity_id] as
| HassEntity
| undefined;
return !stateObj || isUnavailableState(stateObj.state)
return !stateObj ||
stateObj.state === UNAVAILABLE ||
stateObj.state === UNKNOWN
? ""
: this.hass.formatEntityState(stateObj);
}
@@ -427,7 +429,9 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
const stateObj = this.hass.states[area.humidity_entity_id] as
| HassEntity
| undefined;
return !stateObj || isUnavailableState(stateObj.state)
return !stateObj ||
stateObj.state === UNAVAILABLE ||
stateObj.state === UNKNOWN
? ""
: this.hass.formatEntityState(stateObj);
}
@@ -444,7 +448,8 @@ export class HuiAreaCard extends LitElement implements LovelaceCard {
const stateObj = this.hass.states[entityId];
if (
stateObj &&
!isUnavailableState(stateObj.state) &&
stateObj.state !== UNAVAILABLE &&
stateObj.state !== UNKNOWN &&
isNumericState(stateObj) &&
!isNaN(Number(stateObj.state))
) {
+11 -4
View File
@@ -4,12 +4,13 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import type { HASSDomCurrentTargetEvent } from "../../../common/dom/fire_event";
import { computeDomain } from "../../../common/entity/compute_domain";
import "../../../components/entity/state-badge";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import "../../../components/ha-relative-time";
import { isUnavailableState } from "../../../data/entity/entity";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import type {
CallServiceActionConfig,
@@ -293,7 +294,8 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
SENSOR_TIMESTAMP_DEVICE_CLASSES.includes(
stateObj.attributes.device_class
) &&
!isUnavailableState(stateObj.state)
stateObj.state !== UNAVAILABLE &&
stateObj.state !== UNKNOWN
? html`
<hui-timestamp-display
.hass=${this.hass}
@@ -322,8 +324,13 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard {
`;
}
private _handleAction(ev: ActionHandlerEvent) {
const config = (ev.currentTarget as any).config as GlanceConfigEntity;
private _handleAction(
ev: HASSDomCurrentTargetEvent<
HTMLElement & { config: GlanceConfigEntity }
> &
ActionHandlerEvent
) {
const { config } = ev.currentTarget;
handleAction(this, this.hass!, config, ev.detail.action!);
}
}
+4 -4
View File
@@ -11,7 +11,7 @@ import { stateColorBrightness } from "../../../common/entity/state_color";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
import "../../../components/ha-state-icon";
import { UNAVAILABLE, isUnavailableState } from "../../../data/entity/entity";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
import type { LightEntity } from "../../../data/light";
import { lightSupportsBrightness } from "../../../data/light";
import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
@@ -113,7 +113,7 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
min="1"
max="100"
.value=${brightness}
.disabled=${isUnavailableState(stateObj.state)}
.disabled=${stateObj.state === UNAVAILABLE}
@value-changing=${this._dragEvent}
@value-changed=${this._setBrightness}
style=${styleMap({
@@ -128,7 +128,7 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
"state-on": stateObj.state === "on",
"state-unavailable": stateObj.state === UNAVAILABLE,
})}"
.disabled=${isUnavailableState(stateObj.state)}
.disabled=${stateObj.state === UNAVAILABLE}
style=${styleMap({
filter: this._computeBrightness(stateObj),
color: this._computeColor(stateObj),
@@ -149,7 +149,7 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
</div>
<div id="info" .title=${name}>
${isUnavailableState(stateObj.state)
${stateObj.state === UNAVAILABLE || stateObj.state === UNKNOWN
? html` <div>${this.hass.formatEntityState(stateObj)}</div> `
: html` <div class="brightness">%</div> `}
${name}
@@ -21,7 +21,7 @@ import type { HaSlider } from "../../../components/ha-slider";
import "../../../components/ha-state-icon";
import { showJoinMediaPlayersDialog } from "../../../components/media-player/show-join-media-players-dialog";
import { showMediaBrowserDialog } from "../../../components/media-player/show-media-browser-dialog";
import { isUnavailableState } from "../../../data/entity/entity";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
import type {
MediaPickedEvent,
MediaPlayerEntity,
@@ -173,9 +173,12 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
const entityState = stateObj.state;
const isOffState =
!stateActive(stateObj) && !isUnavailableState(entityState);
!stateActive(stateObj) &&
entityState !== UNAVAILABLE &&
entityState !== UNKNOWN;
const isUnavailable =
isUnavailableState(entityState) ||
entityState === UNAVAILABLE ||
entityState === UNKNOWN ||
(isOffState &&
!supportsFeature(stateObj, MediaPlayerEntityFeature.TURN_ON));
const hasNoImage = !this._image;
@@ -45,7 +45,7 @@ import "../../../components/ha-sortable";
import "../../../components/ha-svg-icon";
import "../../../components/input/ha-input";
import type { HaInput } from "../../../components/input/ha-input";
import { isUnavailableState } from "../../../data/entity/entity";
import { UNAVAILABLE, UNKNOWN } from "../../../data/entity/entity";
import type { TodoItem } from "../../../data/todo";
import {
TodoItemStatus,
@@ -383,7 +383,8 @@ export class HuiTodoListCard extends LitElement implements LovelaceCard {
`;
}
const unavailable = isUnavailableState(stateObj.state);
const unavailable =
stateObj.state === UNAVAILABLE || stateObj.state === UNKNOWN;
// Discard memoization when we rollover to a new day, so filters can be recalculated
const memoTime = this._config.due_date_period
@@ -279,14 +279,15 @@ export class HuiActionEditor extends LitElement {
});
}
private _valueChanged(ev: InputEvent): void {
private _valueChanged(
ev: InputEvent & { target: HaInput & { configValue?: string } }
): void {
ev.stopPropagation();
if (!this.hass) {
return;
}
const target = ev.target! as HaInput;
const configValue: string | undefined = (target as any).configValue;
const value = target.value ?? "";
const { configValue } = ev.target;
const value = ev.target.value ?? "";
if (this[`_${configValue}`] === value) {
return;
}
@@ -8,6 +8,7 @@ import type { HomeAssistant } from "../../../types";
import type { EntitiesCardEntityConfig } from "../cards/types";
import { computeTooltip } from "../common/compute-tooltip";
import { actionHandler } from "../common/directives/action-handler-directive";
import type { HASSDomCurrentTargetEvent } from "../../../common/dom/fire_event";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import "../../../components/chips/ha-assist-chip";
@@ -65,9 +66,13 @@ export class HuiButtonsBase extends LitElement {
`;
}
private _handleAction(ev: ActionHandlerEvent) {
const config = (ev.currentTarget as any).config as EntitiesCardEntityConfig;
handleAction(this, this.hass, config, ev.detail.action!);
private _handleAction(
ev: HASSDomCurrentTargetEvent<
HTMLElement & { config: EntitiesCardEntityConfig }
> &
ActionHandlerEvent
) {
handleAction(this, this.hass, ev.currentTarget.config, ev.detail.action);
}
static get styles(): CSSResultGroup {
@@ -13,7 +13,7 @@ import { stringCompare } from "../../../../common/string/compare";
import "../../../../components/ha-spinner";
import "../../../../components/input/ha-input-search";
import type { HaInputSearch } from "../../../../components/input/ha-input-search";
import { isUnavailableState } from "../../../../data/entity/entity";
import { UNAVAILABLE, UNKNOWN } from "../../../../data/entity/entity";
import type { LovelaceBadgeConfig } from "../../../../data/lovelace/config/badge";
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
import type { CustomBadgeEntry } from "../../../../data/lovelace_custom_cards";
@@ -235,12 +235,14 @@ export class HuiBadgePicker extends LitElement {
this._usedEntities = [...usedEntities].filter(
(eid) =>
this.hass!.states[eid] &&
!isUnavailableState(this.hass!.states[eid].state)
this.hass!.states[eid].state !== UNAVAILABLE &&
this.hass!.states[eid].state !== UNKNOWN
);
this._unusedEntities = [...unusedEntities].filter(
(eid) =>
this.hass!.states[eid] &&
!isUnavailableState(this.hass!.states[eid].state)
this.hass!.states[eid].state !== UNAVAILABLE &&
this.hass!.states[eid].state !== UNKNOWN
);
this._loadBages();
@@ -13,7 +13,7 @@ import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-spinner";
import "../../../../components/input/ha-input-search";
import type { HaInputSearch } from "../../../../components/input/ha-input-search";
import { isUnavailableState } from "../../../../data/entity/entity";
import { UNAVAILABLE, UNKNOWN } from "../../../../data/entity/entity";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type { LovelaceConfig } from "../../../../data/lovelace/config/types";
import type { CustomCardEntry } from "../../../../data/lovelace_custom_cards";
@@ -270,12 +270,14 @@ export class HuiCardPicker extends LitElement {
this._usedEntities = [...usedEntities].filter(
(eid) =>
this.hass!.states[eid] &&
!isUnavailableState(this.hass!.states[eid].state)
this.hass!.states[eid].state !== UNAVAILABLE &&
this.hass!.states[eid].state !== UNKNOWN
);
this._unusedEntities = [...unusedEntities].filter(
(eid) =>
this.hass!.states[eid] &&
!isUnavailableState(this.hass!.states[eid].state)
this.hass!.states[eid].state !== UNAVAILABLE &&
this.hass!.states[eid].state !== UNKNOWN
);
this._loadCards();
@@ -73,11 +73,11 @@ function computeBreakpointsKey(breakpoints) {
}
// Compute all possible media queries from each breakpoints combination (2 ^ breakpoints = 16)
const queries = getAllCombinations(BREAKPOINTS as unknown as Breakpoint[])
const queries = getAllCombinations<Breakpoint>(BREAKPOINTS)
.filter((arr) => arr.length !== 0)
.map(
(breakpoints) =>
[breakpoints, computeBreakpointsSize(breakpoints)] as [
[breakpoints, computeBreakpointsSize(breakpoints)] satisfies [
Breakpoint[],
string,
]
@@ -69,7 +69,7 @@ export class HuiAreaControlsCardFeatureEditor
return [];
}
const controlEntities = getAreaControlEntities(
AREA_CONTROL_DOMAINS as unknown as AreaControlDomain[],
AREA_CONTROL_DOMAINS,
areaId,
excludeEntities,
this.hass!

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