mirror of
https://github.com/home-assistant/frontend.git
synced 2026-09-02 07:36:19 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e4b6d22bf | ||
|
|
402aeaacd9 |
@@ -12,7 +12,6 @@ import { fullEntitiesContext } from "../../data/context";
|
||||
import type { DeviceAutomation } from "../../data/device/device_automation";
|
||||
import {
|
||||
deviceAutomationsEqual,
|
||||
deviceAutomationsSimilar,
|
||||
sortDeviceAutomations,
|
||||
} from "../../data/device/device_automation";
|
||||
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
|
||||
@@ -180,12 +179,15 @@ export abstract class HaDeviceAutomationPicker<
|
||||
(a, idx) => value === `${a.device_id}_${idx}`
|
||||
);
|
||||
|
||||
const text = automation
|
||||
const described =
|
||||
automation ?? (this.value?.domain ? this.value : undefined);
|
||||
|
||||
const text = described
|
||||
? this._localizeDeviceAutomation(
|
||||
this.hass.localize,
|
||||
this.hass.states,
|
||||
this._entityReg,
|
||||
automation
|
||||
described
|
||||
)
|
||||
: value === NO_AUTOMATION_KEY
|
||||
? this.NO_AUTOMATION_TEXT
|
||||
@@ -195,29 +197,24 @@ export abstract class HaDeviceAutomationPicker<
|
||||
};
|
||||
|
||||
private async _updateDeviceInfo() {
|
||||
// Asking a removed device for its automations fails rather than returning
|
||||
// an empty list.
|
||||
this._automations = this.deviceId
|
||||
? (
|
||||
await this._fetchDeviceAutomations(this.hass.callWS, this.deviceId)
|
||||
await this._fetchDeviceAutomations(
|
||||
this.hass.callWS,
|
||||
this.deviceId
|
||||
).catch(() => [] as T[])
|
||||
).sort(sortDeviceAutomations)
|
||||
: // No device, clear the list of automations
|
||||
[];
|
||||
|
||||
// If there is no value, or if we have changed the device ID, reset the
|
||||
// value. When the device changed (for example after replacing a removed
|
||||
// device), try to keep the same automation type/subtype on the new device
|
||||
// before falling back to the first available automation.
|
||||
// If there is no value, or if we have changed the device ID, reset the value.
|
||||
if (!this.value || this.value.device_id !== this.deviceId) {
|
||||
const equivalent =
|
||||
this.value && this.deviceId
|
||||
? this._automations.find((automation) =>
|
||||
deviceAutomationsSimilar(automation, this.value!)
|
||||
)
|
||||
: undefined;
|
||||
this._setValue(
|
||||
equivalent ||
|
||||
(this._automations.length
|
||||
? this._automations[0]
|
||||
: this._createNoAutomation(this.deviceId))
|
||||
this._automations.length
|
||||
? this._automations[0]
|
||||
: this._createNoAutomation(this.deviceId)
|
||||
);
|
||||
}
|
||||
this._renderEmpty = true;
|
||||
|
||||
@@ -105,6 +105,14 @@ export class HaDevicePicker extends LitElement {
|
||||
@property({ attribute: "hide-clear-icon", type: Boolean })
|
||||
public hideClearIcon = false;
|
||||
|
||||
/**
|
||||
* The split devices that can actually replace the current value, when the
|
||||
* caller knows better than this picker. Narrows the replacement candidates,
|
||||
* so the user is only asked to choose when there is a real choice.
|
||||
*/
|
||||
@property({ attribute: false })
|
||||
public replacementDeviceIds?: string[];
|
||||
|
||||
@query("ha-generic-picker") private _picker?: HaGenericPicker;
|
||||
|
||||
@state() private _configEntryLookup: Record<string, ConfigEntry> = {};
|
||||
@@ -188,7 +196,8 @@ export class HaDevicePicker extends LitElement {
|
||||
value: string | undefined,
|
||||
_devices: HomeAssistant["devices"],
|
||||
compositeSplits: DeviceCompositeSplits | undefined,
|
||||
items: (DevicePickerItem | string)[]
|
||||
items: (DevicePickerItem | string)[],
|
||||
replacementDeviceIds: string[] | undefined
|
||||
) => {
|
||||
if (!value || !compositeSplits || this.hass.devices[value]) {
|
||||
return undefined;
|
||||
@@ -204,7 +213,11 @@ export class HaDevicePicker extends LitElement {
|
||||
.filter((item): item is DevicePickerItem => typeof item !== "string")
|
||||
.map((item) => item.id)
|
||||
);
|
||||
const candidates = split.split_ids.filter((id) => selectableIds.has(id));
|
||||
const candidates = split.split_ids.filter(
|
||||
(id) =>
|
||||
selectableIds.has(id) &&
|
||||
(!replacementDeviceIds || replacementDeviceIds.includes(id))
|
||||
);
|
||||
return { candidates, primaryId: split.primary_id };
|
||||
}
|
||||
);
|
||||
@@ -400,7 +413,8 @@ export class HaDevicePicker extends LitElement {
|
||||
this.value,
|
||||
this.hass.devices,
|
||||
this._compositeSplits,
|
||||
this._getItems()
|
||||
this._getItems(),
|
||||
this.replacementDeviceIds
|
||||
)
|
||||
: undefined;
|
||||
|
||||
@@ -507,7 +521,8 @@ export class HaDevicePicker extends LitElement {
|
||||
this.value,
|
||||
this.hass.devices,
|
||||
this._compositeSplits,
|
||||
this._getItems()
|
||||
this._getItems(),
|
||||
this.replacementDeviceIds
|
||||
);
|
||||
if (!replacement?.candidates.length) {
|
||||
return;
|
||||
|
||||
@@ -182,26 +182,69 @@ export const deviceAutomationEditorMode = (
|
||||
: "unknown-device";
|
||||
};
|
||||
|
||||
// Like deviceAutomationsEqual, but ignores device_id and entity_id so an
|
||||
// automation can be matched to the equivalent one on a different device (for
|
||||
// example when a referenced device was replaced by a split device).
|
||||
export const deviceAutomationsSimilar = (
|
||||
const deviceAutomationsSameType = (a: DeviceAutomation, b: DeviceAutomation) =>
|
||||
deviceAutomationIdentifiers
|
||||
.filter((property) => property !== "device_id" && property !== "entity_id")
|
||||
.every((property) => Object.is(a[property], b[property]));
|
||||
|
||||
// An entity can be referenced by its registry id or by its entity id, and the
|
||||
// two sides do not have to agree.
|
||||
const deviceAutomationsSameEntity = (
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
a: DeviceAutomation,
|
||||
b: DeviceAutomation
|
||||
) => {
|
||||
if (typeof a !== typeof b) {
|
||||
if (!a.entity_id && !b.entity_id) {
|
||||
return true;
|
||||
}
|
||||
if (!a.entity_id || !b.entity_id) {
|
||||
return false;
|
||||
}
|
||||
return deviceAutomationIdentifiers
|
||||
.filter((property) => property !== "device_id" && property !== "entity_id")
|
||||
.every((property) => {
|
||||
const inA = property in a;
|
||||
const inB = property in b;
|
||||
if (!inA && !inB) {
|
||||
return true;
|
||||
}
|
||||
return Object.is(a[property], b[property]);
|
||||
});
|
||||
return (
|
||||
a.entity_id === b.entity_id ||
|
||||
compareEntityIdWithEntityRegId(entityRegistry, a.entity_id, b.entity_id)
|
||||
);
|
||||
};
|
||||
|
||||
// A device exposes the same automation type once per entity, so the same type
|
||||
// on another entity is a different automation, not an equivalent one.
|
||||
export const findEquivalentDeviceAutomation = <T extends DeviceAutomation>(
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
automations: T[],
|
||||
automation: DeviceAutomation
|
||||
): T | undefined =>
|
||||
automations.find(
|
||||
(candidate) =>
|
||||
deviceAutomationsSameType(candidate, automation) &&
|
||||
deviceAutomationsSameEntity(entityRegistry, candidate, automation)
|
||||
);
|
||||
|
||||
// Among the split devices that replaced a removed device, the ones that offer
|
||||
// the given automation. Nothing in the registry says which of them took it over,
|
||||
// so each candidate has to be asked.
|
||||
export const fetchReplacementDevices = async <T extends DeviceAutomation>(
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[],
|
||||
automation: DeviceAutomation,
|
||||
compositeSplits: DeviceCompositeSplits,
|
||||
fetchDeviceAutomations: (callWS: CallWS, deviceId: string) => Promise<T[]>
|
||||
): Promise<string[]> => {
|
||||
const candidates =
|
||||
compositeSplits[automation.device_id]?.split_ids.filter(
|
||||
(id) => id in hass.devices
|
||||
) ?? [];
|
||||
const automationsPerCandidate = await Promise.all(
|
||||
candidates.map((id) =>
|
||||
fetchDeviceAutomations(hass.callWS, id).catch(() => [] as T[])
|
||||
)
|
||||
);
|
||||
return candidates.filter((_id, index) =>
|
||||
findEquivalentDeviceAutomation(
|
||||
entityRegistry,
|
||||
automationsPerCandidate[index],
|
||||
automation
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const compareEntityIdWithEntityRegId = (
|
||||
|
||||
@@ -14,6 +14,8 @@ import type {
|
||||
} from "../../../../../data/device/device_automation";
|
||||
import {
|
||||
deviceAutomationEditorMode,
|
||||
fetchReplacementDevices,
|
||||
fetchDeviceActions,
|
||||
deviceAutomationsEqual,
|
||||
fetchDeviceActionCapabilities,
|
||||
localizeExtraFieldsComputeHelperCallback,
|
||||
@@ -40,6 +42,8 @@ export class HaDeviceAction extends LitElement {
|
||||
|
||||
@state() private _compositeSplits?: DeviceCompositeSplits;
|
||||
|
||||
@state() private _replacementDeviceIds?: string[];
|
||||
|
||||
private _loadingCompositeSplits = false;
|
||||
|
||||
@state()
|
||||
@@ -101,7 +105,17 @@ export class HaDeviceAction extends LitElement {
|
||||
}
|
||||
this._loadingCompositeSplits = true;
|
||||
try {
|
||||
this._compositeSplits = await fetchDeviceCompositeSplits(this.hass);
|
||||
// Resolve the candidates before exposing the split map, so the picker
|
||||
// never offers one that cannot host the automation.
|
||||
const compositeSplits = await fetchDeviceCompositeSplits(this.hass);
|
||||
this._replacementDeviceIds = await fetchReplacementDevices(
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
this.action,
|
||||
compositeSplits,
|
||||
fetchDeviceActions
|
||||
);
|
||||
this._compositeSplits = compositeSplits;
|
||||
} catch (_err) {
|
||||
this._compositeSplits = {};
|
||||
} finally {
|
||||
@@ -115,6 +129,7 @@ export class HaDeviceAction extends LitElement {
|
||||
return html`
|
||||
<ha-device-picker
|
||||
.value=${deviceId}
|
||||
.replacementDeviceIds=${this._replacementDeviceIds}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._devicePicked}
|
||||
.hass=${this.hass}
|
||||
@@ -185,6 +200,15 @@ export class HaDeviceAction extends LitElement {
|
||||
|
||||
private _devicePicked(ev) {
|
||||
ev.stopPropagation();
|
||||
// The automation exists as is on the replacement, so only the reference
|
||||
// changes and the rest of the configuration is left untouched.
|
||||
if (this._replacementDeviceIds?.includes(ev.target.value)) {
|
||||
this._deviceId = undefined;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.action, device_id: ev.target.value },
|
||||
});
|
||||
return;
|
||||
}
|
||||
this._deviceId = ev.target.value;
|
||||
if (this._deviceId === undefined) {
|
||||
fireEvent(this, "value-changed", {
|
||||
|
||||
@@ -14,6 +14,8 @@ import type {
|
||||
} from "../../../../../data/device/device_automation";
|
||||
import {
|
||||
deviceAutomationEditorMode,
|
||||
fetchReplacementDevices,
|
||||
fetchDeviceConditions,
|
||||
deviceAutomationsEqual,
|
||||
fetchDeviceConditionCapabilities,
|
||||
localizeExtraFieldsComputeHelperCallback,
|
||||
@@ -40,6 +42,8 @@ export class HaDeviceCondition extends LitElement {
|
||||
|
||||
@state() private _compositeSplits?: DeviceCompositeSplits;
|
||||
|
||||
@state() private _replacementDeviceIds?: string[];
|
||||
|
||||
private _loadingCompositeSplits = false;
|
||||
|
||||
@state()
|
||||
@@ -102,7 +106,17 @@ export class HaDeviceCondition extends LitElement {
|
||||
}
|
||||
this._loadingCompositeSplits = true;
|
||||
try {
|
||||
this._compositeSplits = await fetchDeviceCompositeSplits(this.hass);
|
||||
// Resolve the candidates before exposing the split map, so the picker
|
||||
// never offers one that cannot host the automation.
|
||||
const compositeSplits = await fetchDeviceCompositeSplits(this.hass);
|
||||
this._replacementDeviceIds = await fetchReplacementDevices(
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
this.condition,
|
||||
compositeSplits,
|
||||
fetchDeviceConditions
|
||||
);
|
||||
this._compositeSplits = compositeSplits;
|
||||
} catch (_err) {
|
||||
this._compositeSplits = {};
|
||||
} finally {
|
||||
@@ -116,6 +130,7 @@ export class HaDeviceCondition extends LitElement {
|
||||
return html`
|
||||
<ha-device-picker
|
||||
.value=${deviceId}
|
||||
.replacementDeviceIds=${this._replacementDeviceIds}
|
||||
@value-changed=${this._devicePicked}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
@@ -187,6 +202,15 @@ export class HaDeviceCondition extends LitElement {
|
||||
|
||||
private _devicePicked(ev) {
|
||||
ev.stopPropagation();
|
||||
// The automation exists as is on the replacement, so only the reference
|
||||
// changes and the rest of the configuration is left untouched.
|
||||
if (this._replacementDeviceIds?.includes(ev.target.value)) {
|
||||
this._deviceId = undefined;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.condition, device_id: ev.target.value },
|
||||
});
|
||||
return;
|
||||
}
|
||||
this._deviceId = ev.target.value;
|
||||
if (this._deviceId === undefined) {
|
||||
fireEvent(this, "value-changed", {
|
||||
|
||||
@@ -479,7 +479,12 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
warning = true;
|
||||
badgeTargetId = undefined;
|
||||
badgeTargetType = undefined;
|
||||
if (targetType === "device" && this._compositeSplits?.[targetId]) {
|
||||
if (
|
||||
targetType === "device" &&
|
||||
this._compositeSplits?.[targetId]?.split_ids.some(
|
||||
(id) => id in this._registries.devices
|
||||
)
|
||||
) {
|
||||
// The device was replaced by one or more split devices; make clear
|
||||
// this reference needs to be updated, distinct from "unknown device".
|
||||
icon = mdiSwapHorizontal;
|
||||
|
||||
@@ -16,6 +16,8 @@ import type {
|
||||
} from "../../../../../data/device/device_automation";
|
||||
import {
|
||||
deviceAutomationEditorMode,
|
||||
fetchReplacementDevices,
|
||||
fetchDeviceTriggers,
|
||||
deviceAutomationsEqual,
|
||||
fetchDeviceTriggerCapabilities,
|
||||
localizeExtraFieldsComputeHelperCallback,
|
||||
@@ -42,6 +44,8 @@ export class HaDeviceTrigger extends LitElement {
|
||||
|
||||
@state() private _compositeSplits?: DeviceCompositeSplits;
|
||||
|
||||
@state() private _replacementDeviceIds?: string[];
|
||||
|
||||
private _loadingCompositeSplits = false;
|
||||
|
||||
@state()
|
||||
@@ -106,7 +110,17 @@ export class HaDeviceTrigger extends LitElement {
|
||||
}
|
||||
this._loadingCompositeSplits = true;
|
||||
try {
|
||||
this._compositeSplits = await fetchDeviceCompositeSplits(this.hass);
|
||||
// Resolve the candidates before exposing the split map, so the picker
|
||||
// never offers one that cannot host the automation.
|
||||
const compositeSplits = await fetchDeviceCompositeSplits(this.hass);
|
||||
this._replacementDeviceIds = await fetchReplacementDevices(
|
||||
this.hass,
|
||||
this._entityReg,
|
||||
this.trigger,
|
||||
compositeSplits,
|
||||
fetchDeviceTriggers
|
||||
);
|
||||
this._compositeSplits = compositeSplits;
|
||||
} catch (_err) {
|
||||
this._compositeSplits = {};
|
||||
} finally {
|
||||
@@ -120,6 +134,7 @@ export class HaDeviceTrigger extends LitElement {
|
||||
return html`
|
||||
<ha-device-picker
|
||||
.value=${deviceId}
|
||||
.replacementDeviceIds=${this._replacementDeviceIds}
|
||||
@value-changed=${this._devicePicked}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
@@ -208,6 +223,15 @@ export class HaDeviceTrigger extends LitElement {
|
||||
|
||||
private _devicePicked(ev) {
|
||||
ev.stopPropagation();
|
||||
// The automation exists as is on the replacement, so only the reference
|
||||
// changes and the rest of the configuration is left untouched.
|
||||
if (this._replacementDeviceIds?.includes(ev.target.value)) {
|
||||
this._deviceId = undefined;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: { ...this.trigger, device_id: ev.target.value },
|
||||
});
|
||||
return;
|
||||
}
|
||||
this._deviceId = ev.target.value;
|
||||
if (this._deviceId === undefined) {
|
||||
fireEvent(this, "value-changed", {
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { DeviceTrigger } from "../../src/data/device/device_automation";
|
||||
import {
|
||||
fetchReplacementDevices,
|
||||
findEquivalentDeviceAutomation,
|
||||
} from "../../src/data/device/device_automation";
|
||||
import type { DeviceCompositeSplits } from "../../src/data/device/device_registry";
|
||||
import type { EntityRegistryEntry } from "../../src/data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../src/types";
|
||||
|
||||
const entityRegistry = [
|
||||
{ id: "regid1", entity_id: "binary_sensor.one" },
|
||||
{ id: "regid2", entity_id: "binary_sensor.two" },
|
||||
] as EntityRegistryEntry[];
|
||||
|
||||
const trigger = (partial: Partial<DeviceTrigger>): DeviceTrigger =>
|
||||
({
|
||||
trigger: "device",
|
||||
domain: "binary_sensor",
|
||||
device_id: "device1",
|
||||
...partial,
|
||||
}) as DeviceTrigger;
|
||||
|
||||
describe("findEquivalentDeviceAutomation", () => {
|
||||
it("picks the automation on the same entity among several of the same type", () => {
|
||||
const automations = [
|
||||
trigger({ device_id: "device2", type: "turned_on", entity_id: "regid1" }),
|
||||
trigger({ device_id: "device2", type: "turned_on", entity_id: "regid2" }),
|
||||
];
|
||||
|
||||
expect(
|
||||
findEquivalentDeviceAutomation(
|
||||
entityRegistry,
|
||||
automations,
|
||||
trigger({ type: "turned_on", entity_id: "regid2" })
|
||||
)
|
||||
).toBe(automations[1]);
|
||||
});
|
||||
|
||||
it("matches an entity referenced by entity id against one referenced by registry id", () => {
|
||||
const automations = [
|
||||
trigger({ device_id: "device2", type: "turned_on", entity_id: "regid1" }),
|
||||
trigger({ device_id: "device2", type: "turned_on", entity_id: "regid2" }),
|
||||
];
|
||||
|
||||
expect(
|
||||
findEquivalentDeviceAutomation(
|
||||
entityRegistry,
|
||||
automations,
|
||||
trigger({ type: "turned_on", entity_id: "binary_sensor.two" })
|
||||
)
|
||||
).toBe(automations[1]);
|
||||
});
|
||||
|
||||
it("returns undefined when the same type is only offered for another entity", () => {
|
||||
const automations = [
|
||||
trigger({ device_id: "device2", type: "turned_on", entity_id: "regid1" }),
|
||||
trigger({
|
||||
device_id: "device2",
|
||||
type: "turned_off",
|
||||
entity_id: "regid1",
|
||||
}),
|
||||
];
|
||||
|
||||
expect(
|
||||
findEquivalentDeviceAutomation(
|
||||
entityRegistry,
|
||||
automations,
|
||||
trigger({ type: "turned_on", entity_id: "regid2" })
|
||||
)
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("matches entity-less automations on their subtype", () => {
|
||||
const automations = [
|
||||
trigger({
|
||||
device_id: "device2",
|
||||
domain: "zha",
|
||||
type: "remote_button_short_press",
|
||||
subtype: "button_1",
|
||||
}),
|
||||
trigger({
|
||||
device_id: "device2",
|
||||
domain: "zha",
|
||||
type: "remote_button_short_press",
|
||||
subtype: "button_2",
|
||||
}),
|
||||
];
|
||||
|
||||
expect(
|
||||
findEquivalentDeviceAutomation(
|
||||
entityRegistry,
|
||||
automations,
|
||||
trigger({
|
||||
domain: "zha",
|
||||
type: "remote_button_short_press",
|
||||
subtype: "button_2",
|
||||
})
|
||||
)
|
||||
).toBe(automations[1]);
|
||||
});
|
||||
|
||||
it("returns undefined when the device offers no automation of that type", () => {
|
||||
const automations = [
|
||||
trigger({
|
||||
device_id: "device2",
|
||||
type: "turned_off",
|
||||
entity_id: "regid1",
|
||||
}),
|
||||
];
|
||||
|
||||
expect(
|
||||
findEquivalentDeviceAutomation(
|
||||
entityRegistry,
|
||||
automations,
|
||||
trigger({ type: "turned_on", entity_id: "regid1" })
|
||||
)
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetchReplacementDevices", () => {
|
||||
const hass = {
|
||||
callWS: () => Promise.resolve([]),
|
||||
devices: { device2: {}, device3: {} },
|
||||
} as unknown as HomeAssistant;
|
||||
|
||||
const compositeSplits = {
|
||||
removed: { split_ids: ["device2", "device3"], primary_id: "device2" },
|
||||
} as unknown as DeviceCompositeSplits;
|
||||
|
||||
const value = trigger({
|
||||
device_id: "removed",
|
||||
type: "turned_on",
|
||||
entity_id: "regid1",
|
||||
});
|
||||
|
||||
it("keeps only the devices that offer the automation", async () => {
|
||||
const offers = {
|
||||
device2: [
|
||||
trigger({
|
||||
device_id: "device2",
|
||||
type: "turned_on",
|
||||
entity_id: "regid1",
|
||||
}),
|
||||
],
|
||||
device3: [
|
||||
trigger({
|
||||
device_id: "device3",
|
||||
type: "turned_on",
|
||||
entity_id: "regid2",
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
expect(
|
||||
await fetchReplacementDevices(
|
||||
hass,
|
||||
entityRegistry,
|
||||
value,
|
||||
compositeSplits,
|
||||
(_callWS, deviceId) => Promise.resolve(offers[deviceId])
|
||||
)
|
||||
).toEqual(["device2"]);
|
||||
});
|
||||
|
||||
it("drops a device whose automations cannot be listed", async () => {
|
||||
expect(
|
||||
await fetchReplacementDevices(
|
||||
hass,
|
||||
entityRegistry,
|
||||
value,
|
||||
compositeSplits,
|
||||
(_callWS, deviceId) =>
|
||||
deviceId === "device2"
|
||||
? Promise.reject(new Error("unknown device"))
|
||||
: Promise.resolve([
|
||||
trigger({
|
||||
device_id: "device3",
|
||||
type: "turned_on",
|
||||
entity_id: "regid1",
|
||||
}),
|
||||
])
|
||||
)
|
||||
).toEqual(["device3"]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user