Compare commits

...
7 changed files with 447 additions and 109 deletions
+177 -76
View File
@@ -117,6 +117,22 @@ const literalTimeToSeconds = (value: unknown): number | undefined => {
return hours * 3600 + minutes * 60 + seconds;
};
const numericThresholdSuffix = (config: {
above?: number | string;
below?: number | string;
}): "above" | "below" | "above_below" | undefined => {
if (config.above !== undefined && config.below !== undefined) {
return "above_below";
}
if (config.above !== undefined) {
return "above";
}
if (config.below !== undefined) {
return "below";
}
return undefined;
};
const formatNumericLimitValue = (
hass: HomeAssistant,
value?: number | string
@@ -130,18 +146,26 @@ const formatNumericLimitValue = (
: value;
};
export interface DescribeOptions {
// Skip the user defined alias and describe the underlying config.
ignoreAlias?: boolean;
// Leave the entities out of the sentence, for rows that render them as
// target badges.
hideEntities?: boolean;
}
export const describeTrigger = (
trigger: Trigger,
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
ignoreAlias = false
options?: DescribeOptions
): string => {
try {
const description = tryDescribeTrigger(
trigger,
hass,
entityRegistry,
ignoreAlias
options
);
if (typeof description !== "string") {
throw new Error(String(description));
@@ -163,7 +187,7 @@ const tryDescribeTrigger = (
trigger: Trigger,
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
ignoreAlias = false
options?: DescribeOptions
) => {
if (isTriggerList(trigger)) {
const triggers = ensureArray(trigger.triggers);
@@ -179,14 +203,15 @@ const tryDescribeTrigger = (
});
}
if (trigger.alias && !ignoreAlias) {
if (trigger.alias && !options?.ignoreAlias) {
return trigger.alias;
}
const description = describeLegacyTrigger(
trigger as LegacyTrigger,
hass,
entityRegistry
entityRegistry,
options?.hideEntities
);
if (description) {
@@ -210,7 +235,8 @@ const tryDescribeTrigger = (
const describeLegacyTrigger = (
trigger: LegacyTrigger,
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[]
entityRegistry: EntityRegistryEntry[],
hideEntities = false
) => {
// Event Trigger
if (trigger.trigger === "event" && trigger.event_type) {
@@ -241,28 +267,16 @@ const describeLegacyTrigger = (
}
// Numeric State Trigger
if (trigger.trigger === "numeric_state" && trigger.entity_id) {
const entities: string[] = [];
if (
trigger.trigger === "numeric_state" &&
(trigger.entity_id || hideEntities)
) {
const states = hass.states;
const stateObj = Array.isArray(trigger.entity_id)
? hass.states[trigger.entity_id[0]]
: (hass.states[trigger.entity_id] as HassEntity | undefined);
if (Array.isArray(trigger.entity_id)) {
for (const entity of trigger.entity_id.values()) {
if (states[entity]) {
entities.push(computeStateName(states[entity]) || entity);
}
}
} else if (trigger.entity_id) {
entities.push(
states[trigger.entity_id]
? computeStateName(states[trigger.entity_id])
: trigger.entity_id
);
}
const attribute = trigger.attribute
? stateObj
? computeAttributeNameDisplay(
@@ -278,6 +292,39 @@ const describeLegacyTrigger = (
? describeDuration(hass.locale, trigger.for)
: undefined;
if (hideEntities) {
const suffix = numericThresholdSuffix(trigger);
if (!suffix) {
return hass.localize(
`${triggerTranslationBaseKey}.numeric_state.label`
);
}
return hass.localize(
`${triggerTranslationBaseKey}.numeric_state.description.crossed_${suffix}`,
{
attribute: attribute,
above: formatNumericLimitValue(hass, trigger.above),
below: formatNumericLimitValue(hass, trigger.below),
duration: duration,
}
);
}
const entities: string[] = [];
if (Array.isArray(trigger.entity_id)) {
for (const entity of trigger.entity_id.values()) {
if (states[entity]) {
entities.push(computeStateName(states[entity]) || entity);
}
}
} else if (trigger.entity_id) {
entities.push(
states[trigger.entity_id]
? computeStateName(states[trigger.entity_id])
: trigger.entity_id
);
}
if (trigger.above !== undefined && trigger.below !== undefined) {
return hass.localize(
`${triggerTranslationBaseKey}.numeric_state.description.above-below`,
@@ -319,14 +366,14 @@ const describeLegacyTrigger = (
// State Trigger
if (trigger.trigger === "state") {
const entities: string[] = [];
const states = hass.states;
const entityArray: string[] = ensureArray(trigger.entity_id);
const stateObj = hass.states[entityArray?.[0]] as HassEntity | undefined;
let attribute = "";
if (trigger.attribute) {
const stateObj = Array.isArray(trigger.entity_id)
? hass.states[trigger.entity_id[0]]
: (hass.states[trigger.entity_id] as HassEntity | undefined);
attribute = stateObj
? computeAttributeNameDisplay(
hass.localize,
@@ -337,17 +384,6 @@ const describeLegacyTrigger = (
: trigger.attribute;
}
const entityArray: string[] = ensureArray(trigger.entity_id);
if (entityArray) {
for (const entity of entityArray) {
if (states[entity]) {
entities.push(computeStateName(states[entity]) || entity);
}
}
}
const stateObj = hass.states[entityArray[0]] as HassEntity | undefined;
let fromChoice = "other";
let fromString = "";
if (trigger.from !== undefined) {
@@ -427,6 +463,32 @@ const describeLegacyTrigger = (
duration = describeDuration(hass.locale, trigger.for) ?? "";
}
if (hideEntities) {
return hass.localize(
`${triggerTranslationBaseKey}.state.description.changed`,
{
hasAttribute: attribute !== "" ? "true" : "false",
attribute: attribute,
anyChange: toChoice === "special" ? "true" : "false",
fromChoice: fromChoice,
fromString: fromString,
toChoice: toChoice,
toString: toString,
hasDuration: duration !== "" ? "true" : "false",
duration: duration,
}
);
}
const entities: string[] = [];
if (entityArray) {
for (const entity of entityArray) {
if (states[entity]) {
entities.push(computeStateName(states[entity]) || entity);
}
}
}
return hass.localize(
`${triggerTranslationBaseKey}.state.description.full`,
{
@@ -916,14 +978,14 @@ export const describeCondition = (
condition: Condition,
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
ignoreAlias = false
options?: DescribeOptions
): string => {
try {
const description = tryDescribeCondition(
condition,
hass,
entityRegistry,
ignoreAlias
options
);
if (typeof description !== "string") {
throw new Error(String(description));
@@ -945,7 +1007,7 @@ const tryDescribeCondition = (
condition: Condition,
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
ignoreAlias = false
options?: DescribeOptions
) => {
if (typeof condition === "string" && hasTemplate(condition)) {
return hass.localize(
@@ -953,7 +1015,7 @@ const tryDescribeCondition = (
);
}
if (condition.alias && !ignoreAlias) {
if (condition.alias && !options?.ignoreAlias) {
return condition.alias;
}
@@ -975,7 +1037,8 @@ const tryDescribeCondition = (
const description = describeLegacyCondition(
condition as LegacyCondition,
hass,
entityRegistry
entityRegistry,
options?.hideEntities
);
if (description) {
@@ -1001,7 +1064,8 @@ const tryDescribeCondition = (
const describeLegacyCondition = (
condition: LegacyCondition,
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[]
entityRegistry: EntityRegistryEntry[],
hideEntities = false
) => {
if (condition.condition === "or") {
const conditions = ensureArray(condition.conditions);
@@ -1058,17 +1122,20 @@ const describeLegacyCondition = (
// State Condition
if (condition.condition === "state") {
if (!condition.entity_id) {
if (!condition.entity_id && !hideEntities) {
return hass.localize(
`${conditionsTranslationBaseKey}.state.description.no_entity`
);
}
const stateObj = hass.states[
Array.isArray(condition.entity_id)
? condition.entity_id[0]
: condition.entity_id
] as HassEntity | undefined;
let attribute = "";
if (condition.attribute) {
const stateObj = Array.isArray(condition.entity_id)
? hass.states[condition.entity_id[0]]
: (hass.states[condition.entity_id] as HassEntity | undefined);
attribute = stateObj
? computeAttributeNameDisplay(
hass.localize,
@@ -1079,27 +1146,7 @@ const describeLegacyCondition = (
: condition.attribute;
}
const entities: string[] = [];
if (Array.isArray(condition.entity_id)) {
for (const entity of condition.entity_id.values()) {
if (hass.states[entity]) {
entities.push(computeStateName(hass.states[entity]) || entity);
}
}
} else if (condition.entity_id) {
entities.push(
hass.states[condition.entity_id]
? computeStateName(hass.states[condition.entity_id])
: condition.entity_id
);
}
const states: string[] = [];
const stateObj = hass.states[
Array.isArray(condition.entity_id)
? condition.entity_id[0]
: condition.entity_id
] as HassEntity | undefined;
if (Array.isArray(condition.state)) {
for (const state of condition.state.values()) {
states.push(
@@ -1116,7 +1163,7 @@ const describeLegacyCondition = (
: state
);
}
} else if (condition.state !== "") {
} else if (condition.state != null && condition.state !== "") {
states.push(
stateObj
? condition.attribute
@@ -1137,6 +1184,37 @@ const describeLegacyCondition = (
duration = describeDuration(hass.locale, condition.for) || "";
}
if (hideEntities) {
if (states.length === 0) {
return hass.localize(`${conditionsTranslationBaseKey}.state.label`);
}
return hass.localize(
`${conditionsTranslationBaseKey}.state.description.is`,
{
hasAttribute: attribute !== "" ? "true" : "false",
attribute: attribute,
states: formatListWithOrs(hass.locale, states),
hasDuration: duration !== "" ? "true" : "false",
duration: duration,
}
);
}
const entities: string[] = [];
if (Array.isArray(condition.entity_id)) {
for (const entity of condition.entity_id.values()) {
if (hass.states[entity]) {
entities.push(computeStateName(hass.states[entity]) || entity);
}
}
} else if (condition.entity_id) {
entities.push(
hass.states[condition.entity_id]
? computeStateName(hass.states[condition.entity_id])
: condition.entity_id
);
}
return hass.localize(
`${conditionsTranslationBaseKey}.state.description.full`,
{
@@ -1159,15 +1237,14 @@ const describeLegacyCondition = (
}
// Numeric State Condition
if (condition.condition === "numeric_state" && condition.entity_id) {
const entity_ids = ensureArray(condition.entity_id);
if (
condition.condition === "numeric_state" &&
(condition.entity_id || hideEntities)
) {
const entity_ids = condition.entity_id
? ensureArray(condition.entity_id)
: [];
const stateObj = hass.states[entity_ids[0]] as HassEntity | undefined;
const entity = formatListWithAnds(
hass.locale,
entity_ids.map((id) =>
hass.states[id] ? computeStateName(hass.states[id]) : id || ""
)
);
const attribute = condition.attribute
? stateObj
@@ -1180,6 +1257,30 @@ const describeLegacyCondition = (
: condition.attribute
: undefined;
if (hideEntities) {
const suffix = numericThresholdSuffix(condition);
if (!suffix) {
return hass.localize(
`${conditionsTranslationBaseKey}.numeric_state.label`
);
}
return hass.localize(
`${conditionsTranslationBaseKey}.numeric_state.description.is_${suffix}`,
{
attribute,
above: formatNumericLimitValue(hass, condition.above),
below: formatNumericLimitValue(hass, condition.below),
}
);
}
const entity = formatListWithAnds(
hass.locale,
entity_ids.map((id) =>
hass.states[id] ? computeStateName(hass.states[id]) : id || ""
)
);
if (condition.above !== undefined && condition.below !== undefined) {
return hass.localize(
`${conditionsTranslationBaseKey}.numeric_state.description.above-below`,
@@ -52,7 +52,6 @@ import type {
AutomationClipboard,
Condition,
ConditionSidebarConfig,
PlatformCondition,
} from "../../../../data/automation";
import { isCondition, testCondition } from "../../../../data/automation";
import { describeCondition } from "../../../../data/automation_i18n";
@@ -64,7 +63,6 @@ import {
type ValidConfig,
} from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context";
import type { DeviceCondition } from "../../../../data/device/device_automation";
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
import type { TargetSelector } from "../../../../data/selector";
import {
@@ -76,6 +74,8 @@ import { isMac } from "../../../../util/is_mac";
import { showEditorToast } from "../editor-toast";
import "../ha-automation-editor-warning";
import { overflowStyles, rowStyles } from "../styles";
import { getDeviceTarget } from "../target/get_device_target";
import { getEntityTarget } from "../target/get_entity_target";
import "../target/ha-automation-row-targets";
import "./ha-automation-condition-editor";
import type HaAutomationConditionEditor from "./ha-automation-condition-editor";
@@ -182,12 +182,14 @@ export default class HaAutomationConditionRow extends LitElement {
const descriptionHasTarget =
"target" in (this.conditionDescriptions[this.condition.condition] || {});
const target = descriptionHasTarget
? (this.condition as PlatformCondition).target
: "device_id" in this.condition &&
(this.condition as DeviceCondition).device_id
? { device_id: [(this.condition as DeviceCondition).device_id] }
: undefined;
const hasEntityTarget =
this.condition.condition === "state" ||
this.condition.condition === "numeric_state";
const target = this._getTarget(descriptionHasTarget, hasEntityTarget);
const targetRequired =
(descriptionHasTarget || hasEntityTarget) && !this._isNew;
const conditionTargetSpec =
this.conditionDescriptions[this.condition.condition]?.target;
@@ -224,13 +226,15 @@ export default class HaAutomationConditionRow extends LitElement {
}
<h3 slot="header">
${capitalizeFirstLetter(
describeCondition(this.condition, this.hass, this._entityReg)
describeCondition(this.condition, this.hass, this._entityReg, {
hideEntities: true,
})
)}
${
target !== undefined || (descriptionHasTarget && !this._isNew)
target !== undefined || targetRequired
? this._renderTargets(
target,
descriptionHasTarget && !this._isNew,
targetRequired,
conditionTargetSpec,
this.condition.condition !== "device"
)
@@ -600,6 +604,30 @@ export default class HaAutomationConditionRow extends LitElement {
`;
}
private _getEntityTarget = memoizeOne(getEntityTarget);
private _getDeviceTarget = memoizeOne(getDeviceTarget);
private _getTarget(
descriptionHasTarget: boolean,
hasEntityTarget: boolean
): HassServiceTarget | undefined {
if (descriptionHasTarget && "target" in this.condition) {
return this.condition.target;
}
if (
"entity_id" in this.condition &&
this.condition.entity_id &&
hasEntityTarget
) {
return this._getEntityTarget(this.condition.entity_id);
}
if ("device_id" in this.condition && this.condition.device_id) {
return this._getDeviceTarget(this.condition.device_id);
}
return undefined;
}
private _renderTargets = memoizeOne(
(
target?: HassServiceTarget,
@@ -777,7 +805,9 @@ export default class HaAutomationConditionRow extends LitElement {
),
inputType: "string",
placeholder: capitalizeFirstLetter(
describeCondition(this.condition, this.hass, this._entityReg, true)
describeCondition(this.condition, this.hass, this._entityReg, {
ignoreAlias: true,
})
),
defaultValue: this.condition.alias,
confirmText: this.hass.localize("ui.common.submit"),
@@ -0,0 +1,6 @@
import type { HassServiceTarget } from "home-assistant-js-websocket";
export const getDeviceTarget = (
deviceId?: string
): HassServiceTarget | undefined =>
deviceId ? { device_id: [deviceId] } : undefined;
@@ -0,0 +1,9 @@
import type { HassServiceTarget } from "home-assistant-js-websocket";
import { ensureArray } from "../../../../common/array/ensure-array";
export const getEntityTarget = (
entityId?: string | string[]
): HassServiceTarget | undefined => {
const entityIds = entityId ? ensureArray(entityId).filter(Boolean) : [];
return entityIds.length ? { entity_id: entityIds } : undefined;
};
@@ -60,7 +60,6 @@ import { isTrigger, subscribeTrigger } from "../../../../data/automation";
import { describeTrigger } from "../../../../data/automation_i18n";
import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context";
import type { DeviceTrigger } from "../../../../data/device/device_automation";
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
import type { TargetSelector } from "../../../../data/selector";
import type { TriggerDescriptions } from "../../../../data/trigger";
@@ -74,6 +73,8 @@ import { isMac } from "../../../../util/is_mac";
import { showEditorToast } from "../editor-toast";
import "../ha-automation-editor-warning";
import { overflowStyles, rowStyles } from "../styles";
import { getDeviceTarget } from "../target/get_device_target";
import { getEntityTarget } from "../target/get_entity_target";
import "../target/ha-automation-row-targets";
import "./ha-automation-trigger-editor";
import type HaAutomationTriggerEditor from "./ha-automation-trigger-editor";
@@ -214,11 +215,12 @@ export default class HaAutomationTriggerRow extends LitElement {
"target" in
this.triggerDescriptions[(this.trigger as PlatformTrigger).trigger];
const target = descriptionHasTarget
? (this.trigger as PlatformTrigger).target
: type === "device" && (this.trigger as DeviceTrigger).device_id
? { device_id: (this.trigger as DeviceTrigger).device_id }
: undefined;
const hasEntityTarget = type === "state" || type === "numeric_state";
const target = this._getTarget(type, descriptionHasTarget, hasEntityTarget);
const targetRequired =
(descriptionHasTarget || hasEntityTarget) && !this._isNew;
const triggerTargetSpec =
type === "platform"
@@ -248,12 +250,16 @@ export default class HaAutomationTriggerRow extends LitElement {
></ha-trigger-icon>`
}
<h3 slot="header">
${describeTrigger(this.trigger, this.hass, this._entityReg)}
${capitalizeFirstLetter(
describeTrigger(this.trigger, this.hass, this._entityReg, {
hideEntities: true,
})
)}
${
target !== undefined || (descriptionHasTarget && !this._isNew)
target !== undefined || targetRequired
? this._renderTargets(
target,
descriptionHasTarget && !this._isNew,
targetRequired,
triggerTargetSpec,
type !== "device"
)
@@ -595,6 +601,27 @@ export default class HaAutomationTriggerRow extends LitElement {
`;
}
private _getEntityTarget = memoizeOne(getEntityTarget);
private _getDeviceTarget = memoizeOne(getDeviceTarget);
private _getTarget(
type: string,
descriptionHasTarget: boolean,
hasEntityTarget: boolean
): HassServiceTarget | undefined {
if (descriptionHasTarget && "target" in this.trigger) {
return this.trigger.target;
}
if (hasEntityTarget && "entity_id" in this.trigger) {
return this._getEntityTarget(this.trigger.entity_id);
}
if (type === "device" && "device_id" in this.trigger) {
return this._getDeviceTarget(this.trigger.device_id);
}
return undefined;
}
private _renderTargets = memoizeOne(
(
target?: HassServiceTarget,
@@ -857,7 +884,9 @@ export default class HaAutomationTriggerRow extends LitElement {
),
inputType: "string",
placeholder: capitalizeFirstLetter(
describeTrigger(this.trigger, this.hass, this._entityReg, true)
describeTrigger(this.trigger, this.hass, this._entityReg, {
ignoreAlias: true,
})
),
defaultValue: this.trigger.alias,
confirmText: this.hass.localize("ui.common.submit"),
+19 -11
View File
@@ -702,9 +702,9 @@
"geo_location": "[%key:ui::panel::config::automation::editor::triggers::type::geo_location::label%]",
"homeassistant": "[%key:ui::panel::config::automation::editor::triggers::type::homeassistant::label%]",
"mqtt": "[%key:ui::panel::config::automation::editor::triggers::type::mqtt::label%]",
"numeric_state": "[%key:ui::panel::config::automation::editor::triggers::type::numeric_state::label%]",
"numeric_state": "[%key:ui::panel::config::automation::editor::conditions::type::numeric_state::label%]",
"persistent_notification": "[%key:ui::panel::config::automation::editor::triggers::type::persistent_notification::label%]",
"state": "[%key:ui::panel::config::automation::editor::triggers::type::state::label%]",
"state": "[%key:ui::panel::config::automation::editor::conditions::type::state::label%]",
"sun": "[%key:ui::panel::config::automation::editor::triggers::type::sun::label%]",
"tag": "[%key:ui::panel::config::automation::editor::triggers::type::tag::label%]",
"template": "[%key:ui::panel::config::automation::editor::triggers::type::template::label%]",
@@ -5348,7 +5348,7 @@
}
},
"state": {
"label": "State",
"label": "State changed",
"attribute": "Attribute (optional)",
"from": "From (optional)",
"for": "For",
@@ -5356,7 +5356,8 @@
"any_state_ignore_attributes": "Any state (ignoring attribute changes)",
"description": {
"picker": "Triggers when the state of an entity (or attribute) changes.",
"full": "When{hasAttribute, select, \n true { {attribute} of} \n other {}\n} {hasEntity, select, \n true {{entity}} \n other {something}\n} changes{fromChoice, select, \n fromUsed { from {fromString}}\n null { from any state} \n other {}\n}{toChoice, select, \n toUsed { to {toString}} \n null { to any state} \n special { state or any attributes} \n other {}\n}{hasDuration, select, \n true { for {duration}} \n other {}\n}"
"full": "When{hasAttribute, select, \n true { {attribute} of} \n other {}\n} {hasEntity, select, \n true {{entity}} \n other {something}\n} changes{fromChoice, select, \n fromUsed { from {fromString}}\n null { from any state} \n other {}\n}{toChoice, select, \n toUsed { to {toString}} \n null { to any state} \n special { state or any attributes} \n other {}\n}{hasDuration, select, \n true { for {duration}} \n other {}\n}",
"changed": "{hasAttribute, select, \n true {{attribute}} \n other {State}\n}{anyChange, select, \n true { or any attribute} \n other {}\n} changed{fromChoice, select, \n fromUsed { from {fromString}}\n null { from any state} \n other {}\n}{toChoice, select, \n toUsed { to {toString}} \n null { to any state} \n other {}\n}{hasDuration, select, \n true { for {duration}} \n other {}\n}"
},
"for_type": {
"choices": {
@@ -5386,7 +5387,7 @@
}
},
"numeric_state": {
"label": "Numeric state",
"label": "Numeric state crossed threshold",
"above": "Above",
"below": "Below",
"lower_limit": "Lower limit",
@@ -5398,7 +5399,10 @@
"picker": "Triggers when the numeric value of an entity''s state (or attribute''s value) crosses a given threshold.",
"above": "When {attribute, select, \n undefined {} \n other {{attribute} from }\n }{entity} {numberOfEntities, plural,\n one {is}\n other {is}\n} above {above}{duration, select, \n undefined {} \n other { for {duration}}\n }",
"below": "When {attribute, select, \n undefined {} \n other {{attribute} from }\n }{entity} {numberOfEntities, plural,\n one {is}\n other {is}\n} below {below}{duration, select, \n undefined {} \n other { for {duration}}\n }",
"above-below": "When {attribute, select, \n undefined {} \n other {{attribute} from }\n }{entity} {numberOfEntities, plural,\n one {is}\n other {is}\n} above {above} and below {below}{duration, select, \n undefined {} \n other { for {duration}}\n }"
"above-below": "When {attribute, select, \n undefined {} \n other {{attribute} from }\n }{entity} {numberOfEntities, plural,\n one {is}\n other {is}\n} above {above} and below {below}{duration, select, \n undefined {} \n other { for {duration}}\n }",
"crossed_above": "{attribute, select, \n undefined {Numeric state} \n other {{attribute}}\n} crossed above {above}{duration, select, \n undefined {} \n other { for {duration}}\n}",
"crossed_below": "{attribute, select, \n undefined {Numeric state} \n other {{attribute}}\n} crossed below {below}{duration, select, \n undefined {} \n other { for {duration}}\n}",
"crossed_above_below": "{attribute, select, \n undefined {Numeric state} \n other {{attribute}}\n} crossed above {above} and below {below}{duration, select, \n undefined {} \n other { for {duration}}\n}"
},
"threshold_type": {
"choices": {
@@ -5642,7 +5646,7 @@
"numeric_state": {
"type_value": "[%key:ui::panel::config::automation::editor::triggers::type::numeric_state::type_value%]",
"type_input": "[%key:ui::panel::config::automation::editor::triggers::type::numeric_state::type_input%]",
"label": "[%key:ui::panel::config::automation::editor::triggers::type::numeric_state::label%]",
"label": "Numeric state",
"above": "[%key:ui::panel::config::automation::editor::triggers::type::numeric_state::above%]",
"below": "[%key:ui::panel::config::automation::editor::triggers::type::numeric_state::below%]",
"lower_limit": "[%key:ui::panel::config::automation::editor::triggers::type::numeric_state::lower_limit%]",
@@ -5652,7 +5656,10 @@
"picker": "Tests if the numeric value of an entity's state (or attribute's value) is above or below a given threshold.",
"above": "If {attribute, select, \n undefined {} \n other {{attribute} from }\n }{entity} {numberOfEntities, plural,\n one {is}\n other {are}\n} above {above}",
"below": "If {attribute, select, \n undefined {} \n other {{attribute} from }\n }{entity} {numberOfEntities, plural,\n one {is}\n other {are}\n} below {below}",
"above-below": "If {attribute, select, \n undefined {} \n other {{attribute} from }\n }{entity} {numberOfEntities, plural,\n one {is}\n other {are}\n} above {above} and below {below}"
"above-below": "If {attribute, select, \n undefined {} \n other {{attribute} from }\n }{entity} {numberOfEntities, plural,\n one {is}\n other {are}\n} above {above} and below {below}",
"is_above": "{attribute, select, \n undefined {Numeric state} \n other {{attribute}}\n} is above {above}",
"is_below": "{attribute, select, \n undefined {Numeric state} \n other {{attribute}}\n} is below {below}",
"is_above_below": "{attribute, select, \n undefined {Numeric state} \n other {{attribute}}\n} is above {above} and below {below}"
}
},
"or": {
@@ -5664,12 +5671,13 @@
}
},
"state": {
"label": "[%key:ui::panel::config::automation::editor::triggers::type::state::label%]",
"state": "[%key:ui::panel::config::automation::editor::triggers::type::state::label%]",
"label": "State",
"state": "[%key:ui::panel::config::automation::editor::conditions::type::state::label%]",
"description": {
"picker": "Tests if an entity (or attribute) is in a specific state.",
"no_entity": "If state confirmed",
"full": "If{hasAttribute, select, \n true { {attribute} of}\n other {}\n} {numberOfEntities, plural,\n =0 {an entity is}\n one {{entities} is}\n other {{entities} {matchAny, select,\n true {is}\n other {are}\n}}\n} {numberOfStates, plural,\n =0 {a state}\n other {{states}}\n}{hasDuration, select, \n true { for {duration}} \n other {}\n }"
"full": "If{hasAttribute, select, \n true { {attribute} of}\n other {}\n} {numberOfEntities, plural,\n =0 {an entity is}\n one {{entities} is}\n other {{entities} {matchAny, select,\n true {is}\n other {are}\n}}\n} {numberOfStates, plural,\n =0 {a state}\n other {{states}}\n}{hasDuration, select, \n true { for {duration}} \n other {}\n }",
"is": "{hasAttribute, select, \n true {{attribute}} \n other {State}\n} is {states}{hasDuration, select, \n true { for {duration}} \n other {}\n}"
}
},
"sun": {
@@ -0,0 +1,155 @@
import { IntlMessageFormat } from "intl-messageformat";
import { describe, expect, it } from "vitest";
import type { Condition, Trigger } from "../../src/data/automation";
import {
describeCondition,
describeTrigger,
} from "../../src/data/automation_i18n";
import {
DateFormat,
FirstWeekday,
NumberFormat,
TimeFormat,
TimeZone,
} from "../../src/data/translation";
import en from "../../src/translations/en.json";
import type { HomeAssistant } from "../../src/types";
type TranslationNode = string | { [key: string]: TranslationNode };
const localize = (key: string, values?: Record<string, unknown>) => {
const message = key
.split(".")
.reduce<TranslationNode | undefined>(
(translations, part) =>
typeof translations === "object" ? translations[part] : undefined,
en as TranslationNode
);
return typeof message === "string"
? (new IntlMessageFormat(message, "en").format(values) as string)
: "";
};
const hass = {
localize,
locale: {
language: "en",
number_format: NumberFormat.language,
time_format: TimeFormat.twenty_four,
date_format: DateFormat.language,
first_weekday: FirstWeekday.language,
time_zone: TimeZone.local,
},
config: { time_zone: "Etc/UTC" },
states: {
"light.kitchen": {
entity_id: "light.kitchen",
state: "on",
attributes: { friendly_name: "Kitchen light" },
},
"sensor.temperature": {
entity_id: "sensor.temperature",
state: "21",
attributes: { friendly_name: "Temperature" },
},
},
entities: {},
formatEntityState: (_stateObj, state?: string) => state ?? "",
} as unknown as HomeAssistant;
const describeRowTrigger = (trigger: Trigger) =>
describeTrigger(trigger, hass, [], { hideEntities: true });
const describeRowCondition = (condition: Condition) =>
describeCondition(condition, hass, [], { hideEntities: true });
describe("describing state triggers and conditions", () => {
const trigger: Trigger = {
trigger: "state",
entity_id: "light.kitchen",
to: "on",
};
const condition: Condition = {
condition: "state",
entity_id: "light.kitchen",
state: "on",
};
it("names the entities by default", () => {
expect(describeTrigger(trigger, hass, [])).toBe(
"When Kitchen light changes to on"
);
expect(describeCondition(condition, hass, [])).toBe(
"If Kitchen light is on"
);
});
it("leaves the entities out when they are rendered as targets", () => {
expect(describeRowTrigger(trigger)).toBe("State changed to on");
expect(describeRowCondition(condition)).toBe("State is on");
});
it("falls back to the label when nothing is configured yet", () => {
expect(
describeRowTrigger({ trigger: "state", entity_id: "light.kitchen" })
).toBe("State or any attribute changed");
expect(
describeRowCondition({
condition: "state",
entity_id: "light.kitchen",
state: [],
})
).toBe("State");
});
});
describe("describing numeric state triggers and conditions", () => {
const trigger: Trigger = {
trigger: "numeric_state",
entity_id: "sensor.temperature",
above: 20,
};
const condition: Condition = {
condition: "numeric_state",
entity_id: "sensor.temperature",
above: 20,
};
it("names the entities by default", () => {
expect(describeTrigger(trigger, hass, [])).toBe(
"When Temperature is above 20"
);
expect(describeCondition(condition, hass, [])).toBe(
"If Temperature is above 20"
);
});
it("leaves the entities out when they are rendered as targets", () => {
expect(describeRowTrigger(trigger)).toBe("Numeric state crossed above 20");
expect(describeRowCondition(condition)).toBe("Numeric state is above 20");
});
it("describes both thresholds", () => {
expect(describeRowTrigger({ ...trigger, below: 30 })).toBe(
"Numeric state crossed above 20 and below 30"
);
expect(describeRowCondition({ ...condition, below: 30 })).toBe(
"Numeric state is above 20 and below 30"
);
});
it("falls back to the label without a threshold", () => {
expect(
describeRowTrigger({
trigger: "numeric_state",
entity_id: "sensor.temperature",
})
).toBe("Numeric state crossed threshold");
expect(
describeRowCondition({
condition: "numeric_state",
entity_id: "sensor.temperature",
})
).toBe("Numeric state");
});
});