mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-19 12:19:32 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5aec73ddd3 | ||
|
|
5041357643 |
No files matched your search
@@ -1,9 +1,5 @@
|
||||
import { mdiHelpCircleOutline } from "@mdi/js";
|
||||
import type {
|
||||
HassService,
|
||||
HassServices,
|
||||
HassServiceTarget,
|
||||
} from "home-assistant-js-websocket";
|
||||
import type { HassService, HassServices } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
@@ -30,9 +26,11 @@ import {
|
||||
expandDeviceTarget,
|
||||
expandFloorTarget,
|
||||
expandLabelTarget,
|
||||
resolveEntityIDs,
|
||||
type Selector,
|
||||
type TargetSelector,
|
||||
} from "../data/selector";
|
||||
import type { HassServiceTargetWithExclude } from "../data/target";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
|
||||
import { documentationUrl } from "../util/documentation-url";
|
||||
import { getSelectorFallbackValue } from "./ha-form/get-selector-fallback-value";
|
||||
@@ -105,7 +103,7 @@ export class HaServiceControl extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public value?: {
|
||||
action: string;
|
||||
target?: HassServiceTarget;
|
||||
target?: HassServiceTargetWithExclude;
|
||||
data?: Record<string, any>;
|
||||
};
|
||||
|
||||
@@ -343,7 +341,7 @@ export class HaServiceControl extends LitElement {
|
||||
);
|
||||
targetDevices.push(...expanded.devices);
|
||||
const primaryEntities = expanded.entities.filter(isPrimaryEntity);
|
||||
targetEntities.push(primaryEntities);
|
||||
targetEntities.push(...primaryEntities);
|
||||
targetAreas.push(...expanded.areas);
|
||||
});
|
||||
}
|
||||
@@ -384,6 +382,19 @@ export class HaServiceControl extends LitElement {
|
||||
targetEntities.push(...primaryEntities);
|
||||
});
|
||||
}
|
||||
const exclude = value?.target?.exclude;
|
||||
if (exclude) {
|
||||
const excluded = new Set(
|
||||
resolveEntityIDs(
|
||||
this.hass,
|
||||
exclude,
|
||||
this.hass.entities,
|
||||
this.hass.devices,
|
||||
this.hass.areas
|
||||
)
|
||||
);
|
||||
return targetEntities.filter((entityId) => !excluded.has(entityId));
|
||||
}
|
||||
return targetEntities;
|
||||
});
|
||||
|
||||
@@ -438,6 +449,9 @@ export class HaServiceControl extends LitElement {
|
||||
} else {
|
||||
this._stickySelector.target = { object: null };
|
||||
}
|
||||
} else if (typeof value === "object" && "exclude" in value) {
|
||||
// The target picker can't show an exclude yet, so edit it as YAML.
|
||||
this._stickySelector.target = { object: null };
|
||||
}
|
||||
return (
|
||||
this._stickySelector.target ??
|
||||
@@ -843,7 +857,7 @@ export class HaServiceControl extends LitElement {
|
||||
}
|
||||
|
||||
const newService = ev.detail.value || "";
|
||||
let target: HassServiceTarget | undefined;
|
||||
let target: HassServiceTargetWithExclude | undefined;
|
||||
|
||||
if (newService) {
|
||||
const serviceData = this._getServiceInfo(newService, this.hass.services);
|
||||
@@ -894,11 +908,42 @@ export class HaServiceControl extends LitElement {
|
||||
)
|
||||
);
|
||||
}
|
||||
const targetFloors = ensureArray(
|
||||
currentTarget.floor_id || this._value!.data?.floor_id
|
||||
)?.filter(
|
||||
(floor) =>
|
||||
expandFloorTarget(this.hass, floor, this.hass.areas, targetSelector)
|
||||
.areas.length > 0
|
||||
);
|
||||
const targetLabels = ensureArray(
|
||||
currentTarget.label_id || this._value!.data?.label_id
|
||||
)?.filter((label) => {
|
||||
const expanded = expandLabelTarget(
|
||||
this.hass,
|
||||
label,
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
targetSelector
|
||||
);
|
||||
return (
|
||||
expanded.areas.length > 0 ||
|
||||
expanded.devices.length > 0 ||
|
||||
expanded.entities.length > 0
|
||||
);
|
||||
});
|
||||
target = {
|
||||
...(targetEntities.length ? { entity_id: targetEntities } : {}),
|
||||
...(targetDevices.length ? { device_id: targetDevices } : {}),
|
||||
...(targetAreas.length ? { area_id: targetAreas } : {}),
|
||||
...(targetFloors?.length ? { floor_id: targetFloors } : {}),
|
||||
...(targetLabels?.length ? { label_id: targetLabels } : {}),
|
||||
};
|
||||
// An exclude on its own is not a target, so it only stays while
|
||||
// something is still included.
|
||||
if (currentTarget.exclude && Object.keys(target).length) {
|
||||
target.exclude = currentTarget.exclude;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -935,7 +980,7 @@ export class HaServiceControl extends LitElement {
|
||||
|
||||
private _targetChanged(
|
||||
ev: HASSDomEvent<{
|
||||
value: HassServiceTarget | undefined;
|
||||
value: HassServiceTargetWithExclude | undefined;
|
||||
isValid?: boolean;
|
||||
}>
|
||||
) {
|
||||
|
||||
@@ -602,7 +602,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
const val = { ...value }!;
|
||||
delete val[typeId];
|
||||
if (Object.keys(val).length) {
|
||||
// An exclude on its own is not a target.
|
||||
if (Object.keys(val).some((key) => key !== "exclude")) {
|
||||
return val;
|
||||
}
|
||||
return undefined;
|
||||
|
||||
@@ -2,7 +2,6 @@ import type {
|
||||
Connection,
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
HassServiceTarget,
|
||||
} from "home-assistant-js-websocket";
|
||||
import { ensureArray } from "../common/array/ensure-array";
|
||||
import type { WeekdayShort } from "../common/datetime/weekday";
|
||||
@@ -18,6 +17,7 @@ import type {
|
||||
DeviceTrigger,
|
||||
} from "./device/device_automation";
|
||||
import type { Action, Field, MODES, Option } from "./script";
|
||||
import type { HassServiceTargetWithExclude } from "./target";
|
||||
import { migrateAutomationAction } from "./script";
|
||||
import type { TriggerDescription } from "./trigger";
|
||||
|
||||
@@ -106,7 +106,7 @@ export interface BaseTrigger {
|
||||
|
||||
export interface PlatformTrigger extends BaseTrigger {
|
||||
trigger: Exclude<string, LegacyTrigger["trigger"]>;
|
||||
target?: HassServiceTarget;
|
||||
target?: HassServiceTargetWithExclude;
|
||||
}
|
||||
|
||||
export interface StateTrigger extends BaseTrigger {
|
||||
@@ -251,7 +251,7 @@ interface BaseCondition {
|
||||
|
||||
export interface PlatformCondition extends BaseCondition {
|
||||
condition: Exclude<string, LegacyCondition["condition"]>;
|
||||
target?: HassServiceTarget;
|
||||
target?: HassServiceTargetWithExclude;
|
||||
}
|
||||
|
||||
export interface LogicalCondition extends BaseCondition {
|
||||
|
||||
+13
-4
@@ -2,7 +2,6 @@ import type {
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
HassServices,
|
||||
HassServiceTarget,
|
||||
} from "home-assistant-js-websocket";
|
||||
import type { Describe } from "superstruct";
|
||||
import {
|
||||
@@ -31,6 +30,7 @@ import type {
|
||||
} from "./automation";
|
||||
import { migrateAutomationTrigger } from "./automation";
|
||||
import type { BlueprintInput } from "./blueprint";
|
||||
import type { HassServiceTargetWithExclude } from "./target";
|
||||
|
||||
export const MODES = ["single", "restart", "queued", "parallel"] as const;
|
||||
export const MODES_MAX = ["queued", "parallel"] as const;
|
||||
@@ -43,12 +43,21 @@ export const baseActionStruct = object({
|
||||
enabled: optional(boolean()),
|
||||
});
|
||||
|
||||
export const targetStruct = object({
|
||||
const targetIncludeFields = {
|
||||
entity_id: optional(union([string(), array(string())])),
|
||||
device_id: optional(union([string(), array(string())])),
|
||||
area_id: optional(union([string(), array(string())])),
|
||||
floor_id: optional(union([string(), array(string())])),
|
||||
label_id: optional(union([string(), array(string())])),
|
||||
};
|
||||
|
||||
// A target without an exclude.
|
||||
export const targetIncludeStruct = object(targetIncludeFields);
|
||||
|
||||
export const targetStruct = object({
|
||||
...targetIncludeFields,
|
||||
// Same shape as the target, without nesting another exclude.
|
||||
exclude: optional(targetIncludeStruct),
|
||||
});
|
||||
|
||||
export const serviceActionStruct: Describe<ServiceActionWithTemplate> = assign(
|
||||
@@ -126,14 +135,14 @@ export interface ServiceAction extends BaseAction {
|
||||
action?: string;
|
||||
service_template?: string;
|
||||
entity_id?: string;
|
||||
target?: HassServiceTarget;
|
||||
target?: HassServiceTargetWithExclude;
|
||||
data?: Record<string, unknown>;
|
||||
response_variable?: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
type ServiceActionWithTemplate = ServiceAction & {
|
||||
target?: HassServiceTarget | string;
|
||||
target?: HassServiceTargetWithExclude | string;
|
||||
};
|
||||
|
||||
export type { ServiceActionWithTemplate };
|
||||
|
||||
+39
-1
@@ -23,6 +23,7 @@ import type {
|
||||
EntityRegistryEntry,
|
||||
} from "./entity/entity_registry";
|
||||
import type { EntitySources } from "./entity/entity_sources";
|
||||
import type { HassServiceTargetWithExclude } from "./target";
|
||||
|
||||
export type ThresholdMode = "crossed" | "changed" | "is";
|
||||
|
||||
@@ -1170,7 +1171,7 @@ export const computeCreateDomains = (
|
||||
|
||||
export const resolveEntityIDs = (
|
||||
hass: HomeAssistant,
|
||||
targetPickerValue: HassServiceTarget,
|
||||
targetPickerValue: HassServiceTargetWithExclude,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"],
|
||||
@@ -1180,6 +1181,43 @@ export const resolveEntityIDs = (
|
||||
return [];
|
||||
}
|
||||
|
||||
const included = resolveIncludedEntityIDs(
|
||||
hass,
|
||||
targetPickerValue,
|
||||
entities,
|
||||
devices,
|
||||
areas,
|
||||
targetSelector
|
||||
);
|
||||
|
||||
if (!targetPickerValue.exclude) {
|
||||
return included;
|
||||
}
|
||||
|
||||
// Excludes are resolved like a target and taken out of the whole result,
|
||||
// matching core. They are not narrowed by the selector: leaving out more
|
||||
// than the selector allows can't add anything back.
|
||||
const excluded = new Set(
|
||||
resolveIncludedEntityIDs(
|
||||
hass,
|
||||
targetPickerValue.exclude,
|
||||
entities,
|
||||
devices,
|
||||
areas,
|
||||
{ target: {} }
|
||||
)
|
||||
);
|
||||
return included.filter((entityId) => !excluded.has(entityId));
|
||||
};
|
||||
|
||||
const resolveIncludedEntityIDs = (
|
||||
hass: HomeAssistant,
|
||||
targetPickerValue: HassServiceTarget,
|
||||
entities: HomeAssistant["entities"],
|
||||
devices: HomeAssistant["devices"],
|
||||
areas: HomeAssistant["areas"],
|
||||
targetSelector: TargetSelector
|
||||
): string[] => {
|
||||
const targetEntities = new Set(ensureArray(targetPickerValue.entity_id));
|
||||
const targetDevices = new Set(ensureArray(targetPickerValue.device_id));
|
||||
const targetAreas = new Set(ensureArray(targetPickerValue.area_id));
|
||||
|
||||
@@ -26,6 +26,14 @@ export interface TargetItem {
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* A target that can leave things out again. `exclude` has the shape of a
|
||||
* target and is subtracted, as a whole, from everything the target resolves to.
|
||||
*/
|
||||
export type HassServiceTargetWithExclude = HassServiceTarget & {
|
||||
exclude?: HassServiceTarget;
|
||||
};
|
||||
|
||||
export interface SingleHassServiceTarget {
|
||||
entity_id?: string;
|
||||
device_id?: string;
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
mdiShape,
|
||||
mdiSwapHorizontal,
|
||||
} from "@mdi/js";
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import {
|
||||
css,
|
||||
html,
|
||||
@@ -25,7 +24,10 @@ import { consume } from "../../../../common/decorators/consume";
|
||||
import { ensureArray } from "../../../../common/array/ensure-array";
|
||||
import { transform } from "../../../../common/decorators/transform";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import { isTemplate } from "../../../../common/string/has-template";
|
||||
import {
|
||||
hasTemplate,
|
||||
isTemplate,
|
||||
} from "../../../../common/string/has-template";
|
||||
import "../../../../components/ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
@@ -51,7 +53,11 @@ import {
|
||||
entityMeetsTargetSelector,
|
||||
type TargetSelector,
|
||||
} from "../../../../data/selector";
|
||||
import { extractFromTarget, type TargetType } from "../../../../data/target";
|
||||
import {
|
||||
extractFromTarget,
|
||||
type HassServiceTargetWithExclude,
|
||||
type TargetType,
|
||||
} from "../../../../data/target";
|
||||
import { showMoreInfoDialog } from "../../../../dialogs/more-info/show-ha-more-info-dialog";
|
||||
import { isConfigObject } from "../ui-support";
|
||||
import { getTargetIcon } from "./get_target_icon";
|
||||
@@ -60,7 +66,7 @@ import { getTargetText } from "./get_target_text";
|
||||
@customElement("ha-automation-row-targets")
|
||||
export class HaAutomationRowTargets extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public target?: HassServiceTarget;
|
||||
public target?: HassServiceTargetWithExclude;
|
||||
|
||||
@property({ attribute: false })
|
||||
public targetRequired = false;
|
||||
@@ -113,9 +119,12 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
|
||||
private _countCache = new Map<
|
||||
string,
|
||||
Promise<number | undefined> | number | undefined
|
||||
Promise<TargetCount | undefined> | TargetCount | undefined
|
||||
>();
|
||||
|
||||
// What the target's `exclude` resolves to, shared by every count.
|
||||
private _excludedEntities?: Promise<Set<string>>;
|
||||
|
||||
private _rerenderCount = true;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
@@ -126,6 +135,7 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
changedProps.has("_registries")
|
||||
) {
|
||||
this._rerenderCount = true;
|
||||
this._excludedEntities = undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -212,6 +222,47 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
}).length;
|
||||
}
|
||||
|
||||
// A templated exclude can only be resolved when the automation runs.
|
||||
private _getExclude() {
|
||||
const exclude = isConfigObject(this.target)
|
||||
? this.target.exclude
|
||||
: undefined;
|
||||
return isConfigObject(exclude) && !hasTemplate(exclude)
|
||||
? exclude
|
||||
: undefined;
|
||||
}
|
||||
|
||||
private _getExcludedEntities(): Promise<Set<string>> {
|
||||
const exclude = this._getExclude();
|
||||
if (!exclude) {
|
||||
return Promise.resolve(new Set());
|
||||
}
|
||||
if (!this._excludedEntities) {
|
||||
this._excludedEntities = extractFromTarget(
|
||||
this._api.callWS,
|
||||
exclude,
|
||||
false,
|
||||
this.selector?.target?.primary_entities_only
|
||||
)
|
||||
.then((result) => new Set(result.referenced_entities))
|
||||
// Counts then leave the exclude out rather than failing altogether.
|
||||
.catch(() => new Set<string>());
|
||||
}
|
||||
return this._excludedEntities;
|
||||
}
|
||||
|
||||
private _formatCount(count: TargetCount | undefined) {
|
||||
if (!count) {
|
||||
return " (-)";
|
||||
}
|
||||
return count.excluded
|
||||
? ` ${this._i18n.localize(
|
||||
"ui.panel.config.automation.editor.target_summary.count_excluded",
|
||||
{ count: count.count, excluded: count.excluded }
|
||||
)}`
|
||||
: ` (${count.count})`;
|
||||
}
|
||||
|
||||
private _renderCount(
|
||||
targetType: "floor" | "area" | "device" | "label",
|
||||
targetId: string
|
||||
@@ -219,22 +270,36 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
const key = `${targetType}:${targetId}`;
|
||||
let fallback = " (-)";
|
||||
if (!this._countCache.has(key) || this._rerenderCount) {
|
||||
if (typeof this._countCache.get(key) === "number") {
|
||||
fallback = ` (${this._countCache.get(key)})`;
|
||||
const cached = this._countCache.get(key);
|
||||
if (cached && !(cached instanceof Promise)) {
|
||||
fallback = this._formatCount(cached);
|
||||
}
|
||||
this._countCache.set(
|
||||
key,
|
||||
extractFromTarget(
|
||||
this._api.callWS,
|
||||
{
|
||||
[`${targetType}_id`]: [targetId],
|
||||
},
|
||||
false,
|
||||
this.selector?.target?.primary_entities_only
|
||||
)
|
||||
.then((result) =>
|
||||
this._countMatchingEntities(result.referenced_entities)
|
||||
)
|
||||
Promise.all([
|
||||
extractFromTarget(
|
||||
this._api.callWS,
|
||||
{
|
||||
[`${targetType}_id`]: [targetId],
|
||||
},
|
||||
false,
|
||||
this.selector?.target?.primary_entities_only
|
||||
),
|
||||
this._getExcludedEntities(),
|
||||
])
|
||||
.then(([result, excluded]): TargetCount => {
|
||||
// Excludes are global, so anything they resolve to is taken out
|
||||
// of this target too.
|
||||
const remaining = result.referenced_entities.filter(
|
||||
(entityId) => !excluded.has(entityId)
|
||||
);
|
||||
const count = this._countMatchingEntities(remaining);
|
||||
return {
|
||||
count,
|
||||
excluded:
|
||||
this._countMatchingEntities(result.referenced_entities) - count,
|
||||
};
|
||||
})
|
||||
.catch((err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error counting target entities", err);
|
||||
@@ -243,20 +308,21 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
if (this._countCache.get(key) instanceof Promise) {
|
||||
const cached = this._countCache.get(key);
|
||||
if (cached instanceof Promise) {
|
||||
return until(
|
||||
(this._countCache.get(key) as Promise<number | undefined>)!.then(
|
||||
(count) => {
|
||||
cached.then((count) => {
|
||||
if (this._countCache.get(key) === cached) {
|
||||
this._countCache.set(key, count);
|
||||
return count === undefined ? nothing : html` (${count})`;
|
||||
}
|
||||
),
|
||||
return count === undefined ? nothing : this._formatCount(count);
|
||||
}),
|
||||
fallback
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof this._countCache.get(key) === "number") {
|
||||
return ` (${this._countCache.get(key)})`;
|
||||
if (cached) {
|
||||
return this._formatCount(cached);
|
||||
}
|
||||
return nothing;
|
||||
}
|
||||
@@ -271,8 +337,12 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
|
||||
// Any other value that isn't a mapping is invalid. Without this, the
|
||||
// characters of a string or the items of a list would count as targets.
|
||||
const target = isConfigObject(this.target) ? this.target : undefined;
|
||||
const length = Object.keys(target || {}).length;
|
||||
// `exclude` takes entities away from the other keys; it is not a target
|
||||
// of its own, so it gets no chip. Its effect shows in each count.
|
||||
const { exclude: _exclude, ...target } = isConfigObject(this.target)
|
||||
? this.target
|
||||
: {};
|
||||
const length = Object.keys(target).length;
|
||||
if (!length) {
|
||||
return this._renderTargetBadge(
|
||||
this.targetRequired
|
||||
@@ -285,13 +355,13 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
this.targetRequired
|
||||
);
|
||||
}
|
||||
const totalLength = Object.values(target || {}).reduce(
|
||||
const totalLength = Object.values(target).reduce(
|
||||
(acc, val) => acc + ensureArray(val).length,
|
||||
0
|
||||
);
|
||||
|
||||
if (totalLength <= 5) {
|
||||
const targets = Object.entries(target!).reduce<
|
||||
const targets = Object.entries(target).reduce<
|
||||
["floor" | "area" | "device" | "entity" | "label", string][]
|
||||
>((acc, [targetType, targetId]) => {
|
||||
const type = targetType.replace("_id", "") as
|
||||
@@ -313,7 +383,7 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
const rows = Object.entries(target!)
|
||||
const rows = Object.entries(target)
|
||||
.reduce<["floor" | "area" | "device" | "entity" | "label", string][]>(
|
||||
(acc, [targetType, targetId]) => {
|
||||
const type = targetType.replace("_id", "") as
|
||||
@@ -727,6 +797,11 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
interface TargetCount {
|
||||
count: number;
|
||||
excluded: number;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-row-targets": HaAutomationRowTargets;
|
||||
|
||||
@@ -24,7 +24,7 @@ import "../../../../../components/ha-alert";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import type { HaFormSchema } from "../../../../../components/ha-form/types";
|
||||
import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
import { targetStruct } from "../../../../../data/script";
|
||||
import { targetIncludeStruct } from "../../../../../data/script";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import {
|
||||
LOCATION_MATCH_MODES,
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
const locationConditionStruct = object({
|
||||
condition: literal("location"),
|
||||
locations: optional(array(string())),
|
||||
target: optional(targetStruct),
|
||||
target: optional(targetIncludeStruct),
|
||||
match_mode: optional(enums(LOCATION_MATCH_MODES)),
|
||||
away: optional(boolean()),
|
||||
});
|
||||
|
||||
@@ -5599,6 +5599,7 @@
|
||||
"template": "Template",
|
||||
"device_replaced": "Replaced device",
|
||||
"device_replaced_description": "This device was replaced by one or more new devices. Update this automation to use them.",
|
||||
"count_excluded": "({count}, {excluded} excluded)",
|
||||
"types": {
|
||||
"entity": "[%key:ui::components::target-picker::type::entities%]",
|
||||
"device": "[%key:ui::components::target-picker::type::devices%]",
|
||||
|
||||
@@ -193,4 +193,52 @@ describe("resolveEntityIDs", () => {
|
||||
"sensor.kitchen_hub_temp",
|
||||
]);
|
||||
});
|
||||
|
||||
// Excludes are global: resolved like a target and taken out of everything
|
||||
// the target includes, so logbook, history and state selectors only show
|
||||
// what the action or trigger actually acts on.
|
||||
describe("exclude", () => {
|
||||
it("takes an excluded entity out of an area", () => {
|
||||
expect(
|
||||
resolve({
|
||||
area_id: "kitchen",
|
||||
exclude: { entity_id: "light.kitchen" },
|
||||
})
|
||||
).toEqual(["sensor.kitchen_hub_temp"]);
|
||||
});
|
||||
|
||||
it("takes an excluded area out of a device", () => {
|
||||
expect(
|
||||
resolve({ device_id: "hub", exclude: { area_id: "hallway" } })
|
||||
).toEqual(["light.kitchen", "sensor.kitchen_hub_temp"]);
|
||||
});
|
||||
|
||||
it("takes an excluded device out of every included target", () => {
|
||||
expect(
|
||||
resolve({
|
||||
area_id: ["kitchen", "hallway"],
|
||||
exclude: { device_id: ["hub"] },
|
||||
})
|
||||
).toEqual([]);
|
||||
});
|
||||
|
||||
it("wins over an explicit include of the same entity", () => {
|
||||
expect(
|
||||
resolve({
|
||||
entity_id: ["light.kitchen", "sensor.hallway_probe"],
|
||||
exclude: { entity_id: "light.kitchen" },
|
||||
})
|
||||
).toEqual(["sensor.hallway_probe"]);
|
||||
});
|
||||
|
||||
it("leaves the target alone when the exclude matches nothing", () => {
|
||||
expect(
|
||||
resolve({ area_id: "kitchen", exclude: { area_id: "deleted_area" } })
|
||||
).toEqual(["light.kitchen", "sensor.kitchen_hub_temp"]);
|
||||
});
|
||||
|
||||
it("resolves nothing for an exclude on its own", () => {
|
||||
expect(resolve({ exclude: { area_id: "kitchen" } })).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user