@camstack/addon-provider-homeassistant 1.2.655 → 1.2.656
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 +6 -0
- package/dist/addon.js +2 -1
- package/dist/addon.mjs +2 -1
- package/dist/ha-export/ha-export.addon.js +461 -248
- package/dist/ha-export/ha-export.addon.mjs +448 -249
- package/dist/{ha-instance-settings-5Ms7yRju.js → ha-instance-settings-Dm0tU8iv.js} +115 -41
- package/dist/{ha-instance-settings-DLDNOmgL.mjs → ha-instance-settings-EQ5Ey3Q-.mjs} +110 -30
- package/package.json +2 -2
|
@@ -5981,7 +5981,7 @@ var ZodIssueCode = {
|
|
|
5981
5981
|
var ZodFirstPartyTypeKind;
|
|
5982
5982
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5983
5983
|
//#endregion
|
|
5984
|
-
//#region ../types/dist/sleep-
|
|
5984
|
+
//#region ../types/dist/sleep-BaWhXZFm.mjs
|
|
5985
5985
|
/**
|
|
5986
5986
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5987
5987
|
* window to float samples (D455).
|
|
@@ -8783,6 +8783,13 @@ var RecordsError = class extends Error {
|
|
|
8783
8783
|
this.detail = detail;
|
|
8784
8784
|
}
|
|
8785
8785
|
};
|
|
8786
|
+
/** A write guarded by `ifRev` found another revision (or an existing row for `ifRev: 0`). */
|
|
8787
|
+
var RevConflictError = class extends RecordsError {
|
|
8788
|
+
constructor(detail) {
|
|
8789
|
+
super("rev-conflict", detail);
|
|
8790
|
+
this.name = "RevConflictError";
|
|
8791
|
+
}
|
|
8792
|
+
};
|
|
8786
8793
|
/** The keychain could not store or serve a value: explicit, never an empty string. */
|
|
8787
8794
|
var RecordSecretUnavailableError = class extends RecordsError {
|
|
8788
8795
|
constructor(detail) {
|
|
@@ -8827,6 +8834,10 @@ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
|
|
|
8827
8834
|
* (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
|
|
8828
8835
|
* (`auth.accessToken`, `headers[].value`).
|
|
8829
8836
|
*
|
|
8837
|
+
* `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
|
|
8838
|
+
* names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
|
|
8839
|
+
* key releases exactly that key's secret.
|
|
8840
|
+
*
|
|
8830
8841
|
* Array elements are matched by a DECLARED id key (`secretArrayKeys`:
|
|
8831
8842
|
* `{ headers: 'name' }`), never by index, so reordering a list keeps every
|
|
8832
8843
|
* keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
|
|
@@ -8839,11 +8850,13 @@ function parseSecretPath(path) {
|
|
|
8839
8850
|
if (path.length === 0) throw new InvalidRecordError("an empty secret path");
|
|
8840
8851
|
return path.split(".").map((raw) => {
|
|
8841
8852
|
const each = raw.endsWith("[]");
|
|
8842
|
-
const
|
|
8853
|
+
const map = raw.endsWith("{}");
|
|
8854
|
+
const key = each || map ? raw.slice(0, -2) : raw;
|
|
8843
8855
|
if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
|
|
8844
8856
|
return {
|
|
8845
8857
|
key,
|
|
8846
|
-
each
|
|
8858
|
+
each,
|
|
8859
|
+
map
|
|
8847
8860
|
};
|
|
8848
8861
|
});
|
|
8849
8862
|
}
|
|
@@ -8895,6 +8908,22 @@ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arra
|
|
|
8895
8908
|
[segment.key]: mapped
|
|
8896
8909
|
};
|
|
8897
8910
|
}
|
|
8911
|
+
if (segment.map) {
|
|
8912
|
+
if (!isRecord$4(child)) return node;
|
|
8913
|
+
const kept = Object.entries(child).map(([mapKey, value]) => {
|
|
8914
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
|
|
8915
|
+
if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
|
|
8916
|
+
return [mapKey, visit({
|
|
8917
|
+
declared,
|
|
8918
|
+
concrete,
|
|
8919
|
+
value
|
|
8920
|
+
})];
|
|
8921
|
+
}).filter(([, value]) => value !== REMOVE_LEAF);
|
|
8922
|
+
return {
|
|
8923
|
+
...node,
|
|
8924
|
+
[segment.key]: Object.fromEntries(kept)
|
|
8925
|
+
};
|
|
8926
|
+
}
|
|
8898
8927
|
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
|
|
8899
8928
|
if (last) {
|
|
8900
8929
|
const replacement = visit({
|
|
@@ -9036,7 +9065,8 @@ function isZodSecret(schema) {
|
|
|
9036
9065
|
}
|
|
9037
9066
|
/**
|
|
9038
9067
|
* Every declared secret path in a config schema: dotted keys, `[]` for "each
|
|
9039
|
-
* element of this array" (`connection.password`, `rows[].token`)
|
|
9068
|
+
* element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
|
|
9069
|
+
* map" (`applianceSecrets{}`). A declared
|
|
9040
9070
|
* object or array is reported as one path and not descended into — everything
|
|
9041
9071
|
* under it is secret.
|
|
9042
9072
|
*/
|
|
@@ -9047,6 +9077,7 @@ function collectZodSecretPaths(schema, prefix = "") {
|
|
|
9047
9077
|
const def = defOf(layer);
|
|
9048
9078
|
if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
|
|
9049
9079
|
else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
|
|
9080
|
+
else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
|
|
9050
9081
|
else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
|
|
9051
9082
|
else if (def.type === "intersection") {
|
|
9052
9083
|
if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
|
|
@@ -13423,11 +13454,49 @@ function buildCapturingReply(envelope) {
|
|
|
13423
13454
|
};
|
|
13424
13455
|
return wrapper;
|
|
13425
13456
|
}
|
|
13457
|
+
/**
|
|
13458
|
+
* `RecordIntegrationInstances` - the `integration-instances` provider over ONE
|
|
13459
|
+
* entry kind, so an addon that holds a connection serves it in ~30 lines.
|
|
13460
|
+
*
|
|
13461
|
+
* Every instance is one record of the addon's own kind, keyed by the
|
|
13462
|
+
* `integrations` registry id. The records door does the secret work: a field the
|
|
13463
|
+
* kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
|
|
13464
|
+
* deleting the row deletes the secret, and one row's value is never another's.
|
|
13465
|
+
*
|
|
13466
|
+
* Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
|
|
13467
|
+
* token or the view the form was given KEEP the stored secret;
|
|
13468
|
+
* `{ $secretClear: true }` clears it; any other string replaces it.
|
|
13469
|
+
*/
|
|
13426
13470
|
/** The most instances one addon lists (an integrations page, not a table). */
|
|
13427
13471
|
var INSTANCES_LIST_MAX = 1e3;
|
|
13428
13472
|
/** The keys of the kind's secret fields that sit at the top level of the body. */
|
|
13429
13473
|
function topLevelSecretKeys(paths) {
|
|
13430
|
-
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[")));
|
|
13474
|
+
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
|
|
13475
|
+
}
|
|
13476
|
+
/** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
|
|
13477
|
+
function secretMapKeys(paths) {
|
|
13478
|
+
return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
|
|
13479
|
+
}
|
|
13480
|
+
/**
|
|
13481
|
+
* A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
|
|
13482
|
+
* view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
|
|
13483
|
+
* or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
|
|
13484
|
+
*/
|
|
13485
|
+
function secretMapPatch(value) {
|
|
13486
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
13487
|
+
const out = {};
|
|
13488
|
+
for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
|
|
13489
|
+
else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
|
|
13490
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
13491
|
+
}
|
|
13492
|
+
/** Does the kind's body schema carry an `enabled` field? */
|
|
13493
|
+
function declaresEnabled(kind) {
|
|
13494
|
+
const json = toJSONSchema(kind.schema, {
|
|
13495
|
+
unrepresentable: "any",
|
|
13496
|
+
io: "input"
|
|
13497
|
+
});
|
|
13498
|
+
const properties = Reflect.get(json, "properties");
|
|
13499
|
+
return typeof properties === "object" && properties !== null && "enabled" in properties;
|
|
13431
13500
|
}
|
|
13432
13501
|
var RecordIntegrationInstances = class {
|
|
13433
13502
|
records;
|
|
@@ -13435,14 +13504,20 @@ var RecordIntegrationInstances = class {
|
|
|
13435
13504
|
hooks;
|
|
13436
13505
|
handle;
|
|
13437
13506
|
secretKeys;
|
|
13507
|
+
secretMaps;
|
|
13438
13508
|
nameField;
|
|
13509
|
+
/** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
|
|
13510
|
+
registryOwnsEnabled;
|
|
13439
13511
|
constructor(records, kind, hooks) {
|
|
13440
13512
|
this.records = records;
|
|
13441
13513
|
this.kind = kind;
|
|
13442
13514
|
this.hooks = hooks;
|
|
13443
13515
|
this.handle = records.kind(kind);
|
|
13444
13516
|
this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
|
|
13517
|
+
this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
|
|
13445
13518
|
this.nameField = kind.declaration.entry?.nameField ?? "name";
|
|
13519
|
+
this.registryOwnsEnabled = hooks.description.role === "devices";
|
|
13520
|
+
if (this.registryOwnsEnabled && declaresEnabled(kind)) throw new Error(`kind "${kind.name}": the integrations registry owns the on/off switch of a role "devices" addon, so the body must not declare "enabled"`);
|
|
13446
13521
|
}
|
|
13447
13522
|
/**
|
|
13448
13523
|
* One instance read for the addon's own runtime; null when absent. When the keychain is
|
|
@@ -13493,7 +13568,7 @@ var RecordIntegrationInstances = class {
|
|
|
13493
13568
|
id: record.id,
|
|
13494
13569
|
name: typeof rawName === "string" ? rawName : record.id,
|
|
13495
13570
|
configured: this.hooks.configured(record.data),
|
|
13496
|
-
enabled: record.data.enabled !== false,
|
|
13571
|
+
...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
|
|
13497
13572
|
formValues: shown,
|
|
13498
13573
|
...kind !== void 0 ? { kind } : {},
|
|
13499
13574
|
...health?.status !== void 0 ? { status: health.status } : {},
|
|
@@ -13523,32 +13598,36 @@ var RecordIntegrationInstances = class {
|
|
|
13523
13598
|
id: minted,
|
|
13524
13599
|
data: this.hooks.initialBody(name, kind)
|
|
13525
13600
|
}, { ifRev: 0 });
|
|
13526
|
-
await this.hooks.onChanged?.(minted);
|
|
13601
|
+
await this.hooks.onChanged?.(minted, "created");
|
|
13527
13602
|
return { id: minted };
|
|
13528
13603
|
};
|
|
13529
13604
|
updateInstance = async ({ id, name, values }) => {
|
|
13530
13605
|
const patch = {};
|
|
13531
13606
|
for (const [key, value] of Object.entries(values)) {
|
|
13532
13607
|
if (value === void 0) continue;
|
|
13533
|
-
if (
|
|
13608
|
+
if (this.secretMaps.has(key)) {
|
|
13609
|
+
const entries = secretMapPatch(value);
|
|
13610
|
+
if (entries !== null) patch[key] = entries;
|
|
13611
|
+
} else if (!this.secretKeys.has(key)) patch[key] = value;
|
|
13534
13612
|
else if (isSecretClear(value)) patch[key] = null;
|
|
13535
13613
|
else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
|
|
13536
13614
|
}
|
|
13537
13615
|
if (name !== void 0) patch[this.nameField] = name;
|
|
13538
13616
|
await this.handle.patch(id, patch);
|
|
13539
|
-
await this.hooks.onChanged?.(id);
|
|
13617
|
+
await this.hooks.onChanged?.(id, "updated");
|
|
13540
13618
|
return { ok: true };
|
|
13541
13619
|
};
|
|
13542
13620
|
/** The switch lives in the record body; the addon is told so it applies it (D996). */
|
|
13543
13621
|
setInstanceEnabled = async ({ id, enabled }) => {
|
|
13622
|
+
if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
|
|
13544
13623
|
await this.handle.patch(id, { enabled });
|
|
13545
|
-
await this.hooks.onChanged?.(id);
|
|
13624
|
+
await this.hooks.onChanged?.(id, "enabled");
|
|
13546
13625
|
return { ok: true };
|
|
13547
13626
|
};
|
|
13548
13627
|
/** The row and its keychain refs go together (the records door). */
|
|
13549
13628
|
deleteInstance = async ({ id }) => {
|
|
13550
13629
|
await this.handle.delete(id);
|
|
13551
|
-
await this.hooks.onChanged?.(id);
|
|
13630
|
+
await this.hooks.onChanged?.(id, "deleted");
|
|
13552
13631
|
return { ok: true };
|
|
13553
13632
|
};
|
|
13554
13633
|
testInstance = async (input) => {
|
|
@@ -56775,6 +56854,21 @@ var IntegrationInstanceKindSchema = object({
|
|
|
56775
56854
|
disconnectPath: string().min(1).optional()
|
|
56776
56855
|
});
|
|
56777
56856
|
var IntegrationInstanceDescriptionSchema = object({
|
|
56857
|
+
/** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
|
|
56858
|
+
role: _enum([
|
|
56859
|
+
"devices",
|
|
56860
|
+
"services",
|
|
56861
|
+
"both"
|
|
56862
|
+
]),
|
|
56863
|
+
/** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
|
|
56864
|
+
discovery: object({
|
|
56865
|
+
mode: _enum([
|
|
56866
|
+
"none",
|
|
56867
|
+
"lan",
|
|
56868
|
+
"account"
|
|
56869
|
+
]),
|
|
56870
|
+
scopedToInstance: boolean()
|
|
56871
|
+
}).optional(),
|
|
56778
56872
|
/** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
|
|
56779
56873
|
instanceMode: IntegrationInstanceModeSchema,
|
|
56780
56874
|
/** What the instance is, for the UI (`destination`, `relay`, `account`). */
|
|
@@ -56814,10 +56908,11 @@ var IntegrationInstanceSchema = object({
|
|
|
56814
56908
|
/** The addon's own rule (an URL is present, a client id is present...). */
|
|
56815
56909
|
configured: boolean(),
|
|
56816
56910
|
/**
|
|
56817
|
-
* The instance's own on/off switch (D996)
|
|
56818
|
-
*
|
|
56911
|
+
* The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
|
|
56912
|
+
* so a row written before the field existed is on). Absent when the `integrations` registry
|
|
56913
|
+
* owns the switch (`describe().role` is `devices`): read it from the registry row.
|
|
56819
56914
|
*/
|
|
56820
|
-
enabled: boolean(),
|
|
56915
|
+
enabled: boolean().optional(),
|
|
56821
56916
|
/** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
|
|
56822
56917
|
kind: string().optional(),
|
|
56823
56918
|
/** Values for the instance form; a secret is a `SecretFieldView`, never text. */
|
|
@@ -98300,21 +98395,6 @@ function haInstanceSettingsPortsOver(api) {
|
|
|
98300
98395
|
}
|
|
98301
98396
|
};
|
|
98302
98397
|
}
|
|
98303
|
-
/** The brokerId → settings view of the instances: what a reader keeps per broker. */
|
|
98304
|
-
function settingsByBroker(rows) {
|
|
98305
|
-
const out = /* @__PURE__ */ new Map();
|
|
98306
|
-
for (const row of rows) if (row.brokerId !== null) out.set(row.brokerId, row.settings);
|
|
98307
|
-
return out;
|
|
98308
|
-
}
|
|
98309
|
-
/** Read the instances, or report why not and answer nothing (a reader falls back to the legacy key). */
|
|
98310
|
-
async function listInstancesOrNothing(ports, onError) {
|
|
98311
|
-
try {
|
|
98312
|
-
return await ports.listInstances();
|
|
98313
|
-
} catch (err) {
|
|
98314
|
-
onError(errMsg(err));
|
|
98315
|
-
return [];
|
|
98316
|
-
}
|
|
98317
|
-
}
|
|
98318
98398
|
//#endregion
|
|
98319
98399
|
Object.defineProperty(exports, "AlarmArmModeSchema", {
|
|
98320
98400
|
enumerable: true,
|
|
@@ -98472,6 +98552,12 @@ Object.defineProperty(exports, "RecordSecretUnavailableError", {
|
|
|
98472
98552
|
return RecordSecretUnavailableError;
|
|
98473
98553
|
}
|
|
98474
98554
|
});
|
|
98555
|
+
Object.defineProperty(exports, "RevConflictError", {
|
|
98556
|
+
enumerable: true,
|
|
98557
|
+
get: function() {
|
|
98558
|
+
return RevConflictError;
|
|
98559
|
+
}
|
|
98560
|
+
});
|
|
98475
98561
|
Object.defineProperty(exports, "TargetSchema", {
|
|
98476
98562
|
enumerable: true,
|
|
98477
98563
|
get: function() {
|
|
@@ -98808,12 +98894,6 @@ Object.defineProperty(exports, "listHomeAssistantBrokers", {
|
|
|
98808
98894
|
return listHomeAssistantBrokers;
|
|
98809
98895
|
}
|
|
98810
98896
|
});
|
|
98811
|
-
Object.defineProperty(exports, "listInstancesOrNothing", {
|
|
98812
|
-
enumerable: true,
|
|
98813
|
-
get: function() {
|
|
98814
|
-
return listInstancesOrNothing;
|
|
98815
|
-
}
|
|
98816
|
-
});
|
|
98817
98897
|
Object.defineProperty(exports, "literal", {
|
|
98818
98898
|
enumerable: true,
|
|
98819
98899
|
get: function() {
|
|
@@ -98994,12 +99074,6 @@ Object.defineProperty(exports, "sendToExportedTarget", {
|
|
|
98994
99074
|
return sendToExportedTarget;
|
|
98995
99075
|
}
|
|
98996
99076
|
});
|
|
98997
|
-
Object.defineProperty(exports, "settingsByBroker", {
|
|
98998
|
-
enumerable: true,
|
|
98999
|
-
get: function() {
|
|
99000
|
-
return settingsByBroker;
|
|
99001
|
-
}
|
|
99002
|
-
});
|
|
99003
99077
|
Object.defineProperty(exports, "smokeCapability", {
|
|
99004
99078
|
enumerable: true,
|
|
99005
99079
|
get: function() {
|
|
@@ -5980,7 +5980,7 @@ var ZodIssueCode = {
|
|
|
5980
5980
|
var ZodFirstPartyTypeKind;
|
|
5981
5981
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5982
5982
|
//#endregion
|
|
5983
|
-
//#region ../types/dist/sleep-
|
|
5983
|
+
//#region ../types/dist/sleep-BaWhXZFm.mjs
|
|
5984
5984
|
/**
|
|
5985
5985
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5986
5986
|
* window to float samples (D455).
|
|
@@ -8782,6 +8782,13 @@ var RecordsError = class extends Error {
|
|
|
8782
8782
|
this.detail = detail;
|
|
8783
8783
|
}
|
|
8784
8784
|
};
|
|
8785
|
+
/** A write guarded by `ifRev` found another revision (or an existing row for `ifRev: 0`). */
|
|
8786
|
+
var RevConflictError = class extends RecordsError {
|
|
8787
|
+
constructor(detail) {
|
|
8788
|
+
super("rev-conflict", detail);
|
|
8789
|
+
this.name = "RevConflictError";
|
|
8790
|
+
}
|
|
8791
|
+
};
|
|
8785
8792
|
/** The keychain could not store or serve a value: explicit, never an empty string. */
|
|
8786
8793
|
var RecordSecretUnavailableError = class extends RecordsError {
|
|
8787
8794
|
constructor(detail) {
|
|
@@ -8826,6 +8833,10 @@ var RECORD_SECRET_COLLECTION_PREFIX = "records:";
|
|
|
8826
8833
|
* (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
|
|
8827
8834
|
* (`auth.accessToken`, `headers[].value`).
|
|
8828
8835
|
*
|
|
8836
|
+
* `{}` after a key means "each value of this map" (`applianceSecrets{}`): the map's own key
|
|
8837
|
+
* names the value (`applianceSecrets{fridge}`), so no id key is declared and a body that drops a
|
|
8838
|
+
* key releases exactly that key's secret.
|
|
8839
|
+
*
|
|
8829
8840
|
* Array elements are matched by a DECLARED id key (`secretArrayKeys`:
|
|
8830
8841
|
* `{ headers: 'name' }`), never by index, so reordering a list keeps every
|
|
8831
8842
|
* keychain ref (the same rule as `arrayItemFieldPath`). The concrete path of a
|
|
@@ -8838,11 +8849,13 @@ function parseSecretPath(path) {
|
|
|
8838
8849
|
if (path.length === 0) throw new InvalidRecordError("an empty secret path");
|
|
8839
8850
|
return path.split(".").map((raw) => {
|
|
8840
8851
|
const each = raw.endsWith("[]");
|
|
8841
|
-
const
|
|
8852
|
+
const map = raw.endsWith("{}");
|
|
8853
|
+
const key = each || map ? raw.slice(0, -2) : raw;
|
|
8842
8854
|
if (key.length === 0) throw new InvalidRecordError(`secret path "${path}" has an empty segment`);
|
|
8843
8855
|
return {
|
|
8844
8856
|
key,
|
|
8845
|
-
each
|
|
8857
|
+
each,
|
|
8858
|
+
map
|
|
8846
8859
|
};
|
|
8847
8860
|
});
|
|
8848
8861
|
}
|
|
@@ -8894,6 +8907,22 @@ function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arra
|
|
|
8894
8907
|
[segment.key]: mapped
|
|
8895
8908
|
};
|
|
8896
8909
|
}
|
|
8910
|
+
if (segment.map) {
|
|
8911
|
+
if (!isRecord$4(child)) return node;
|
|
8912
|
+
const kept = Object.entries(child).map(([mapKey, value]) => {
|
|
8913
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}{${encodeURIComponent(mapKey)}}`;
|
|
8914
|
+
if (!last) return [mapKey, mapAt(value, segments, index + 1, keys, concrete, declared, arrayKeys, visit)];
|
|
8915
|
+
return [mapKey, visit({
|
|
8916
|
+
declared,
|
|
8917
|
+
concrete,
|
|
8918
|
+
value
|
|
8919
|
+
})];
|
|
8920
|
+
}).filter(([, value]) => value !== REMOVE_LEAF);
|
|
8921
|
+
return {
|
|
8922
|
+
...node,
|
|
8923
|
+
[segment.key]: Object.fromEntries(kept)
|
|
8924
|
+
};
|
|
8925
|
+
}
|
|
8897
8926
|
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
|
|
8898
8927
|
if (last) {
|
|
8899
8928
|
const replacement = visit({
|
|
@@ -9035,7 +9064,8 @@ function isZodSecret(schema) {
|
|
|
9035
9064
|
}
|
|
9036
9065
|
/**
|
|
9037
9066
|
* Every declared secret path in a config schema: dotted keys, `[]` for "each
|
|
9038
|
-
* element of this array" (`connection.password`, `rows[].token`)
|
|
9067
|
+
* element of this array" (`connection.password`, `rows[].token`), `{}` for "each value of this
|
|
9068
|
+
* map" (`applianceSecrets{}`). A declared
|
|
9039
9069
|
* object or array is reported as one path and not descended into — everything
|
|
9040
9070
|
* under it is secret.
|
|
9041
9071
|
*/
|
|
@@ -9046,6 +9076,7 @@ function collectZodSecretPaths(schema, prefix = "") {
|
|
|
9046
9076
|
const def = defOf(layer);
|
|
9047
9077
|
if (def.type === "object" && def.shape) for (const [key, child] of Object.entries(def.shape)) paths.push(...collectZodSecretPaths(child, prefix ? `${prefix}.${key}` : key));
|
|
9048
9078
|
else if (def.type === "array" && def.element) paths.push(...collectZodSecretPaths(def.element, `${prefix}[]`));
|
|
9079
|
+
else if (def.type === "record" && def.valueType) paths.push(...collectZodSecretPaths(def.valueType, `${prefix}{}`));
|
|
9049
9080
|
else if (def.type === "union" && def.options) for (const option of def.options) paths.push(...collectZodSecretPaths(option, prefix));
|
|
9050
9081
|
else if (def.type === "intersection") {
|
|
9051
9082
|
if (def.left) paths.push(...collectZodSecretPaths(def.left, prefix));
|
|
@@ -13422,11 +13453,49 @@ function buildCapturingReply(envelope) {
|
|
|
13422
13453
|
};
|
|
13423
13454
|
return wrapper;
|
|
13424
13455
|
}
|
|
13456
|
+
/**
|
|
13457
|
+
* `RecordIntegrationInstances` - the `integration-instances` provider over ONE
|
|
13458
|
+
* entry kind, so an addon that holds a connection serves it in ~30 lines.
|
|
13459
|
+
*
|
|
13460
|
+
* Every instance is one record of the addon's own kind, keyed by the
|
|
13461
|
+
* `integrations` registry id. The records door does the secret work: a field the
|
|
13462
|
+
* kind marks `.meta({ secret: true })` is a keychain marker bound to the row, so
|
|
13463
|
+
* deleting the row deletes the secret, and one row's value is never another's.
|
|
13464
|
+
*
|
|
13465
|
+
* Patch rules for a secret (`updateInstance`): omitted, `''`, the redaction
|
|
13466
|
+
* token or the view the form was given KEEP the stored secret;
|
|
13467
|
+
* `{ $secretClear: true }` clears it; any other string replaces it.
|
|
13468
|
+
*/
|
|
13425
13469
|
/** The most instances one addon lists (an integrations page, not a table). */
|
|
13426
13470
|
var INSTANCES_LIST_MAX = 1e3;
|
|
13427
13471
|
/** The keys of the kind's secret fields that sit at the top level of the body. */
|
|
13428
13472
|
function topLevelSecretKeys(paths) {
|
|
13429
|
-
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[")));
|
|
13473
|
+
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[") && !p.includes("{")));
|
|
13474
|
+
}
|
|
13475
|
+
/** The keys of the kind's top-level MAPS whose values are secrets (`applianceSecrets{}`). */
|
|
13476
|
+
function secretMapKeys(paths) {
|
|
13477
|
+
return new Set(paths.filter((p) => p.endsWith("{}") && !p.slice(0, -2).match(/[.[{]/)).map((p) => p.slice(0, -2)));
|
|
13478
|
+
}
|
|
13479
|
+
/**
|
|
13480
|
+
* A patch value for a map of secrets: an entry that is omitted, `''`, the redaction token or the
|
|
13481
|
+
* view the form was given KEEPS its stored secret (it is left out of the merge); `{ $secretClear }`
|
|
13482
|
+
* or `null` removes the entry; any other string replaces it. Null when nothing is left to merge.
|
|
13483
|
+
*/
|
|
13484
|
+
function secretMapPatch(value) {
|
|
13485
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
13486
|
+
const out = {};
|
|
13487
|
+
for (const [key, entry] of Object.entries(value)) if (entry === null || isSecretClear(entry)) out[key] = null;
|
|
13488
|
+
else if (typeof entry === "string" && entry !== "" && entry !== "__camstack_redacted__" && !isSecretFieldView(entry)) out[key] = entry;
|
|
13489
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
13490
|
+
}
|
|
13491
|
+
/** Does the kind's body schema carry an `enabled` field? */
|
|
13492
|
+
function declaresEnabled(kind) {
|
|
13493
|
+
const json = toJSONSchema(kind.schema, {
|
|
13494
|
+
unrepresentable: "any",
|
|
13495
|
+
io: "input"
|
|
13496
|
+
});
|
|
13497
|
+
const properties = Reflect.get(json, "properties");
|
|
13498
|
+
return typeof properties === "object" && properties !== null && "enabled" in properties;
|
|
13430
13499
|
}
|
|
13431
13500
|
var RecordIntegrationInstances = class {
|
|
13432
13501
|
records;
|
|
@@ -13434,14 +13503,20 @@ var RecordIntegrationInstances = class {
|
|
|
13434
13503
|
hooks;
|
|
13435
13504
|
handle;
|
|
13436
13505
|
secretKeys;
|
|
13506
|
+
secretMaps;
|
|
13437
13507
|
nameField;
|
|
13508
|
+
/** The `integrations` registry owns the on/off switch (`role: devices`); the body has none. */
|
|
13509
|
+
registryOwnsEnabled;
|
|
13438
13510
|
constructor(records, kind, hooks) {
|
|
13439
13511
|
this.records = records;
|
|
13440
13512
|
this.kind = kind;
|
|
13441
13513
|
this.hooks = hooks;
|
|
13442
13514
|
this.handle = records.kind(kind);
|
|
13443
13515
|
this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
|
|
13516
|
+
this.secretMaps = secretMapKeys(kind.declaration.secretPaths);
|
|
13444
13517
|
this.nameField = kind.declaration.entry?.nameField ?? "name";
|
|
13518
|
+
this.registryOwnsEnabled = hooks.description.role === "devices";
|
|
13519
|
+
if (this.registryOwnsEnabled && declaresEnabled(kind)) throw new Error(`kind "${kind.name}": the integrations registry owns the on/off switch of a role "devices" addon, so the body must not declare "enabled"`);
|
|
13445
13520
|
}
|
|
13446
13521
|
/**
|
|
13447
13522
|
* One instance read for the addon's own runtime; null when absent. When the keychain is
|
|
@@ -13492,7 +13567,7 @@ var RecordIntegrationInstances = class {
|
|
|
13492
13567
|
id: record.id,
|
|
13493
13568
|
name: typeof rawName === "string" ? rawName : record.id,
|
|
13494
13569
|
configured: this.hooks.configured(record.data),
|
|
13495
|
-
enabled: record.data.enabled !== false,
|
|
13570
|
+
...this.registryOwnsEnabled ? {} : { enabled: record.data.enabled !== false },
|
|
13496
13571
|
formValues: shown,
|
|
13497
13572
|
...kind !== void 0 ? { kind } : {},
|
|
13498
13573
|
...health?.status !== void 0 ? { status: health.status } : {},
|
|
@@ -13522,32 +13597,36 @@ var RecordIntegrationInstances = class {
|
|
|
13522
13597
|
id: minted,
|
|
13523
13598
|
data: this.hooks.initialBody(name, kind)
|
|
13524
13599
|
}, { ifRev: 0 });
|
|
13525
|
-
await this.hooks.onChanged?.(minted);
|
|
13600
|
+
await this.hooks.onChanged?.(minted, "created");
|
|
13526
13601
|
return { id: minted };
|
|
13527
13602
|
};
|
|
13528
13603
|
updateInstance = async ({ id, name, values }) => {
|
|
13529
13604
|
const patch = {};
|
|
13530
13605
|
for (const [key, value] of Object.entries(values)) {
|
|
13531
13606
|
if (value === void 0) continue;
|
|
13532
|
-
if (
|
|
13607
|
+
if (this.secretMaps.has(key)) {
|
|
13608
|
+
const entries = secretMapPatch(value);
|
|
13609
|
+
if (entries !== null) patch[key] = entries;
|
|
13610
|
+
} else if (!this.secretKeys.has(key)) patch[key] = value;
|
|
13533
13611
|
else if (isSecretClear(value)) patch[key] = null;
|
|
13534
13612
|
else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
|
|
13535
13613
|
}
|
|
13536
13614
|
if (name !== void 0) patch[this.nameField] = name;
|
|
13537
13615
|
await this.handle.patch(id, patch);
|
|
13538
|
-
await this.hooks.onChanged?.(id);
|
|
13616
|
+
await this.hooks.onChanged?.(id, "updated");
|
|
13539
13617
|
return { ok: true };
|
|
13540
13618
|
};
|
|
13541
13619
|
/** The switch lives in the record body; the addon is told so it applies it (D996). */
|
|
13542
13620
|
setInstanceEnabled = async ({ id, enabled }) => {
|
|
13621
|
+
if (this.registryOwnsEnabled) throw new Error(`instance "${id}": the integrations registry owns the on/off switch of this addon; set it with integrations.update`);
|
|
13543
13622
|
await this.handle.patch(id, { enabled });
|
|
13544
|
-
await this.hooks.onChanged?.(id);
|
|
13623
|
+
await this.hooks.onChanged?.(id, "enabled");
|
|
13545
13624
|
return { ok: true };
|
|
13546
13625
|
};
|
|
13547
13626
|
/** The row and its keychain refs go together (the records door). */
|
|
13548
13627
|
deleteInstance = async ({ id }) => {
|
|
13549
13628
|
await this.handle.delete(id);
|
|
13550
|
-
await this.hooks.onChanged?.(id);
|
|
13629
|
+
await this.hooks.onChanged?.(id, "deleted");
|
|
13551
13630
|
return { ok: true };
|
|
13552
13631
|
};
|
|
13553
13632
|
testInstance = async (input) => {
|
|
@@ -56774,6 +56853,21 @@ var IntegrationInstanceKindSchema = object({
|
|
|
56774
56853
|
disconnectPath: string().min(1).optional()
|
|
56775
56854
|
});
|
|
56776
56855
|
var IntegrationInstanceDescriptionSchema = object({
|
|
56856
|
+
/** What the instances are for the Integrations page; see {@link IntegrationInstanceRoleSchema}. */
|
|
56857
|
+
role: _enum([
|
|
56858
|
+
"devices",
|
|
56859
|
+
"services",
|
|
56860
|
+
"both"
|
|
56861
|
+
]),
|
|
56862
|
+
/** How the devices of an instance are found; absent means `{ mode: 'none' }`. */
|
|
56863
|
+
discovery: object({
|
|
56864
|
+
mode: _enum([
|
|
56865
|
+
"none",
|
|
56866
|
+
"lan",
|
|
56867
|
+
"account"
|
|
56868
|
+
]),
|
|
56869
|
+
scopedToInstance: boolean()
|
|
56870
|
+
}).optional(),
|
|
56777
56871
|
/** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
|
|
56778
56872
|
instanceMode: IntegrationInstanceModeSchema,
|
|
56779
56873
|
/** What the instance is, for the UI (`destination`, `relay`, `account`). */
|
|
@@ -56813,10 +56907,11 @@ var IntegrationInstanceSchema = object({
|
|
|
56813
56907
|
/** The addon's own rule (an URL is present, a client id is present...). */
|
|
56814
56908
|
configured: boolean(),
|
|
56815
56909
|
/**
|
|
56816
|
-
* The instance's own on/off switch (D996)
|
|
56817
|
-
*
|
|
56910
|
+
* The instance's own on/off switch (D996), derived from the record body (`enabled !== false`,
|
|
56911
|
+
* so a row written before the field existed is on). Absent when the `integrations` registry
|
|
56912
|
+
* owns the switch (`describe().role` is `devices`): read it from the registry row.
|
|
56818
56913
|
*/
|
|
56819
|
-
enabled: boolean(),
|
|
56914
|
+
enabled: boolean().optional(),
|
|
56820
56915
|
/** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
|
|
56821
56916
|
kind: string().optional(),
|
|
56822
56917
|
/** Values for the instance form; a secret is a `SecretFieldView`, never text. */
|
|
@@ -98299,20 +98394,5 @@ function haInstanceSettingsPortsOver(api) {
|
|
|
98299
98394
|
}
|
|
98300
98395
|
};
|
|
98301
98396
|
}
|
|
98302
|
-
/** The brokerId → settings view of the instances: what a reader keeps per broker. */
|
|
98303
|
-
function settingsByBroker(rows) {
|
|
98304
|
-
const out = /* @__PURE__ */ new Map();
|
|
98305
|
-
for (const row of rows) if (row.brokerId !== null) out.set(row.brokerId, row.settings);
|
|
98306
|
-
return out;
|
|
98307
|
-
}
|
|
98308
|
-
/** Read the instances, or report why not and answer nothing (a reader falls back to the legacy key). */
|
|
98309
|
-
async function listInstancesOrNothing(ports, onError) {
|
|
98310
|
-
try {
|
|
98311
|
-
return await ports.listInstances();
|
|
98312
|
-
} catch (err) {
|
|
98313
|
-
onError(errMsg(err));
|
|
98314
|
-
return [];
|
|
98315
|
-
}
|
|
98316
|
-
}
|
|
98317
98397
|
//#endregion
|
|
98318
|
-
export {
|
|
98398
|
+
export { fanControlCapability as $, createEvent as $t, bestLocationMatch as A, runningFromMediaPlayer as At, climateControlCapability as B, tryConvertDelta as Bt, accessoriesCapability as C, pressureSensorCapability as Ct, ambientLightSensorCapability as D, resolveAttachmentUrls as Dt, alarmPanelCapability as E, quantityFromUnit as Et, buildAddonRouteProvider as F, smokeCapability as Ft, contactCapability as G, vibrationCapability as Gt, connectivityCapability as H, updateCapability as Ht, buildDeviceAdminUrl as I, statusInstanceOf as It, deviceAdminLinkCapability as J, BaseAddon as Jt, controlCapability as K, waterHeaterCapability as Kt, buttonCapability as L, switchCapability as Lt, boardKeySourceCapability as M, runningStateCapability as Mt, brightnessCapability as N, scriptRunnerCapability as Nt, automationControlCapability as O, runExportedAction as Ot, brokerCapability as P, sendToExportedTarget as Pt, eventEmitterCapability as Q, baseUnitOf as Qt, calendarSourceCapability as R, tamperCapability as Rt, TargetSchema as S, EventCategory as Sn, presenceCapability as St, airQualitySensorCapability as T, publicEndpointReaderFrom as Tt, consumableFormOf as U, vacuumControlCapability as Ut, colorCapability as V, tryConvertUnit as Vt, consumableKindOf as W, valveCapability as Wt, deviceExportCapability as X, RecordSecretUnavailableError as Xt, deviceAdoptionCapability as Y, REDACTED_SECRET as Yt, enumSensorCapability as Z, RevConflictError as Zt, HvacModeSchema as _, url as _n, oauthIntegrationCapability as _t, AlarmArmModeSchema as a, nodePin as an, humiditySensorCapability as at, RUNNING_STATE_COLD as b, DeviceType as bn, preconditionOf as bt, CAMERA_SWITCH_ORDER as c, boolean as cn, integrationInstancesCapability as ct, ConsumableFormSchema as d, number as dn, mediaPlayerCapability as dt, defineAddonMigrations as en, flattenNestedRecords as et, ConsumableItemSchema as f, object as fn, motionCapability as ft, FanDirectionSchema as g, unknown as gn, numericSensorCapability as gt, ExportInstanceUnknownError as h, union as hn, notifierCapability as ht, listHomeAssistantBrokers as i, isReadingQuantity as in, humidifierCapability as it, binaryCapability as j, runningFromVacuumState as jt, batteryCapability as k, runningFromLawnMower as kt, COCO_TO_MACRO as l, discriminatedUnion as ln, lawnMowerControlCapability as lt, EnumSensorDateTimeFormatSchema as m, string as mn, notificationOutputCapability as mt, HA_BROKER_KIND as n, defineRecordKinds as nn, floodCapability as nt, BaseDevice as o, _enum as on, imageCapability as ot, ConsumableStatusSchema as p, record as pn, normalizeUnit as pt, coverCapability as q, weatherCapability as qt, HA_PROVIDER_ADDON_ID$1 as r, entryBody as rn, gasCapability as rt, BaseDeviceProvider as s, array as sn, ingestReading as st, haInstanceSettingsPortsOver as t, defineEntryKind as tn, flattenPortsOver as tt, CameraSwitchIdSchema as u, literal as un, lockControlCapability as ut, MediaPlayerRepeatSchema as v, DeviceFeature as vn, parseAdminBase as vt, addonRoutesCapability as w, pruneDeletedEntities as wt, RecordIntegrationInstances as x, errMsg as xn, prepareNotification as xt, PLUG_APPLIANCE_TYPES as y, DeviceRole as yn, powerMeterCapability as yt, carbonMonoxideCapability as z, temperatureSensorCapability as zt };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-provider-homeassistant",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.656",
|
|
4
4
|
"description": "Home Assistant device provider addon for CamStack",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
|
@@ -134,7 +134,7 @@
|
|
|
134
134
|
"description": "Device provider (import) + native entity export (push). Two addons, one bundle, separate processes."
|
|
135
135
|
},
|
|
136
136
|
"requires": {
|
|
137
|
-
"server": ">=1.2.
|
|
137
|
+
"server": ">=1.2.762"
|
|
138
138
|
}
|
|
139
139
|
},
|
|
140
140
|
"files": [
|