mirror of
https://github.com/home-assistant/frontend.git
synced 2026-08-21 14:18:17 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00ccdc497c |
@@ -1,4 +1,5 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { dump } from "js-yaml";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
@@ -7,8 +8,16 @@ import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_tim
|
||||
import type { Trigger } from "../../data/automation";
|
||||
import { migrateAutomationTrigger } from "../../data/automation";
|
||||
import { describeCondition, describeTrigger } from "../../data/automation_i18n";
|
||||
import { fullEntitiesContext, labelsContext } from "../../data/context";
|
||||
import type { ConditionDescriptions } from "../../data/condition";
|
||||
import {
|
||||
conditionDescriptionsContext,
|
||||
fullEntitiesContext,
|
||||
labelsContext,
|
||||
manifestsContext,
|
||||
triggerDescriptionsContext,
|
||||
} from "../../data/context";
|
||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
import type { DomainManifestLookup } from "../../data/integration";
|
||||
import type { LabelRegistryEntry } from "../../data/label/label_registry";
|
||||
import type { LogbookEntry } from "../../data/logbook";
|
||||
import { describeAction } from "../../data/script_i18n";
|
||||
@@ -17,7 +26,12 @@ import type {
|
||||
ChooseActionTraceStep,
|
||||
TraceExtended,
|
||||
} from "../../data/trace";
|
||||
import type { TargetSelector } from "../../data/selector";
|
||||
import { getDataFromPath, isTriggerPath } from "../../data/trace";
|
||||
import type { TriggerDescriptions } from "../../data/trigger";
|
||||
import { getDeviceTarget } from "../../panels/config/automation/target/get_device_target";
|
||||
import { getEntityTarget } from "../../panels/config/automation/target/get_entity_target";
|
||||
import "../../panels/config/automation/target/ha-automation-row-targets";
|
||||
import "../../panels/logbook/ha-logbook-renderer";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-alert";
|
||||
@@ -67,6 +81,18 @@ export class HaTracePathDetails extends LitElement {
|
||||
@consume({ context: labelsContext, subscribe: true })
|
||||
_labelReg!: LabelRegistryEntry[];
|
||||
|
||||
@state()
|
||||
@consume({ context: manifestsContext, subscribe: true })
|
||||
private _manifests?: DomainManifestLookup;
|
||||
|
||||
@state()
|
||||
@consume({ context: triggerDescriptionsContext, subscribe: true })
|
||||
private _triggerDescriptions?: TriggerDescriptions;
|
||||
|
||||
@state()
|
||||
@consume({ context: conditionDescriptionsContext, subscribe: true })
|
||||
private _conditionDescriptions?: ConditionDescriptions;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="padded-box trace-info">
|
||||
@@ -191,51 +217,8 @@ export class HaTracePathDetails extends LitElement {
|
||||
)}`;
|
||||
}
|
||||
|
||||
const selectedType = this.selected.type;
|
||||
|
||||
return html`
|
||||
${
|
||||
curPath === this.selected.path
|
||||
? currentDetail.alias
|
||||
? html`<h2>${currentDetail.alias}</h2>`
|
||||
: selectedType === "trigger"
|
||||
? html`<h2>
|
||||
${describeTrigger(
|
||||
migrateAutomationTrigger({
|
||||
...currentDetail,
|
||||
}) as Trigger,
|
||||
this.hass,
|
||||
this._entityReg
|
||||
)}
|
||||
</h2>`
|
||||
: selectedType === "condition"
|
||||
? html`<h2>
|
||||
${describeCondition(
|
||||
currentDetail,
|
||||
this.hass,
|
||||
this._entityReg
|
||||
)}
|
||||
</h2>`
|
||||
: selectedType === "action"
|
||||
? html`<h2>
|
||||
${describeAction(
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
currentDetail
|
||||
)}
|
||||
</h2>`
|
||||
: selectedType === "chooseOption"
|
||||
? html`<h2>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.option",
|
||||
{ number: pathParts[pathParts.length - 1] }
|
||||
)}
|
||||
</h2>`
|
||||
: nothing
|
||||
: html`<h2>
|
||||
${curPath.substring(this.selected.path.length + 1)}
|
||||
</h2>`
|
||||
}
|
||||
${this._renderStepHeading(curPath, currentDetail, pathParts)}
|
||||
${
|
||||
data.length === 1
|
||||
? nothing
|
||||
@@ -246,17 +229,7 @@ export class HaTracePathDetails extends LitElement {
|
||||
)}
|
||||
</h3>`
|
||||
}
|
||||
${
|
||||
curPath
|
||||
.substring(this.selected.path.length + 1)
|
||||
.includes("condition")
|
||||
? html`[${describeCondition(
|
||||
currentDetail,
|
||||
this.hass,
|
||||
this._entityReg
|
||||
)}]<br />`
|
||||
: nothing
|
||||
}
|
||||
${this._renderNestedCondition(curPath, currentDetail)}
|
||||
${this.hass!.localize(
|
||||
"ui.panel.config.automation.trace.path.executed",
|
||||
{
|
||||
@@ -324,6 +297,130 @@ export class HaTracePathDetails extends LitElement {
|
||||
return parts;
|
||||
}
|
||||
|
||||
private _renderStepHeading(
|
||||
curPath: string,
|
||||
currentDetail: any,
|
||||
pathParts: string[]
|
||||
) {
|
||||
if (curPath !== this.selected.path) {
|
||||
return html`<div class="heading">
|
||||
<h2>${curPath.substring(this.selected.path.length + 1)}</h2>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const selectedType = this.selected.type;
|
||||
|
||||
const description = currentDetail.alias
|
||||
? currentDetail.alias
|
||||
: selectedType === "trigger"
|
||||
? describeTrigger(
|
||||
migrateAutomationTrigger({ ...currentDetail }) as Trigger,
|
||||
this.hass,
|
||||
this._entityReg
|
||||
)
|
||||
: selectedType === "condition"
|
||||
? describeCondition(currentDetail, this.hass, this._entityReg)
|
||||
: selectedType === "action"
|
||||
? describeAction(
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
currentDetail,
|
||||
undefined,
|
||||
false,
|
||||
this._manifests
|
||||
)
|
||||
: selectedType === "chooseOption"
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.type.choose.option",
|
||||
{ number: pathParts[pathParts.length - 1] }
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (description === undefined) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<div class="heading">
|
||||
<h2>${description}</h2>
|
||||
${this._renderTargets(currentDetail, selectedType)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _renderNestedCondition(curPath: string, currentDetail: any) {
|
||||
if (
|
||||
!curPath.substring(this.selected.path.length + 1).includes("condition")
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<div class="nested-condition">
|
||||
${describeCondition(currentDetail, this.hass, this._entityReg)}
|
||||
${this._renderTargets(currentDetail, "condition", "s")}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _renderTargets(
|
||||
config: any,
|
||||
type: NodeInfo["type"],
|
||||
size: "s" | "m" = "m"
|
||||
) {
|
||||
const target = this._getTarget(config, type);
|
||||
if (!target) {
|
||||
return nothing;
|
||||
}
|
||||
const targetSpec = this._getTargetSelector(config, type);
|
||||
return html`<div class="targets">
|
||||
<ha-automation-row-targets
|
||||
.target=${target}
|
||||
.selector=${targetSpec ? { target: targetSpec } : undefined}
|
||||
.size=${size}
|
||||
interactive
|
||||
></ha-automation-row-targets>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _getTargetSelector(
|
||||
config: any,
|
||||
type: NodeInfo["type"]
|
||||
): TargetSelector["target"] | undefined {
|
||||
if (type === "trigger") {
|
||||
return this._triggerDescriptions?.[config.trigger]?.target;
|
||||
}
|
||||
if (type === "condition") {
|
||||
return this._conditionDescriptions?.[config.condition]?.target;
|
||||
}
|
||||
if (type === "action" && typeof config.action === "string") {
|
||||
const [domain, service] = config.action.split(".", 2);
|
||||
return this.hass.services?.[domain]?.[service]?.target;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _getTarget(
|
||||
config: any,
|
||||
type: NodeInfo["type"]
|
||||
): HassServiceTarget | undefined {
|
||||
if (config.target) {
|
||||
return config.target;
|
||||
}
|
||||
if (type === "trigger" || type === "condition") {
|
||||
const element = type === "trigger" ? config.trigger : config.condition;
|
||||
if (element === "state" || element === "numeric_state") {
|
||||
return getEntityTarget(config.entity_id);
|
||||
}
|
||||
if (element === "device") {
|
||||
return getDeviceTarget(config.device_id);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
if (type === "action") {
|
||||
return config.entity_id
|
||||
? getEntityTarget(config.entity_id)
|
||||
: getDeviceTarget(config.device_id);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _renderSelectedConfig() {
|
||||
if (!this.selected?.path) {
|
||||
return nothing;
|
||||
@@ -463,6 +560,42 @@ export class HaTracePathDetails extends LitElement {
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
.heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ha-space-2);
|
||||
margin: var(--ha-space-4) 0;
|
||||
}
|
||||
|
||||
.heading h2 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.heading .targets {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.targets {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ha-space-2);
|
||||
margin-top: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.nested-condition {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ha-space-2);
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.nested-condition .targets {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
+35
-223
@@ -149,9 +149,6 @@ const formatNumericLimitValue = (
|
||||
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 = (
|
||||
@@ -210,8 +207,7 @@ const tryDescribeTrigger = (
|
||||
const description = describeLegacyTrigger(
|
||||
trigger as LegacyTrigger,
|
||||
hass,
|
||||
entityRegistry,
|
||||
options?.hideEntities
|
||||
entityRegistry
|
||||
);
|
||||
|
||||
if (description) {
|
||||
@@ -235,8 +231,7 @@ const tryDescribeTrigger = (
|
||||
const describeLegacyTrigger = (
|
||||
trigger: LegacyTrigger,
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
hideEntities = false
|
||||
entityRegistry: EntityRegistryEntry[]
|
||||
) => {
|
||||
// Event Trigger
|
||||
if (trigger.trigger === "event" && trigger.event_type) {
|
||||
@@ -267,12 +262,7 @@ const describeLegacyTrigger = (
|
||||
}
|
||||
|
||||
// Numeric State Trigger
|
||||
if (
|
||||
trigger.trigger === "numeric_state" &&
|
||||
(trigger.entity_id || hideEntities)
|
||||
) {
|
||||
const states = hass.states;
|
||||
|
||||
if (trigger.trigger === "numeric_state") {
|
||||
const stateObj = Array.isArray(trigger.entity_id)
|
||||
? hass.states[trigger.entity_id[0]]
|
||||
: (hass.states[trigger.entity_id] as HassEntity | undefined);
|
||||
@@ -292,82 +282,23 @@ const describeLegacyTrigger = (
|
||||
? describeDuration(hass.locale, trigger.for)
|
||||
: undefined;
|
||||
|
||||
if (hideEntities) {
|
||||
const suffix = numericThresholdSuffix(trigger);
|
||||
if (!suffix) {
|
||||
return hass.localize(
|
||||
`${triggerTranslationBaseKey}.numeric_state.label`
|
||||
);
|
||||
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,
|
||||
}
|
||||
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`,
|
||||
{
|
||||
attribute: attribute,
|
||||
entity: formatListWithOrs(hass.locale, entities),
|
||||
numberOfEntities: entities.length,
|
||||
above: formatNumericLimitValue(hass, trigger.above),
|
||||
below: formatNumericLimitValue(hass, trigger.below),
|
||||
duration: duration,
|
||||
}
|
||||
);
|
||||
}
|
||||
if (trigger.above !== undefined) {
|
||||
return hass.localize(
|
||||
`${triggerTranslationBaseKey}.numeric_state.description.above`,
|
||||
{
|
||||
attribute: attribute,
|
||||
entity: formatListWithOrs(hass.locale, entities),
|
||||
numberOfEntities: entities.length,
|
||||
above: formatNumericLimitValue(hass, trigger.above),
|
||||
duration: duration,
|
||||
}
|
||||
);
|
||||
}
|
||||
if (trigger.below !== undefined) {
|
||||
return hass.localize(
|
||||
`${triggerTranslationBaseKey}.numeric_state.description.below`,
|
||||
{
|
||||
attribute: attribute,
|
||||
entity: formatListWithOrs(hass.locale, entities),
|
||||
numberOfEntities: entities.length,
|
||||
below: formatNumericLimitValue(hass, trigger.below),
|
||||
duration: duration,
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// State Trigger
|
||||
if (trigger.trigger === "state") {
|
||||
const states = hass.states;
|
||||
|
||||
const entityArray: string[] = ensureArray(trigger.entity_id);
|
||||
|
||||
const stateObj = hass.states[entityArray?.[0]] as HassEntity | undefined;
|
||||
@@ -463,39 +394,12 @@ 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`,
|
||||
`${triggerTranslationBaseKey}.state.description.changed`,
|
||||
{
|
||||
hasAttribute: attribute !== "" ? "true" : "false",
|
||||
attribute: attribute,
|
||||
hasEntity: entities.length !== 0 ? "true" : "false",
|
||||
entity: formatListWithOrs(hass.locale, entities),
|
||||
anyChange: toChoice === "special" ? "true" : "false",
|
||||
fromChoice: fromChoice,
|
||||
fromString: fromString,
|
||||
toChoice: toChoice,
|
||||
@@ -1037,8 +941,7 @@ const tryDescribeCondition = (
|
||||
const description = describeLegacyCondition(
|
||||
condition as LegacyCondition,
|
||||
hass,
|
||||
entityRegistry,
|
||||
options?.hideEntities
|
||||
entityRegistry
|
||||
);
|
||||
|
||||
if (description) {
|
||||
@@ -1064,8 +967,7 @@ const tryDescribeCondition = (
|
||||
const describeLegacyCondition = (
|
||||
condition: LegacyCondition,
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
hideEntities = false
|
||||
entityRegistry: EntityRegistryEntry[]
|
||||
) => {
|
||||
if (condition.condition === "or") {
|
||||
const conditions = ensureArray(condition.conditions);
|
||||
@@ -1122,12 +1024,6 @@ const describeLegacyCondition = (
|
||||
|
||||
// State Condition
|
||||
if (condition.condition === "state") {
|
||||
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]
|
||||
@@ -1184,51 +1080,14 @@ 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,
|
||||
}
|
||||
);
|
||||
if (states.length === 0) {
|
||||
return hass.localize(`${conditionsTranslationBaseKey}.state.label`);
|
||||
}
|
||||
|
||||
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`,
|
||||
`${conditionsTranslationBaseKey}.state.description.is`,
|
||||
{
|
||||
hasAttribute: attribute !== "" ? "true" : "false",
|
||||
attribute: attribute,
|
||||
numberOfEntities: entities.length,
|
||||
// With "any", entities are joined with "or", which takes a singular
|
||||
// verb in English even for multiple entities ("A or B is ...").
|
||||
matchAny: condition.match === "any" ? "true" : "false",
|
||||
entities:
|
||||
condition.match === "any"
|
||||
? formatListWithOrs(hass.locale, entities)
|
||||
: formatListWithAnds(hass.locale, entities),
|
||||
numberOfStates: states.length,
|
||||
states: formatListWithOrs(hass.locale, states),
|
||||
hasDuration: duration !== "" ? "true" : "false",
|
||||
duration: duration,
|
||||
@@ -1237,10 +1096,7 @@ const describeLegacyCondition = (
|
||||
}
|
||||
|
||||
// Numeric State Condition
|
||||
if (
|
||||
condition.condition === "numeric_state" &&
|
||||
(condition.entity_id || hideEntities)
|
||||
) {
|
||||
if (condition.condition === "numeric_state") {
|
||||
const entity_ids = condition.entity_id
|
||||
? ensureArray(condition.entity_id)
|
||||
: [];
|
||||
@@ -1257,64 +1113,20 @@ const describeLegacyCondition = (
|
||||
: condition.attribute
|
||||
: undefined;
|
||||
|
||||
if (hideEntities) {
|
||||
const suffix = numericThresholdSuffix(condition);
|
||||
if (!suffix) {
|
||||
return hass.localize(
|
||||
`${conditionsTranslationBaseKey}.numeric_state.label`
|
||||
);
|
||||
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),
|
||||
}
|
||||
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`,
|
||||
{
|
||||
attribute,
|
||||
entity,
|
||||
numberOfEntities: entity_ids.length,
|
||||
above: formatNumericLimitValue(hass, condition.above),
|
||||
below: formatNumericLimitValue(hass, condition.below),
|
||||
}
|
||||
);
|
||||
}
|
||||
if (condition.above !== undefined) {
|
||||
return hass.localize(
|
||||
`${conditionsTranslationBaseKey}.numeric_state.description.above`,
|
||||
{
|
||||
attribute,
|
||||
entity,
|
||||
numberOfEntities: entity_ids.length,
|
||||
above: formatNumericLimitValue(hass, condition.above),
|
||||
}
|
||||
);
|
||||
}
|
||||
if (condition.below !== undefined) {
|
||||
return hass.localize(
|
||||
`${conditionsTranslationBaseKey}.numeric_state.description.below`,
|
||||
{
|
||||
attribute,
|
||||
entity,
|
||||
numberOfEntities: entity_ids.length,
|
||||
below: formatNumericLimitValue(hass, condition.below),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Time condition
|
||||
|
||||
@@ -62,9 +62,11 @@ import type {
|
||||
AutomationClipboard,
|
||||
Condition,
|
||||
} from "../../../../data/automation";
|
||||
import type { ConditionDescriptions } from "../../../../data/condition";
|
||||
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
import {
|
||||
conditionDescriptionsContext,
|
||||
fullEntitiesContext,
|
||||
manifestsContext,
|
||||
} from "../../../../data/context";
|
||||
@@ -90,6 +92,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-action-editor";
|
||||
import type HaAutomationActionEditor from "./ha-automation-action-editor";
|
||||
@@ -205,6 +209,10 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
@consume({ context: manifestsContext, subscribe: true })
|
||||
private _manifests?: DomainManifestLookup;
|
||||
|
||||
@state()
|
||||
@consume({ context: conditionDescriptionsContext, subscribe: true })
|
||||
private _conditionDescriptions?: ConditionDescriptions;
|
||||
|
||||
@state() private _running = false;
|
||||
|
||||
@state() private _runResult?: {
|
||||
@@ -292,13 +300,19 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
? this._extractTargets(this.action as ServiceAction)
|
||||
: type === "device_id" && (this.action as DeviceAction).device_id
|
||||
? { device_id: (this.action as DeviceAction).device_id }
|
||||
: undefined;
|
||||
: type === "condition"
|
||||
? this._extractConditionTarget(this.action as Condition)
|
||||
: undefined;
|
||||
|
||||
const serviceTargetSpec =
|
||||
type === "service" && action
|
||||
? this.hass.services?.[computeDomain(action)]?.[computeObjectId(action)]
|
||||
type === "condition"
|
||||
? this._conditionDescriptions?.[(this.action as Condition).condition]
|
||||
?.target
|
||||
: undefined;
|
||||
: type === "service" && action
|
||||
? this.hass.services?.[computeDomain(action)]?.[
|
||||
computeObjectId(action)
|
||||
]?.target
|
||||
: undefined;
|
||||
|
||||
const noteTooltipText = truncateWithEllipsis(
|
||||
this.action.note?.trim() || "",
|
||||
@@ -777,6 +791,28 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
return {};
|
||||
}
|
||||
|
||||
private _extractConditionTarget(
|
||||
condition: Condition
|
||||
): HassServiceTarget | undefined {
|
||||
if (typeof condition !== "object") {
|
||||
return undefined;
|
||||
}
|
||||
if ("target" in condition && condition.target) {
|
||||
return condition.target;
|
||||
}
|
||||
if (
|
||||
(condition.condition === "state" ||
|
||||
condition.condition === "numeric_state") &&
|
||||
"entity_id" in condition
|
||||
) {
|
||||
return getEntityTarget(condition.entity_id);
|
||||
}
|
||||
if (condition.condition === "device" && "device_id" in condition) {
|
||||
return getDeviceTarget(condition.device_id as string);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _renderTargets = memoizeOne(
|
||||
(
|
||||
target?: HassServiceTarget,
|
||||
|
||||
@@ -226,9 +226,7 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
}
|
||||
<h3 slot="header">
|
||||
${capitalizeFirstLetter(
|
||||
describeCondition(this.condition, this.hass, this._entityReg, {
|
||||
hideEntities: true,
|
||||
})
|
||||
describeCondition(this.condition, this.hass, this._entityReg)
|
||||
)}
|
||||
${
|
||||
target !== undefined || targetRequired
|
||||
|
||||
@@ -68,6 +68,9 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
@property({ type: Boolean })
|
||||
public interactive = false;
|
||||
|
||||
@property({ reflect: true })
|
||||
public size: "s" | "m" = "m";
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
@@ -652,6 +655,23 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
:host([size="s"]) {
|
||||
min-height: 24px;
|
||||
}
|
||||
:host([size="s"]) .target {
|
||||
height: 24px;
|
||||
}
|
||||
/* A default 24px icon would fill the whole small chip. */
|
||||
:host([size="s"]) .target ha-icon,
|
||||
:host([size="s"]) .target ha-svg-icon,
|
||||
:host([size="s"]) .target ha-domain-icon,
|
||||
:host([size="s"]) .target ha-floor-icon {
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
:host([size="s"]) .target ha-floor-icon {
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
button.target {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -251,9 +251,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
}
|
||||
<h3 slot="header">
|
||||
${capitalizeFirstLetter(
|
||||
describeTrigger(this.trigger, this.hass, this._entityReg, {
|
||||
hideEntities: true,
|
||||
})
|
||||
describeTrigger(this.trigger, this.hass, this._entityReg)
|
||||
)}
|
||||
${
|
||||
target !== undefined || targetRequired
|
||||
|
||||
@@ -5373,7 +5373,6 @@
|
||||
"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}",
|
||||
"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": {
|
||||
@@ -5414,9 +5413,6 @@
|
||||
"type_input": "Value of an entity",
|
||||
"description": {
|
||||
"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 }",
|
||||
"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}"
|
||||
@@ -5671,9 +5667,6 @@
|
||||
"value_template": "[%key:ui::panel::config::automation::editor::triggers::type::numeric_state::value_template%]",
|
||||
"description": {
|
||||
"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}",
|
||||
"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}"
|
||||
@@ -5692,8 +5685,6 @@
|
||||
"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 }",
|
||||
"is": "{hasAttribute, select, \n true {{attribute}} \n other {State}\n} is {states}{hasDuration, select, \n true { for {duration}} \n other {}\n}"
|
||||
}
|
||||
},
|
||||
@@ -6023,7 +6014,7 @@
|
||||
},
|
||||
"check_condition": {
|
||||
"description": {
|
||||
"full": "Test {condition}"
|
||||
"full": "Test: {condition}"
|
||||
}
|
||||
},
|
||||
"set_conversation_response": {
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { IntlMessageFormat } from "intl-messageformat";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import en from "../../src/translations/en.json";
|
||||
|
||||
// The state condition summary string. Its verb must agree with how the entity
|
||||
// list is joined: "and" (match "all") takes a plural verb, "or" (match "any")
|
||||
// takes a singular verb in English.
|
||||
const message = (en as any).ui.panel.config.automation.editor.conditions.type
|
||||
.state.description.full;
|
||||
|
||||
const format = (values: Record<string, unknown>) =>
|
||||
new IntlMessageFormat(message, "en").format(values) as string;
|
||||
|
||||
describe("state condition summary grammar", () => {
|
||||
it("uses a singular verb for a single entity", () => {
|
||||
expect(
|
||||
format({
|
||||
hasAttribute: "false",
|
||||
numberOfEntities: 1,
|
||||
matchAny: "false",
|
||||
entities: "Light",
|
||||
numberOfStates: 1,
|
||||
states: "on",
|
||||
hasDuration: "false",
|
||||
})
|
||||
).toBe("If Light is on");
|
||||
});
|
||||
|
||||
it("uses a plural verb for multiple entities matched with all (and)", () => {
|
||||
expect(
|
||||
format({
|
||||
hasAttribute: "false",
|
||||
numberOfEntities: 2,
|
||||
matchAny: "false",
|
||||
entities: "A and B",
|
||||
numberOfStates: 1,
|
||||
states: "on",
|
||||
hasDuration: "false",
|
||||
})
|
||||
).toBe("If A and B are on");
|
||||
});
|
||||
|
||||
it("uses a singular verb for multiple entities matched with any (or)", () => {
|
||||
expect(
|
||||
format({
|
||||
hasAttribute: "false",
|
||||
numberOfEntities: 2,
|
||||
matchAny: "true",
|
||||
entities: "A or B",
|
||||
numberOfStates: 1,
|
||||
states: "on",
|
||||
hasDuration: "false",
|
||||
})
|
||||
).toBe("If A or B is on");
|
||||
});
|
||||
});
|
||||
+4
-22
@@ -58,10 +58,10 @@ const hass = {
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
const describeRowTrigger = (trigger: Trigger) =>
|
||||
describeTrigger(trigger, hass, [], { hideEntities: true });
|
||||
describeTrigger(trigger, hass, []);
|
||||
|
||||
const describeRowCondition = (condition: Condition) =>
|
||||
describeCondition(condition, hass, [], { hideEntities: true });
|
||||
describeCondition(condition, hass, []);
|
||||
|
||||
describe("describing state triggers and conditions", () => {
|
||||
const trigger: Trigger = {
|
||||
@@ -75,16 +75,7 @@ describe("describing state triggers and conditions", () => {
|
||||
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", () => {
|
||||
it("leaves the entities out, they are rendered as targets", () => {
|
||||
expect(describeRowTrigger(trigger)).toBe("State changed to on");
|
||||
expect(describeRowCondition(condition)).toBe("State is on");
|
||||
});
|
||||
@@ -115,16 +106,7 @@ describe("describing numeric state triggers and conditions", () => {
|
||||
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", () => {
|
||||
it("leaves the entities out, they are rendered as targets", () => {
|
||||
expect(describeRowTrigger(trigger)).toBe("Numeric state crossed above 20");
|
||||
expect(describeRowCondition(condition)).toBe("Numeric state is above 20");
|
||||
});
|
||||
Reference in New Issue
Block a user