@camstack/addon-provider-homeassistant 1.2.636 → 1.2.638
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/addon.js +1 -1
- package/dist/addon.mjs +1 -1
- package/dist/{dist-BS9DCd76.js → dist-C_XoeULr.js} +1388 -721
- package/dist/{dist-BusXPPvn.mjs → dist-Cvz76AD1.mjs} +1383 -734
- package/dist/ha-export/ha-export.addon.js +300 -3
- package/dist/ha-export/ha-export.addon.mjs +300 -3
- package/package.json +3 -2
|
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
enumerable: true
|
|
25
25
|
}) : target, mod));
|
|
26
26
|
//#endregion
|
|
27
|
-
const require_dist = require("../dist-
|
|
27
|
+
const require_dist = require("../dist-C_XoeULr.js");
|
|
28
28
|
let node_crypto = require("node:crypto");
|
|
29
29
|
require("node:zlib");
|
|
30
30
|
let node_fs = require("node:fs");
|
|
@@ -585,6 +585,7 @@ var KNOWN_PLATFORMS = new Set([
|
|
|
585
585
|
"camera",
|
|
586
586
|
"alarm_control_panel",
|
|
587
587
|
"number",
|
|
588
|
+
"notify",
|
|
588
589
|
"cover",
|
|
589
590
|
"climate",
|
|
590
591
|
"lock",
|
|
@@ -3488,6 +3489,10 @@ function controlEntitySpecs(slice, quantity) {
|
|
|
3488
3489
|
}, kindExtra];
|
|
3489
3490
|
}
|
|
3490
3491
|
var CAPS_NOT_EXPORTED = [
|
|
3492
|
+
{
|
|
3493
|
+
cap: "pet-fountain",
|
|
3494
|
+
reason: "A fountain is BUILT here from the loose entities Home Assistant already holds (a pump switch, two number boxes, a button), so exporting it back would publish a second copy of entities that already exist there. A dedicated mapping can follow when an ecosystem asks for the built device."
|
|
3495
|
+
},
|
|
3491
3496
|
{
|
|
3492
3497
|
cap: "ir-transmitter",
|
|
3493
3498
|
reason: "A pulse-train emitter with no state: it is driven by the remote that owns a command table, and a raw send has no meaningful single entity."
|
|
@@ -5687,7 +5692,8 @@ function renderCapValue(mapping, raw) {
|
|
|
5687
5692
|
case "alarm_control_panel":
|
|
5688
5693
|
case "image":
|
|
5689
5694
|
case "camera": return typeof raw === "string" ? raw : null;
|
|
5690
|
-
case "button":
|
|
5695
|
+
case "button":
|
|
5696
|
+
case "notify": return null;
|
|
5691
5697
|
case "cover":
|
|
5692
5698
|
case "climate":
|
|
5693
5699
|
case "lock":
|
|
@@ -6644,7 +6650,8 @@ function projectSynthetic(input, nowMs) {
|
|
|
6644
6650
|
}, {
|
|
6645
6651
|
topic: stateTopic(srv, `${slug}_cpu`),
|
|
6646
6652
|
value: String(Math.round(node.cpuPercent))
|
|
6647
|
-
}
|
|
6653
|
+
});
|
|
6654
|
+
if (node.memoryPercent !== null) values.push({
|
|
6648
6655
|
topic: stateTopic(srv, `${slug}_memory`),
|
|
6649
6656
|
value: String(Math.round(node.memoryPercent))
|
|
6650
6657
|
});
|
|
@@ -6674,6 +6681,225 @@ function projectSynthetic(input, nowMs) {
|
|
|
6674
6681
|
return values;
|
|
6675
6682
|
}
|
|
6676
6683
|
//#endregion
|
|
6684
|
+
//#region src/ha-export/exported-entities.ts
|
|
6685
|
+
/**
|
|
6686
|
+
* Actions and notification targets exported to Home Assistant (D968).
|
|
6687
|
+
*
|
|
6688
|
+
* An action is a `button`: pressing it runs the action through `actions.run`.
|
|
6689
|
+
* A notification target is a `notify` entity: a message sent to it goes out
|
|
6690
|
+
* through `notification-output.send` to that target. Both live on ONE
|
|
6691
|
+
* synthetic device — they are list entries, never camstack devices (D954), so
|
|
6692
|
+
* nothing here touches the device registry or the membership store.
|
|
6693
|
+
*
|
|
6694
|
+
* The selection is this addon's own setting. The class holds it, validates an
|
|
6695
|
+
* entry against the live lists, prunes what the operator deleted (a failed
|
|
6696
|
+
* read changes nothing, D49), and answers the commands Home Assistant sends
|
|
6697
|
+
* back. The addon supplies `persist` and `reconcile`, so the lifecycle is
|
|
6698
|
+
* testable without an Home Assistant.
|
|
6699
|
+
*
|
|
6700
|
+
* `notify` is a platform the custom component must BUILD (see
|
|
6701
|
+
* `component-support.ts`): against a component that does not, the targets are
|
|
6702
|
+
* withheld from the plan and the caller logs which ones, by name — an entity
|
|
6703
|
+
* announced to a component that drops it would read as "exported nothing".
|
|
6704
|
+
*/
|
|
6705
|
+
/** The stable id of the synthetic device that carries them. A WIRE FORMAT. */
|
|
6706
|
+
var EXPORTED_ENTITIES_STABLE_ID = "synthetic-exported-entities";
|
|
6707
|
+
var EXPORTER = "homeassistant-export";
|
|
6708
|
+
/** `action_<slug>` — derived from the action ID, so a rename keeps its entity. */
|
|
6709
|
+
function actionEntity(actionId) {
|
|
6710
|
+
return `action_${toSlug(actionId)}`;
|
|
6711
|
+
}
|
|
6712
|
+
/** `notify_<slug>` — derived from the target ID. */
|
|
6713
|
+
function targetEntity(targetId) {
|
|
6714
|
+
return `notify_${toSlug(targetId)}`;
|
|
6715
|
+
}
|
|
6716
|
+
function errMsg$1(err) {
|
|
6717
|
+
return err instanceof Error ? err.message : String(err);
|
|
6718
|
+
}
|
|
6719
|
+
var ExportedEntities = class {
|
|
6720
|
+
ports;
|
|
6721
|
+
state;
|
|
6722
|
+
constructor(ports, initial) {
|
|
6723
|
+
this.ports = ports;
|
|
6724
|
+
this.state = initial;
|
|
6725
|
+
}
|
|
6726
|
+
/** The cap's `listExposedEntities` rows for actions and notifiers. */
|
|
6727
|
+
list() {
|
|
6728
|
+
return [...this.state.actions.map((e) => ({
|
|
6729
|
+
entity: {
|
|
6730
|
+
kind: "action",
|
|
6731
|
+
actionId: e.actionId
|
|
6732
|
+
},
|
|
6733
|
+
exposedAs: e.displayName
|
|
6734
|
+
})), ...this.state.targets.map((e) => ({
|
|
6735
|
+
entity: {
|
|
6736
|
+
kind: "notifier",
|
|
6737
|
+
targetId: e.targetId
|
|
6738
|
+
},
|
|
6739
|
+
exposedAs: e.displayName
|
|
6740
|
+
}))];
|
|
6741
|
+
}
|
|
6742
|
+
async expose(entity) {
|
|
6743
|
+
if (entity.kind === "device") throw new Error("homeassistant-export: a device is not an action or a notification target");
|
|
6744
|
+
if (entity.kind === "action") {
|
|
6745
|
+
const action = (await this.ports.api.actions.list.query({})).find((a) => a.id === entity.actionId);
|
|
6746
|
+
if (action === void 0) throw new Error(`homeassistant-export: action '${entity.actionId}' does not exist`);
|
|
6747
|
+
const previous = this.state.actions.find((e) => e.actionId === entity.actionId);
|
|
6748
|
+
const entry = {
|
|
6749
|
+
actionId: entity.actionId,
|
|
6750
|
+
displayName: action.name,
|
|
6751
|
+
addedAt: previous?.addedAt ?? Date.now()
|
|
6752
|
+
};
|
|
6753
|
+
await this.save({
|
|
6754
|
+
...this.state,
|
|
6755
|
+
actions: [...this.state.actions.filter((e) => e.actionId !== entity.actionId), entry]
|
|
6756
|
+
});
|
|
6757
|
+
this.ports.logger.info("homeassistant-export: exposed action", {
|
|
6758
|
+
tags: { actionId: entity.actionId },
|
|
6759
|
+
meta: { displayName: action.name }
|
|
6760
|
+
});
|
|
6761
|
+
} else {
|
|
6762
|
+
const target = (await this.ports.api.notificationOutput.listTargets.query({})).find((t) => t.id === entity.targetId);
|
|
6763
|
+
if (target === void 0) throw new Error(`homeassistant-export: notification target '${entity.targetId}' does not exist`);
|
|
6764
|
+
const previous = this.state.targets.find((e) => e.targetId === entity.targetId);
|
|
6765
|
+
const entry = {
|
|
6766
|
+
targetId: entity.targetId,
|
|
6767
|
+
displayName: target.name,
|
|
6768
|
+
addedAt: previous?.addedAt ?? Date.now()
|
|
6769
|
+
};
|
|
6770
|
+
await this.save({
|
|
6771
|
+
...this.state,
|
|
6772
|
+
targets: [...this.state.targets.filter((e) => e.targetId !== entity.targetId), entry]
|
|
6773
|
+
});
|
|
6774
|
+
this.ports.logger.info("homeassistant-export: exposed notification target", {
|
|
6775
|
+
tags: { targetId: entity.targetId },
|
|
6776
|
+
meta: { displayName: target.name }
|
|
6777
|
+
});
|
|
6778
|
+
}
|
|
6779
|
+
await this.ports.reconcile();
|
|
6780
|
+
}
|
|
6781
|
+
async unexpose(entity) {
|
|
6782
|
+
if (entity.kind === "device") throw new Error("homeassistant-export: a device is not an action or a notification target");
|
|
6783
|
+
const next = entity.kind === "action" ? {
|
|
6784
|
+
...this.state,
|
|
6785
|
+
actions: this.state.actions.filter((e) => e.actionId !== entity.actionId)
|
|
6786
|
+
} : {
|
|
6787
|
+
...this.state,
|
|
6788
|
+
targets: this.state.targets.filter((e) => e.targetId !== entity.targetId)
|
|
6789
|
+
};
|
|
6790
|
+
await this.save(next);
|
|
6791
|
+
await this.ports.reconcile();
|
|
6792
|
+
}
|
|
6793
|
+
/**
|
|
6794
|
+
* Drop what the operator deleted, logging each drop. A list that could not
|
|
6795
|
+
* be read changes nothing: a failed read must never destroy the selection.
|
|
6796
|
+
*/
|
|
6797
|
+
async pruneDeleted() {
|
|
6798
|
+
if (this.state.actions.length === 0 && this.state.targets.length === 0) return;
|
|
6799
|
+
let actionIds;
|
|
6800
|
+
let targetIds;
|
|
6801
|
+
try {
|
|
6802
|
+
const [actions, targets] = await Promise.all([this.ports.api.actions.list.query({}), this.ports.api.notificationOutput.listTargets.query({})]);
|
|
6803
|
+
actionIds = new Set(actions.map((a) => a.id));
|
|
6804
|
+
targetIds = new Set(targets.map((t) => t.id));
|
|
6805
|
+
} catch (err) {
|
|
6806
|
+
this.ports.logger.warn("homeassistant-export: exported entities not reconciled — a list was unanswered", { meta: { error: errMsg$1(err) } });
|
|
6807
|
+
return;
|
|
6808
|
+
}
|
|
6809
|
+
const { dropped } = require_dist.pruneDeletedEntities([...this.state.actions.map((e) => ({
|
|
6810
|
+
kind: "action",
|
|
6811
|
+
actionId: e.actionId
|
|
6812
|
+
})), ...this.state.targets.map((e) => ({
|
|
6813
|
+
kind: "notifier",
|
|
6814
|
+
targetId: e.targetId
|
|
6815
|
+
}))], {
|
|
6816
|
+
actionIds,
|
|
6817
|
+
targetIds
|
|
6818
|
+
}, this.ports.logger, EXPORTER);
|
|
6819
|
+
if (dropped.length === 0) return;
|
|
6820
|
+
const goneActions = new Set(dropped.flatMap((r) => r.kind === "action" ? [r.actionId] : []));
|
|
6821
|
+
const goneTargets = new Set(dropped.flatMap((r) => r.kind === "notifier" ? [r.targetId] : []));
|
|
6822
|
+
await this.save({
|
|
6823
|
+
actions: this.state.actions.filter((e) => !goneActions.has(e.actionId)),
|
|
6824
|
+
targets: this.state.targets.filter((e) => !goneTargets.has(e.targetId))
|
|
6825
|
+
});
|
|
6826
|
+
}
|
|
6827
|
+
/** The synthetic device the selection announces. Null when nothing is exported. */
|
|
6828
|
+
plan(support) {
|
|
6829
|
+
const deviceKey = syntheticDeviceKeyFor(EXPORTED_ENTITIES_STABLE_ID);
|
|
6830
|
+
const cmps = {};
|
|
6831
|
+
for (const e of this.state.actions) {
|
|
6832
|
+
const entity = actionEntity(e.actionId);
|
|
6833
|
+
cmps[toComponentKey("button", entity)] = {
|
|
6834
|
+
platform: "button",
|
|
6835
|
+
unique_id: `${EXPORTED_ENTITIES_STABLE_ID}_${entity}`,
|
|
6836
|
+
name: e.displayName,
|
|
6837
|
+
command_topic: commandTopic(deviceKey, entity),
|
|
6838
|
+
payload_press: "PRESS",
|
|
6839
|
+
icon: "mdi:play-circle-outline"
|
|
6840
|
+
};
|
|
6841
|
+
}
|
|
6842
|
+
const withheld = [];
|
|
6843
|
+
for (const e of this.state.targets) {
|
|
6844
|
+
if (!support.has("notify")) {
|
|
6845
|
+
withheld.push(e.targetId);
|
|
6846
|
+
continue;
|
|
6847
|
+
}
|
|
6848
|
+
const entity = targetEntity(e.targetId);
|
|
6849
|
+
cmps[toComponentKey("notify", entity)] = {
|
|
6850
|
+
platform: "notify",
|
|
6851
|
+
unique_id: `${EXPORTED_ENTITIES_STABLE_ID}_${entity}`,
|
|
6852
|
+
name: e.displayName,
|
|
6853
|
+
command_topic: commandTopic(deviceKey, entity),
|
|
6854
|
+
icon: "mdi:message-text-outline"
|
|
6855
|
+
};
|
|
6856
|
+
}
|
|
6857
|
+
if (Object.keys(cmps).length === 0) return {
|
|
6858
|
+
plan: null,
|
|
6859
|
+
withheld
|
|
6860
|
+
};
|
|
6861
|
+
return {
|
|
6862
|
+
plan: {
|
|
6863
|
+
deviceKey,
|
|
6864
|
+
deviceId: null,
|
|
6865
|
+
dev: {
|
|
6866
|
+
ids: [deviceKey],
|
|
6867
|
+
name: "CamStack Actions and Notifications",
|
|
6868
|
+
mf: "CamStack",
|
|
6869
|
+
mdl: "Synthetic"
|
|
6870
|
+
},
|
|
6871
|
+
cmps
|
|
6872
|
+
},
|
|
6873
|
+
withheld
|
|
6874
|
+
};
|
|
6875
|
+
}
|
|
6876
|
+
/** Answer a command Home Assistant sent for this device. */
|
|
6877
|
+
async handleCommand(entity, value) {
|
|
6878
|
+
const action = this.state.actions.find((e) => actionEntity(e.actionId) === entity);
|
|
6879
|
+
if (action !== void 0) return {
|
|
6880
|
+
handled: true,
|
|
6881
|
+
applied: (await require_dist.runExportedAction(this.ports.api, this.ports.logger, {
|
|
6882
|
+
exporter: EXPORTER,
|
|
6883
|
+
actionId: action.actionId
|
|
6884
|
+
})).status === "started"
|
|
6885
|
+
};
|
|
6886
|
+
const target = this.state.targets.find((e) => targetEntity(e.targetId) === entity);
|
|
6887
|
+
if (target !== void 0) return {
|
|
6888
|
+
handled: true,
|
|
6889
|
+
applied: (await require_dist.sendToExportedTarget(this.ports.api, this.ports.logger, {
|
|
6890
|
+
exporter: EXPORTER,
|
|
6891
|
+
targetId: target.targetId,
|
|
6892
|
+
notification: { body: value }
|
|
6893
|
+
})).status === "sent"
|
|
6894
|
+
};
|
|
6895
|
+
return { handled: false };
|
|
6896
|
+
}
|
|
6897
|
+
async save(next) {
|
|
6898
|
+
this.state = next;
|
|
6899
|
+
await this.ports.persist(next);
|
|
6900
|
+
}
|
|
6901
|
+
};
|
|
6902
|
+
//#endregion
|
|
6677
6903
|
//#region src/ha-export/supported-device-kinds.ts
|
|
6678
6904
|
/**
|
|
6679
6905
|
* Device types the Home Assistant export picker offers.
|
|
@@ -6778,6 +7004,10 @@ var DEFAULT_CONFIG = {
|
|
|
6778
7004
|
reconcileIntervalSec: 300,
|
|
6779
7005
|
membership: {},
|
|
6780
7006
|
knownBrokers: [],
|
|
7007
|
+
exportedEntities: {
|
|
7008
|
+
actions: [],
|
|
7009
|
+
targets: []
|
|
7010
|
+
},
|
|
6781
7011
|
publicBaseUrl: ""
|
|
6782
7012
|
};
|
|
6783
7013
|
var HaExportAddon = class extends require_dist.BaseAddon {
|
|
@@ -6849,6 +7079,8 @@ var HaExportAddon = class extends require_dist.BaseAddon {
|
|
|
6849
7079
|
* whenever the synthetic devices are, never written to.
|
|
6850
7080
|
*/
|
|
6851
7081
|
syntheticRules = [];
|
|
7082
|
+
/** D968: the actions and notification targets exported beside the devices. */
|
|
7083
|
+
exportedEntities = null;
|
|
6852
7084
|
/**
|
|
6853
7085
|
* `pipelineOrchestrator.getCameraStatuses`, keyed by deviceId — ONE call
|
|
6854
7086
|
* per reconcile pass for every exported camera (Task C3), never per
|
|
@@ -6909,6 +7141,13 @@ var HaExportAddon = class extends require_dist.BaseAddon {
|
|
|
6909
7141
|
for (const link of this.links.values()) await link.client.dispose();
|
|
6910
7142
|
this.links.clear();
|
|
6911
7143
|
});
|
|
7144
|
+
const exportedEntities = new ExportedEntities({
|
|
7145
|
+
api: this.ctx.api,
|
|
7146
|
+
logger: this.ctx.logger,
|
|
7147
|
+
persist: (next) => this.updateGlobalSettings({ exportedEntities: next }),
|
|
7148
|
+
reconcile: () => this.reconcile("exported-entities-changed")
|
|
7149
|
+
}, this.config.exportedEntities ?? DEFAULT_CONFIG.exportedEntities);
|
|
7150
|
+
this.exportedEntities = exportedEntities;
|
|
6912
7151
|
return [
|
|
6913
7152
|
{
|
|
6914
7153
|
capability: require_dist.deviceExportCapability,
|
|
@@ -6921,6 +7160,23 @@ var HaExportAddon = class extends require_dist.BaseAddon {
|
|
|
6921
7160
|
})),
|
|
6922
7161
|
exposeDevice: async ({ deviceId }) => this.setMembershipEverywhere(deviceId, true),
|
|
6923
7162
|
unexposeDevice: async ({ deviceId }) => this.setMembershipEverywhere(deviceId, false),
|
|
7163
|
+
listSupportedEntityKinds: async () => [
|
|
7164
|
+
"device",
|
|
7165
|
+
"action",
|
|
7166
|
+
"notifier"
|
|
7167
|
+
],
|
|
7168
|
+
listExposedEntities: async () => {
|
|
7169
|
+
await exportedEntities.pruneDeleted();
|
|
7170
|
+
return [...exposedDeviceIds(this.config.membership, this.enabledBrokerIds()).map((deviceId) => ({
|
|
7171
|
+
entity: {
|
|
7172
|
+
kind: "device",
|
|
7173
|
+
deviceId
|
|
7174
|
+
},
|
|
7175
|
+
exposedAs: brokersExposing(this.config.membership, deviceId).map((brokerId) => this.brokerName(brokerId)).join(", ")
|
|
7176
|
+
})), ...exportedEntities.list()];
|
|
7177
|
+
},
|
|
7178
|
+
exposeEntity: async ({ entity }) => entity.kind === "device" ? this.setMembershipEverywhere(entity.deviceId, true) : exportedEntities.expose(entity),
|
|
7179
|
+
unexposeEntity: async ({ entity }) => entity.kind === "device" ? this.setMembershipEverywhere(entity.deviceId, false) : exportedEntities.unexpose(entity),
|
|
6924
7180
|
getDeviceSettingsContribution: async ({ deviceId }) => this.buildContribution(deviceId),
|
|
6925
7181
|
getDeviceLiveContribution: async () => null,
|
|
6926
7182
|
applyDeviceSettingsPatch: async ({ deviceId, patch }) => this.applyDeviceSettingsPatch(deviceId, patch)
|
|
@@ -7558,6 +7814,32 @@ var HaExportAddon = class extends require_dist.BaseAddon {
|
|
|
7558
7814
|
* `brokerIds` is every enabled broker, because they belong to the server
|
|
7559
7815
|
* and not to a membership the operator picks per camera.
|
|
7560
7816
|
*/
|
|
7817
|
+
/**
|
|
7818
|
+
* The exported actions and notification targets (D968): one more
|
|
7819
|
+
* synthetic device. What the operator deleted is dropped first (a failed
|
|
7820
|
+
* read changes nothing); a target the component cannot build as `notify`
|
|
7821
|
+
* is named, not silently lost.
|
|
7822
|
+
*/
|
|
7823
|
+
if (this.exportedEntities !== null) {
|
|
7824
|
+
await this.exportedEntities.pruneDeleted();
|
|
7825
|
+
const { plan: entitiesPlan, withheld } = this.exportedEntities.plan(this.componentSupport.supportForAll([...enabled]));
|
|
7826
|
+
if (withheld.length > 0) this.ctx.logger.warn("ha-export: notification targets withheld — the Home Assistant component does not build notify entities yet", { meta: { targetIds: withheld } });
|
|
7827
|
+
if (entitiesPlan !== null) {
|
|
7828
|
+
exported.set(entitiesPlan.deviceKey, {
|
|
7829
|
+
plan: entitiesPlan,
|
|
7830
|
+
deviceId: -1,
|
|
7831
|
+
target: {
|
|
7832
|
+
deviceId: -1,
|
|
7833
|
+
type: "synthetic",
|
|
7834
|
+
boundCaps: []
|
|
7835
|
+
},
|
|
7836
|
+
brokerIds: [...enabled],
|
|
7837
|
+
streams: [],
|
|
7838
|
+
cameraFunctions: null
|
|
7839
|
+
});
|
|
7840
|
+
entityCount += Object.keys(entitiesPlan.cmps).length;
|
|
7841
|
+
}
|
|
7842
|
+
}
|
|
7561
7843
|
const synthetic = await this.buildSynthetic(byId);
|
|
7562
7844
|
if (synthetic !== null) for (const plan of syntheticPlans(synthetic)) {
|
|
7563
7845
|
exported.set(plan.deviceKey, {
|
|
@@ -8580,6 +8862,21 @@ var HaExportAddon = class extends require_dist.BaseAddon {
|
|
|
8580
8862
|
* `setRuleEnabled`, never a store of its own. A second knob that can
|
|
8581
8863
|
* disagree with the admin UI is worse than no knob (D62).
|
|
8582
8864
|
*/
|
|
8865
|
+
if (this.exportedEntities !== null && parsed.deviceKey === syntheticDeviceKeyFor("synthetic-exported-entities")) {
|
|
8866
|
+
const outcome = await this.exportedEntities.handleCommand(parsed.entity, value);
|
|
8867
|
+
if (!outcome.handled) {
|
|
8868
|
+
this.ctx.logger.warn("ha-export: dropping a command for an entity that is not exported", { meta: {
|
|
8869
|
+
topic,
|
|
8870
|
+
entity: parsed.entity
|
|
8871
|
+
} });
|
|
8872
|
+
reply.status(422);
|
|
8873
|
+
reply.send({ error: "entity not exported" });
|
|
8874
|
+
return;
|
|
8875
|
+
}
|
|
8876
|
+
reply.status(outcome.applied ? 200 : 422);
|
|
8877
|
+
reply.send(outcome.applied ? {} : { error: "command not applied" });
|
|
8878
|
+
return;
|
|
8879
|
+
}
|
|
8583
8880
|
if (isSyntheticDeviceKey(parsed.deviceKey)) {
|
|
8584
8881
|
const ruleId = ruleIdFromEntity(parsed.entity, this.syntheticRules);
|
|
8585
8882
|
if (ruleId === null) {
|