@camstack/addon-google-services 0.2.35 → 0.2.37
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 +12 -0
- package/dist/google-services.addon.js +1820 -567
- package/dist/google-services.addon.mjs +1820 -567
- package/i18n/en.json +4 -4
- package/i18n/it.json +4 -4
- package/package.json +5 -2
|
@@ -789,7 +789,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
789
789
|
return EventCategory;
|
|
790
790
|
}({});
|
|
791
791
|
//#endregion
|
|
792
|
-
//#region ../types/dist/device-type-
|
|
792
|
+
//#region ../types/dist/device-type-D1fH5kaC.mjs
|
|
793
793
|
/**
|
|
794
794
|
import { errMsg } from '@camstack/types'
|
|
795
795
|
* Extract a human-readable message from an unknown error value.
|
|
@@ -978,6 +978,9 @@ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
|
|
|
978
978
|
DeviceFeature["CoverPositionable"] = "cover-positionable";
|
|
979
979
|
/** Cover exposes slat-tilt control. Gates the tilt slider UI. */
|
|
980
980
|
DeviceFeature["CoverTilt"] = "cover-tilt";
|
|
981
|
+
/** Cover has a real stop: `stop` halts the travel in place. Gates the stop
|
|
982
|
+
* control in exporters; a provider whose `stop` is a no-op never declares it. */
|
|
983
|
+
DeviceFeature["CoverStoppable"] = "cover-stoppable";
|
|
981
984
|
/** Valve exposes intermediate position control (0..100). Gates the
|
|
982
985
|
* position slider / drag surface UI. */
|
|
983
986
|
DeviceFeature["ValvePositionable"] = "valve-positionable";
|
|
@@ -1017,6 +1020,12 @@ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
|
|
|
1017
1020
|
DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
|
|
1018
1021
|
/** Media player exposes stop distinct from pause. */
|
|
1019
1022
|
DeviceFeature["MediaPlayerStop"] = "media-player-stop";
|
|
1023
|
+
/** Media player exposes volume up / down in steps (an infrared TV has no level). */
|
|
1024
|
+
DeviceFeature["MediaPlayerVolumeStep"] = "media-player-volume-step";
|
|
1025
|
+
/** Media player exposes channel up / down in steps. */
|
|
1026
|
+
DeviceFeature["MediaPlayerChannelStep"] = "media-player-channel-step";
|
|
1027
|
+
/** Media player exposes power on / off. */
|
|
1028
|
+
DeviceFeature["MediaPlayerPower"] = "media-player-power";
|
|
1020
1029
|
/** Alarm panel requires a PIN code on arm/disarm. */
|
|
1021
1030
|
DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
|
|
1022
1031
|
/** Presence device carries GPS coordinates (lat/lng/accuracy) in
|
|
@@ -5976,7 +5985,7 @@ var ZodIssueCode = {
|
|
|
5976
5985
|
var ZodFirstPartyTypeKind;
|
|
5977
5986
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5978
5987
|
//#endregion
|
|
5979
|
-
//#region ../types/dist/sleep-
|
|
5988
|
+
//#region ../types/dist/sleep-BkzQdpzF.mjs
|
|
5980
5989
|
/**
|
|
5981
5990
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5982
5991
|
* window to float samples (D455).
|
|
@@ -6055,6 +6064,22 @@ function buildAlawTable() {
|
|
|
6055
6064
|
}
|
|
6056
6065
|
buildUlawTable();
|
|
6057
6066
|
buildAlawTable();
|
|
6067
|
+
/**
|
|
6068
|
+
* An addon whose own migrations did not apply refuses to initialise (D987). The error is typed
|
|
6069
|
+
* so a runner hosting several addons can tell "this addon is blocked" from a crash and keep its
|
|
6070
|
+
* co-located siblings running. Recognised by NAME and shape, never `instanceof`: a forked addon
|
|
6071
|
+
* bundles its own copy of this module, so the class the runner sees is not the one that threw.
|
|
6072
|
+
*/
|
|
6073
|
+
var MIGRATIONS_BLOCKED_ERROR_NAME = "AddonMigrationsBlockedError";
|
|
6074
|
+
var AddonMigrationsBlockedError = class extends Error {
|
|
6075
|
+
/** The ledger ids that did not apply and block the owner. */
|
|
6076
|
+
migrationIds;
|
|
6077
|
+
constructor(migrationIds) {
|
|
6078
|
+
super(`migration(s) ${migrationIds.join(", ")} did not apply and block this addon — fix or retry them (migrations.retry), then restart it`);
|
|
6079
|
+
this.name = MIGRATIONS_BLOCKED_ERROR_NAME;
|
|
6080
|
+
this.migrationIds = migrationIds;
|
|
6081
|
+
}
|
|
6082
|
+
};
|
|
6058
6083
|
/** Is this field's stored value a credential? */
|
|
6059
6084
|
function isSecretConfigField(field) {
|
|
6060
6085
|
if (field.type === "password") return true;
|
|
@@ -6108,9 +6133,16 @@ function isSecretMarker(value) {
|
|
|
6108
6133
|
const ref = value["$secret"];
|
|
6109
6134
|
return typeof ref === "string" && ref.length > 0;
|
|
6110
6135
|
}
|
|
6136
|
+
/** Anything carrying a `$secret` key — a stored marker or a UI view of one. */
|
|
6137
|
+
function isSecretMarkerLike(value) {
|
|
6138
|
+
return isRecord$7(value) && Object.prototype.hasOwnProperty.call(value, "$secret");
|
|
6139
|
+
}
|
|
6111
6140
|
function isSecretClear(value) {
|
|
6112
6141
|
return isRecord$7(value) && value["$secretClear"] === true;
|
|
6113
6142
|
}
|
|
6143
|
+
function isSecretFieldView(value) {
|
|
6144
|
+
return isRecord$7(value) && value["set"] === true && isSecretMarkerLike(value);
|
|
6145
|
+
}
|
|
6114
6146
|
/**
|
|
6115
6147
|
* The UI view of a stored secret field value: a marker becomes "set", inline
|
|
6116
6148
|
* plaintext becomes "set (inline)", nothing becomes `null`. A value never
|
|
@@ -6492,6 +6524,53 @@ function emitReadiness(bus, params) {
|
|
|
6492
6524
|
ts
|
|
6493
6525
|
}));
|
|
6494
6526
|
}
|
|
6527
|
+
/** Why a catalogue entry is not a runnable body of its declared kind. */
|
|
6528
|
+
var AddonMigrationDefinitionError = class extends Error {
|
|
6529
|
+
id;
|
|
6530
|
+
constructor(id, message) {
|
|
6531
|
+
super(`${id}: ${message}`);
|
|
6532
|
+
this.id = id;
|
|
6533
|
+
this.name = "AddonMigrationDefinitionError";
|
|
6534
|
+
}
|
|
6535
|
+
};
|
|
6536
|
+
/**
|
|
6537
|
+
* Bind a catalogue to the addon's ports. Throws {@link AddonMigrationDefinitionError} when
|
|
6538
|
+
* an entry's `bodyKind` does not match the body it carries, or is `sql` (a hub-only
|
|
6539
|
+
* body). A throwing body never escapes `execute`: it is reported as a failed result.
|
|
6540
|
+
*/
|
|
6541
|
+
function defineAddonMigrations(catalogue, ports) {
|
|
6542
|
+
return catalogue.map((entry) => {
|
|
6543
|
+
const { meta } = entry;
|
|
6544
|
+
if ("plan" in entry) {
|
|
6545
|
+
if (meta.bodyKind !== "plan") throw new AddonMigrationDefinitionError(meta.id, `carries a plan body but declares "${meta.bodyKind}"`);
|
|
6546
|
+
return {
|
|
6547
|
+
meta,
|
|
6548
|
+
execute: () => guarded(async () => ({
|
|
6549
|
+
kind: "plan",
|
|
6550
|
+
plan: await entry.plan(ports)
|
|
6551
|
+
}))
|
|
6552
|
+
};
|
|
6553
|
+
}
|
|
6554
|
+
if (meta.bodyKind !== "procedure") throw new AddonMigrationDefinitionError(meta.id, `carries a procedure body but declares "${meta.bodyKind}"`);
|
|
6555
|
+
return {
|
|
6556
|
+
meta,
|
|
6557
|
+
execute: () => guarded(async () => ({
|
|
6558
|
+
kind: "procedure",
|
|
6559
|
+
result: await entry.procedure(ports)
|
|
6560
|
+
}))
|
|
6561
|
+
};
|
|
6562
|
+
});
|
|
6563
|
+
}
|
|
6564
|
+
async function guarded(work) {
|
|
6565
|
+
try {
|
|
6566
|
+
return await work();
|
|
6567
|
+
} catch (err) {
|
|
6568
|
+
return {
|
|
6569
|
+
kind: "failed",
|
|
6570
|
+
error: errMsg(err)
|
|
6571
|
+
};
|
|
6572
|
+
}
|
|
6573
|
+
}
|
|
6495
6574
|
var NOT_RUN = {
|
|
6496
6575
|
declared: false,
|
|
6497
6576
|
snapshot: null,
|
|
@@ -6713,11 +6792,36 @@ function projectStore(store, perNodeKeys, nodeId) {
|
|
|
6713
6792
|
}
|
|
6714
6793
|
return out;
|
|
6715
6794
|
}
|
|
6795
|
+
/**
|
|
6796
|
+
* The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
|
|
6797
|
+
*
|
|
6798
|
+
* A ref is DETERMINISTIC from where the value lives — the owner addon, the
|
|
6799
|
+
* scope (global, one node, one device, one integration, one row of a business
|
|
6800
|
+
* collection) and the field path inside that scope — so saving the same field
|
|
6801
|
+
* twice updates the same ref, and the ref itself says which settings blob
|
|
6802
|
+
* points at it. It is also the AAD of the value's ciphertext: a ciphertext
|
|
6803
|
+
* copied onto another ref does not decrypt.
|
|
6804
|
+
*
|
|
6805
|
+
* Array items are addressed by ITEM ID, never by index
|
|
6806
|
+
* (`providers[authentik].clientSecret`), so reordering a list keeps every ref.
|
|
6807
|
+
*
|
|
6808
|
+
* Pure and browser-safe: no node builtins, no I/O.
|
|
6809
|
+
*/
|
|
6810
|
+
/** The URI scheme of every keychain ref. */
|
|
6811
|
+
var SECRET_REF_SCHEME = "secret://";
|
|
6716
6812
|
var GLOBAL_SCOPE = { kind: "global" };
|
|
6717
6813
|
/** The scope every addon-wide (not per-node, not per-device) setting lives in. */
|
|
6718
6814
|
function globalSecretScope() {
|
|
6719
6815
|
return GLOBAL_SCOPE;
|
|
6720
6816
|
}
|
|
6817
|
+
/**
|
|
6818
|
+
* An addon id as a ref owner. A hub in-process builtin's context id is
|
|
6819
|
+
* `addon:<id>`; the owner of its secrets is `<id>`, exactly as for the same
|
|
6820
|
+
* addon running forked.
|
|
6821
|
+
*/
|
|
6822
|
+
function normalizeSecretOwner(addonId) {
|
|
6823
|
+
return addonId.startsWith("addon:") ? addonId.slice(6) : addonId;
|
|
6824
|
+
}
|
|
6721
6825
|
function encodeSegment(value) {
|
|
6722
6826
|
return encodeURIComponent(value);
|
|
6723
6827
|
}
|
|
@@ -6728,6 +6832,16 @@ function decodeSegment(value) {
|
|
|
6728
6832
|
return null;
|
|
6729
6833
|
}
|
|
6730
6834
|
}
|
|
6835
|
+
/** The scope as it appears inside a ref (`global`, `node/<id>`, …). */
|
|
6836
|
+
function formatSecretScope(scope) {
|
|
6837
|
+
switch (scope.kind) {
|
|
6838
|
+
case "global": return "global";
|
|
6839
|
+
case "node": return `node/${encodeSegment(scope.nodeId)}`;
|
|
6840
|
+
case "device": return `device/${scope.deviceId}`;
|
|
6841
|
+
case "integration": return `integration/${encodeSegment(scope.integrationId)}`;
|
|
6842
|
+
case "row": return `row/${encodeSegment(scope.collection)}/${encodeSegment(scope.rowId)}`;
|
|
6843
|
+
}
|
|
6844
|
+
}
|
|
6731
6845
|
/**
|
|
6732
6846
|
* The field path of a value inside an array item, addressed by the item's id:
|
|
6733
6847
|
* `arrayItemFieldPath('providers', 'authentik', 'clientSecret')` →
|
|
@@ -6736,6 +6850,13 @@ function decodeSegment(value) {
|
|
|
6736
6850
|
function arrayItemFieldPath(arrayKey, itemId, fieldKey) {
|
|
6737
6851
|
return `${arrayKey}[${encodeSegment(itemId)}].${fieldKey}`;
|
|
6738
6852
|
}
|
|
6853
|
+
/** Build a ref. Throws on an empty owner or field path — both are bugs. */
|
|
6854
|
+
function buildSecretRef(parts) {
|
|
6855
|
+
const owner = normalizeSecretOwner(parts.owner);
|
|
6856
|
+
if (owner.length === 0) throw new Error("buildSecretRef: owner is empty");
|
|
6857
|
+
if (parts.fieldPath.length === 0) throw new Error("buildSecretRef: fieldPath is empty");
|
|
6858
|
+
return `${SECRET_REF_SCHEME}${encodeSegment(owner)}/${formatSecretScope(parts.scope)}/${parts.fieldPath}`;
|
|
6859
|
+
}
|
|
6739
6860
|
function parseScope(segments) {
|
|
6740
6861
|
switch (segments[0]) {
|
|
6741
6862
|
case "global": return {
|
|
@@ -7661,7 +7782,7 @@ var BaseAddon = class {
|
|
|
7661
7782
|
startMigration: (input) => api.startMigration.mutate(input),
|
|
7662
7783
|
finishMigration: (input) => api.finishMigration.mutate(input)
|
|
7663
7784
|
}, migrations, this.ctx.logger);
|
|
7664
|
-
if (report.blockOwner.length > 0) throw new
|
|
7785
|
+
if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
|
|
7665
7786
|
return report;
|
|
7666
7787
|
}
|
|
7667
7788
|
/**
|
|
@@ -8001,6 +8122,10 @@ var BaseAddon = class {
|
|
|
8001
8122
|
const stored = keychain ? await this.readAddonStoreWithRetry() : {};
|
|
8002
8123
|
const patch = {};
|
|
8003
8124
|
for (const key of keys) {
|
|
8125
|
+
if (tree.get(key)?.kind === "array") {
|
|
8126
|
+
patch[key] = [];
|
|
8127
|
+
continue;
|
|
8128
|
+
}
|
|
8004
8129
|
const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
|
|
8005
8130
|
patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
|
|
8006
8131
|
}
|
|
@@ -8689,6 +8814,8 @@ var RECORD_BATCH_MAX = 1e3;
|
|
|
8689
8814
|
var RECORD_QUERY_MAX = 1e3;
|
|
8690
8815
|
/** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
|
|
8691
8816
|
var RECORD_SETTINGS_MAX_ROWS = 5e4;
|
|
8817
|
+
/** The `Column`-free proof prefix: a ref collection for a kind's secret fields. */
|
|
8818
|
+
var RECORD_SECRET_COLLECTION_PREFIX = "records:";
|
|
8692
8819
|
/**
|
|
8693
8820
|
* Secret fields of a record, addressed the way a Zod schema declares them
|
|
8694
8821
|
* (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
|
|
@@ -8725,6 +8852,112 @@ function secretArrayPrefixes(path) {
|
|
|
8725
8852
|
}
|
|
8726
8853
|
return prefixes;
|
|
8727
8854
|
}
|
|
8855
|
+
function isRecord$4(value) {
|
|
8856
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8857
|
+
}
|
|
8858
|
+
function elementId(element, idKey, declared, prefix) {
|
|
8859
|
+
const id = isRecord$4(element) ? element[idKey] : void 0;
|
|
8860
|
+
if (typeof id !== "string" || id.length === 0) throw new InvalidRecordError(`"${declared}": every element of "${prefix}" needs a non-empty string "${idKey}" to carry a secret`);
|
|
8861
|
+
return id;
|
|
8862
|
+
}
|
|
8863
|
+
/**
|
|
8864
|
+
* Visit every secret leaf PRESENT in `data` (an absent branch has nothing to
|
|
8865
|
+
* visit). `replace` returns the new leaf value, or {@link REMOVE_LEAF}; the
|
|
8866
|
+
* result is a new body, `data` is untouched.
|
|
8867
|
+
*/
|
|
8868
|
+
var REMOVE_LEAF = Symbol("remove-leaf");
|
|
8869
|
+
function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arrayKeys, visit) {
|
|
8870
|
+
if (!isRecord$4(node)) return node;
|
|
8871
|
+
const segment = segments[index];
|
|
8872
|
+
if (segment === void 0) return node;
|
|
8873
|
+
if (!Object.prototype.hasOwnProperty.call(node, segment.key)) return node;
|
|
8874
|
+
const child = node[segment.key];
|
|
8875
|
+
const keys = [...walkedKeys, segment.key];
|
|
8876
|
+
const last = index === segments.length - 1;
|
|
8877
|
+
if (segment.each) {
|
|
8878
|
+
if (!Array.isArray(child)) return node;
|
|
8879
|
+
const prefix = keys.join(".");
|
|
8880
|
+
const idKey = arrayKeys[prefix];
|
|
8881
|
+
if (idKey === void 0) throw new InvalidRecordError(`"${declared}": no id key declared for the array "${prefix}"`);
|
|
8882
|
+
const mapped = child.map((element) => {
|
|
8883
|
+
const id = elementId(element, idKey, declared, prefix);
|
|
8884
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}[${encodeURIComponent(id)}]`;
|
|
8885
|
+
return last ? element : mapAt(element, segments, index + 1, keys, concrete, declared, arrayKeys, visit);
|
|
8886
|
+
});
|
|
8887
|
+
return {
|
|
8888
|
+
...node,
|
|
8889
|
+
[segment.key]: mapped
|
|
8890
|
+
};
|
|
8891
|
+
}
|
|
8892
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
|
|
8893
|
+
if (last) {
|
|
8894
|
+
const replacement = visit({
|
|
8895
|
+
declared,
|
|
8896
|
+
concrete,
|
|
8897
|
+
value: child
|
|
8898
|
+
});
|
|
8899
|
+
if (replacement === REMOVE_LEAF) return Object.fromEntries(Object.entries(node).filter(([key]) => key !== segment.key));
|
|
8900
|
+
return {
|
|
8901
|
+
...node,
|
|
8902
|
+
[segment.key]: replacement
|
|
8903
|
+
};
|
|
8904
|
+
}
|
|
8905
|
+
return {
|
|
8906
|
+
...node,
|
|
8907
|
+
[segment.key]: mapAt(child, segments, index + 1, keys, concrete, declared, arrayKeys, visit)
|
|
8908
|
+
};
|
|
8909
|
+
}
|
|
8910
|
+
/** Replace/remove every present secret leaf; returns a NEW body. */
|
|
8911
|
+
function mapSecretLeaves(data, paths, arrayKeys, visit) {
|
|
8912
|
+
let current = data;
|
|
8913
|
+
for (const declared of paths) current = mapAt(current, parseSecretPath(declared), 0, [], "", declared, arrayKeys, visit);
|
|
8914
|
+
return current;
|
|
8915
|
+
}
|
|
8916
|
+
/**
|
|
8917
|
+
* The keychain collection name a kind's secrets live under. Prefixed so a
|
|
8918
|
+
* record's refs never collide with the refs a legacy collection of the same name
|
|
8919
|
+
* already holds during an adoption.
|
|
8920
|
+
*/
|
|
8921
|
+
function recordSecretCollection(kind) {
|
|
8922
|
+
return `${RECORD_SECRET_COLLECTION_PREFIX}${kind}`;
|
|
8923
|
+
}
|
|
8924
|
+
/** The ONE ref a row's secret field may carry: the engine and the facade both build it here. */
|
|
8925
|
+
function recordSecretRef(owner, kind, rowId, concretePath) {
|
|
8926
|
+
return buildSecretRef({
|
|
8927
|
+
owner,
|
|
8928
|
+
scope: {
|
|
8929
|
+
kind: "row",
|
|
8930
|
+
collection: recordSecretCollection(kind),
|
|
8931
|
+
rowId
|
|
8932
|
+
},
|
|
8933
|
+
fieldPath: concretePath
|
|
8934
|
+
});
|
|
8935
|
+
}
|
|
8936
|
+
/**
|
|
8937
|
+
* A stored secret, as an admin read shows it: a VIEW naming the row's own
|
|
8938
|
+
* keychain ref (`set: true`), never the value and no longer the bare redaction
|
|
8939
|
+
* token. The form hands it to the secret field, which reveals the value by ref
|
|
8940
|
+
* through the audited `keychain-admin.reveal`; sent back untouched it keeps the
|
|
8941
|
+
* stored secret (the records door reads a view as KEEP).
|
|
8942
|
+
*
|
|
8943
|
+
* ONE implementation for the entries service and for every addon that serves
|
|
8944
|
+
* its own instances (`RecordIntegrationInstances`).
|
|
8945
|
+
*/
|
|
8946
|
+
function isRecord$3(value) {
|
|
8947
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8948
|
+
}
|
|
8949
|
+
function secretViewsOf(owner, kind, id, decl, data) {
|
|
8950
|
+
if (decl.secretPaths.length === 0) return data;
|
|
8951
|
+
const shown = mapSecretLeaves(data, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
|
|
8952
|
+
if (leaf.value !== "__camstack_redacted__") return leaf.value;
|
|
8953
|
+
return {
|
|
8954
|
+
$secret: recordSecretRef(owner, kind, id, leaf.concrete),
|
|
8955
|
+
v: 1,
|
|
8956
|
+
set: true
|
|
8957
|
+
};
|
|
8958
|
+
});
|
|
8959
|
+
return isRecord$3(shown) ? shown : data;
|
|
8960
|
+
}
|
|
8728
8961
|
/**
|
|
8729
8962
|
* The share-view scope kinds a share-link token can carry. The hub mints
|
|
8730
8963
|
* and validates them (`ShareViewGrantSchema` builds its
|
|
@@ -8947,7 +9180,7 @@ function defineRecordKinds(specs) {
|
|
|
8947
9180
|
retention: spec.retention,
|
|
8948
9181
|
deviceRemoval: spec.deviceRemoval,
|
|
8949
9182
|
maxDataBytes: spec.maxDataBytes,
|
|
8950
|
-
secretPaths: [...secretPaths],
|
|
9183
|
+
secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
|
|
8951
9184
|
secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
|
|
8952
9185
|
transactFamily: spec.transactFamily,
|
|
8953
9186
|
entry: spec.entry === void 0 ? void 0 : {
|
|
@@ -8992,6 +9225,7 @@ function defineEntryKind(name, spec) {
|
|
|
8992
9225
|
...spec.database === void 0 ? {} : { database: spec.database },
|
|
8993
9226
|
...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
|
|
8994
9227
|
...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
|
|
9228
|
+
...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
|
|
8995
9229
|
entry: {
|
|
8996
9230
|
title: spec.title,
|
|
8997
9231
|
...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
|
|
@@ -9652,6 +9886,7 @@ var CAP_NAMES = Object.freeze([
|
|
|
9652
9886
|
"backup",
|
|
9653
9887
|
"battery",
|
|
9654
9888
|
"binary",
|
|
9889
|
+
"board-key-source",
|
|
9655
9890
|
"brightness",
|
|
9656
9891
|
"broker",
|
|
9657
9892
|
"button",
|
|
@@ -9773,6 +10008,7 @@ var CAP_NAMES = Object.freeze([
|
|
|
9773
10008
|
"privacy-mask",
|
|
9774
10009
|
"ptz",
|
|
9775
10010
|
"ptz-autotrack",
|
|
10011
|
+
"pump-cycle",
|
|
9776
10012
|
"reboot",
|
|
9777
10013
|
"record-entries",
|
|
9778
10014
|
"recording",
|
|
@@ -11065,18 +11301,6 @@ Object.freeze({
|
|
|
11065
11301
|
"path": "pumpRunning",
|
|
11066
11302
|
"valueType": "boolean"
|
|
11067
11303
|
},
|
|
11068
|
-
{
|
|
11069
|
-
"path": "pumpIntervalEnabled",
|
|
11070
|
-
"valueType": "boolean"
|
|
11071
|
-
},
|
|
11072
|
-
{
|
|
11073
|
-
"path": "pumpOnTime",
|
|
11074
|
-
"valueType": "number"
|
|
11075
|
-
},
|
|
11076
|
-
{
|
|
11077
|
-
"path": "pumpOffTime",
|
|
11078
|
-
"valueType": "number"
|
|
11079
|
-
},
|
|
11080
11304
|
{
|
|
11081
11305
|
"path": "lastDrink",
|
|
11082
11306
|
"valueType": "number"
|
|
@@ -11200,6 +11424,20 @@ Object.freeze({
|
|
|
11200
11424
|
"valueType": "number"
|
|
11201
11425
|
}
|
|
11202
11426
|
],
|
|
11427
|
+
"pump-cycle": [
|
|
11428
|
+
{
|
|
11429
|
+
"path": "enabled",
|
|
11430
|
+
"valueType": "boolean"
|
|
11431
|
+
},
|
|
11432
|
+
{
|
|
11433
|
+
"path": "onTime",
|
|
11434
|
+
"valueType": "number"
|
|
11435
|
+
},
|
|
11436
|
+
{
|
|
11437
|
+
"path": "offTime",
|
|
11438
|
+
"valueType": "number"
|
|
11439
|
+
}
|
|
11440
|
+
],
|
|
11203
11441
|
"recording-archive": [
|
|
11204
11442
|
{
|
|
11205
11443
|
"path": "deviceId",
|
|
@@ -11663,7 +11901,7 @@ function userError(code, params, englishFallback) {
|
|
|
11663
11901
|
return new CamstackError(code, params, englishFallback);
|
|
11664
11902
|
}
|
|
11665
11903
|
//#endregion
|
|
11666
|
-
//#region ../types/dist/composition-
|
|
11904
|
+
//#region ../types/dist/composition-CRNwcFYY.mjs
|
|
11667
11905
|
/**
|
|
11668
11906
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
11669
11907
|
*
|
|
@@ -12426,38 +12664,6 @@ function validateExpressionSource(src) {
|
|
|
12426
12664
|
return null;
|
|
12427
12665
|
}
|
|
12428
12666
|
/**
|
|
12429
|
-
* One power reading becomes a washer, dryer or dishwasher (D944 helper).
|
|
12430
|
-
*
|
|
12431
|
-
* Standby of the control board is under 5 W; a motor or a heater is far
|
|
12432
|
-
* above that, so 5 W is the line. A door lock or a short pump spike is not
|
|
12433
|
-
* a cycle, so running starts only after 30 s above the line.
|
|
12434
|
-
*
|
|
12435
|
-
* A wash, dry or dishwasher pause (soak, heat-wait, heater off between
|
|
12436
|
-
* bursts) lasts several minutes. Ending the cycle on the first dip would
|
|
12437
|
-
* report "finished" in the middle of the program, so the stop dwell is
|
|
12438
|
-
* 10 minutes on every type. The number is stored per type so one machine
|
|
12439
|
-
* can move later without moving the others.
|
|
12440
|
-
*/
|
|
12441
|
-
var APPLIANCE_TEMPLATE_ID = "appliance";
|
|
12442
|
-
var APPLIANCE_KINDS = [
|
|
12443
|
-
"washer",
|
|
12444
|
-
"dryer",
|
|
12445
|
-
"dishwasher"
|
|
12446
|
-
];
|
|
12447
|
-
DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
|
|
12448
|
-
/**
|
|
12449
|
-
* A dumb plug-in stove or heater, built from a smart plug (a `Heater`).
|
|
12450
|
-
*
|
|
12451
|
-
* The plug is the actuator: `switch.setState` is a FORWARD to the plug's own
|
|
12452
|
-
* (D967), so the heater switches exactly as the plug does. Running is the
|
|
12453
|
-
* plug being on; when a power reading is also named, running is the draw
|
|
12454
|
-
* staying above the line instead (a plug left on with the stove's own
|
|
12455
|
-
* thermostat open is not heating). 50 W is far above any standby and below
|
|
12456
|
-
* the smallest radiator.
|
|
12457
|
-
*/
|
|
12458
|
-
var HEATER_TEMPLATE_ID = "heater";
|
|
12459
|
-
DeviceType.Heater;
|
|
12460
|
-
/**
|
|
12461
12667
|
* Statistics and integrals over a history read (D944).
|
|
12462
12668
|
*
|
|
12463
12669
|
* Pure: the same points always give the same answer, so a restart recomputes
|
|
@@ -12601,34 +12807,6 @@ var HelperSpecSchema = discriminatedUnion("kind", [
|
|
|
12601
12807
|
/** Actuator groups only. Readings leave this unset. */
|
|
12602
12808
|
active: ActiveCriterionSchema.optional()
|
|
12603
12809
|
}),
|
|
12604
|
-
object({
|
|
12605
|
-
kind: literal("appliance"),
|
|
12606
|
-
templateId: literal(APPLIANCE_TEMPLATE_ID),
|
|
12607
|
-
appliance: _enum(APPLIANCE_KINDS),
|
|
12608
|
-
thresholdWatts: number().positive(),
|
|
12609
|
-
startDwellMs: number().int().nonnegative(),
|
|
12610
|
-
stopDwellMs: number().int().nonnegative(),
|
|
12611
|
-
member: HelperMemberSchema,
|
|
12612
|
-
/**
|
|
12613
|
-
* The plug that powers the appliance (`switch.on`). Optional: when set, the
|
|
12614
|
-
* composition carries a `power-source` feature, so the hub can cut and restore
|
|
12615
|
-
* that plug (power-source link of a NEW-target composition).
|
|
12616
|
-
*/
|
|
12617
|
-
powerSwitch: HelperMemberSchema.optional(),
|
|
12618
|
-
/** How long a forced restart keeps the plug off; the cap's default when unset. */
|
|
12619
|
-
offSeconds: number().optional()
|
|
12620
|
-
}),
|
|
12621
|
-
object({
|
|
12622
|
-
kind: literal("heater"),
|
|
12623
|
-
templateId: literal(HEATER_TEMPLATE_ID),
|
|
12624
|
-
/** The plug, read and commanded at `switch.on`. */
|
|
12625
|
-
plug: HelperMemberSchema,
|
|
12626
|
-
/** The plug's power reading, when it has one: running is then the draw, not the switch. */
|
|
12627
|
-
meter: HelperMemberSchema.optional(),
|
|
12628
|
-
thresholdWatts: number().positive(),
|
|
12629
|
-
startDwellMs: number().int().nonnegative(),
|
|
12630
|
-
stopDwellMs: number().int().nonnegative()
|
|
12631
|
-
}),
|
|
12632
12810
|
object({
|
|
12633
12811
|
kind: literal("statistic"),
|
|
12634
12812
|
stat: _enum(STATISTIC_STATS),
|
|
@@ -12800,7 +12978,14 @@ var CompositionForwardCommandSchema = object({
|
|
|
12800
12978
|
method: string().min(1),
|
|
12801
12979
|
args: record(string().min(1), CompositionForwardArgSchema).optional()
|
|
12802
12980
|
});
|
|
12803
|
-
var CompositionCommandTargetSchema = discriminatedUnion("kind", [
|
|
12981
|
+
var CompositionCommandTargetSchema = discriminatedUnion("kind", [
|
|
12982
|
+
CompositionForwardCommandSchema,
|
|
12983
|
+
CompositionCodeSourceSchema,
|
|
12984
|
+
object({
|
|
12985
|
+
kind: literal("extension"),
|
|
12986
|
+
target: literal("remote")
|
|
12987
|
+
})
|
|
12988
|
+
]);
|
|
12804
12989
|
/** An item key inside an item-array cap (`consumables.items[<key>]`). */
|
|
12805
12990
|
var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
|
|
12806
12991
|
/**
|
|
@@ -12845,7 +13030,7 @@ var CompositionSchema = object({
|
|
|
12845
13030
|
kind: literal("existing"),
|
|
12846
13031
|
device: CompositionSourceRefSchema
|
|
12847
13032
|
})]),
|
|
12848
|
-
features: array(CompositionFeatureSchema).min(
|
|
13033
|
+
features: array(CompositionFeatureSchema).min(0).max(16),
|
|
12849
13034
|
/**
|
|
12850
13035
|
* Present when the block is a helper template. Absent on a free
|
|
12851
13036
|
* composition (the room example, device 4603). The editor reads this,
|
|
@@ -12877,6 +13062,73 @@ var NodeColorIdSchema = _enum([
|
|
|
12877
13062
|
"node-g",
|
|
12878
13063
|
"node-h"
|
|
12879
13064
|
]);
|
|
13065
|
+
//#endregion
|
|
13066
|
+
//#region ../types/dist/index.mjs
|
|
13067
|
+
var SMTP_INBOUND_DEFAULT_PORT = 2525;
|
|
13068
|
+
var SMTP_INBOUND_DEFAULT_DOMAIN = "camstack.local";
|
|
13069
|
+
var SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES = 25 * 1024 * 1024;
|
|
13070
|
+
/** The text excerpt a message carries on the bus. */
|
|
13071
|
+
var SMTP_INBOUND_TEXT_MAX_CHARS = 4096;
|
|
13072
|
+
var PortSchema = number().int().min(1).max(65535);
|
|
13073
|
+
/** A recipient local-part: what stands before the `@`. */
|
|
13074
|
+
var SmtpRecipientSchema = string().min(1).max(64).regex(/^[A-Za-z0-9._+-]+$/);
|
|
13075
|
+
object({
|
|
13076
|
+
port: PortSchema.default(SMTP_INBOUND_DEFAULT_PORT),
|
|
13077
|
+
bindHost: string().min(1).default("0.0.0.0"),
|
|
13078
|
+
domain: string().min(1).default(SMTP_INBOUND_DEFAULT_DOMAIN),
|
|
13079
|
+
requireAuth: boolean().default(true),
|
|
13080
|
+
username: string().default(""),
|
|
13081
|
+
password: string().default("").meta({ secret: true }),
|
|
13082
|
+
starttls: boolean().default(false),
|
|
13083
|
+
maxMessageBytes: number().int().positive().default(SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES)
|
|
13084
|
+
});
|
|
13085
|
+
object({
|
|
13086
|
+
host: string().min(1).nullable(),
|
|
13087
|
+
hostReason: string().nullable(),
|
|
13088
|
+
port: PortSchema,
|
|
13089
|
+
domain: string().min(1),
|
|
13090
|
+
requireAuth: boolean(),
|
|
13091
|
+
username: string(),
|
|
13092
|
+
password: string(),
|
|
13093
|
+
starttls: boolean()
|
|
13094
|
+
});
|
|
13095
|
+
object({
|
|
13096
|
+
recipients: array(SmtpRecipientSchema).min(1),
|
|
13097
|
+
/**
|
|
13098
|
+
* Who is subscribing. A second `subscribe` with the same id REPLACES that subscriber's
|
|
13099
|
+
* recipients (the way a consumer that restarted, or lost a device, declares its set again);
|
|
13100
|
+
* a recipient held by another subscriber is refused with `smtp-inbound/recipient-claimed`.
|
|
13101
|
+
* Absent, the subscriber is anonymous and unique.
|
|
13102
|
+
*/
|
|
13103
|
+
subscriberId: string().min(1).max(64).optional()
|
|
13104
|
+
});
|
|
13105
|
+
var SmtpInboundAttachmentSchema = object({
|
|
13106
|
+
filename: string(),
|
|
13107
|
+
contentType: string(),
|
|
13108
|
+
bytes: number().int().nonnegative()
|
|
13109
|
+
});
|
|
13110
|
+
/** The payload of one `broker.message` (the key is the recipient local-part). */
|
|
13111
|
+
var SmtpInboundMessageSchema = object({
|
|
13112
|
+
/** Monotonic per mailbox; a gap tells a consumer it missed an event. */
|
|
13113
|
+
seq: number().int().positive(),
|
|
13114
|
+
messageId: string(),
|
|
13115
|
+
receivedAt: number().int().nonnegative(),
|
|
13116
|
+
from: string(),
|
|
13117
|
+
to: string(),
|
|
13118
|
+
subject: string(),
|
|
13119
|
+
text: string().max(SMTP_INBOUND_TEXT_MAX_CHARS),
|
|
13120
|
+
attachments: array(SmtpInboundAttachmentSchema)
|
|
13121
|
+
});
|
|
13122
|
+
object({
|
|
13123
|
+
/** The newest seq issued so far (0 before any mail). */
|
|
13124
|
+
lastSeq: number().int().nonnegative(),
|
|
13125
|
+
/** The oldest seq still retained; messages below it are gone. */
|
|
13126
|
+
oldestSeq: number().int().nonnegative(),
|
|
13127
|
+
messages: array(object({
|
|
13128
|
+
key: string(),
|
|
13129
|
+
message: SmtpInboundMessageSchema
|
|
13130
|
+
}))
|
|
13131
|
+
});
|
|
12880
13132
|
new Set([
|
|
12881
13133
|
"track",
|
|
12882
13134
|
"face",
|
|
@@ -13113,6 +13365,157 @@ function buildCapturingReply(envelope) {
|
|
|
13113
13365
|
};
|
|
13114
13366
|
return wrapper;
|
|
13115
13367
|
}
|
|
13368
|
+
/** The most instances one addon lists (an integrations page, not a table). */
|
|
13369
|
+
var INSTANCES_LIST_MAX = 1e3;
|
|
13370
|
+
/** The keys of the kind's secret fields that sit at the top level of the body. */
|
|
13371
|
+
function topLevelSecretKeys(paths) {
|
|
13372
|
+
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[")));
|
|
13373
|
+
}
|
|
13374
|
+
var RecordIntegrationInstances = class {
|
|
13375
|
+
records;
|
|
13376
|
+
kind;
|
|
13377
|
+
hooks;
|
|
13378
|
+
handle;
|
|
13379
|
+
secretKeys;
|
|
13380
|
+
nameField;
|
|
13381
|
+
constructor(records, kind, hooks) {
|
|
13382
|
+
this.records = records;
|
|
13383
|
+
this.kind = kind;
|
|
13384
|
+
this.hooks = hooks;
|
|
13385
|
+
this.handle = records.kind(kind);
|
|
13386
|
+
this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
|
|
13387
|
+
this.nameField = kind.declaration.entry?.nameField ?? "name";
|
|
13388
|
+
}
|
|
13389
|
+
/**
|
|
13390
|
+
* One instance read for the addon's own runtime; null when absent. When the keychain is
|
|
13391
|
+
* locked the row is served WITHOUT its secrets and says so (`secrets: 'unavailable'`), so the
|
|
13392
|
+
* caller can keep what it had instead of reconfiguring with an empty secret (D49).
|
|
13393
|
+
*/
|
|
13394
|
+
async readResolved(id) {
|
|
13395
|
+
try {
|
|
13396
|
+
const record = await this.handle.getResolved(id);
|
|
13397
|
+
return record === null ? null : {
|
|
13398
|
+
id,
|
|
13399
|
+
body: record.data,
|
|
13400
|
+
enabled: record.data.enabled !== false,
|
|
13401
|
+
secrets: "resolved"
|
|
13402
|
+
};
|
|
13403
|
+
} catch (err) {
|
|
13404
|
+
if (!(err instanceof RecordSecretUnavailableError)) throw err;
|
|
13405
|
+
const stored = await this.handle.get(id);
|
|
13406
|
+
if (stored === null) return null;
|
|
13407
|
+
return {
|
|
13408
|
+
id,
|
|
13409
|
+
body: Object.fromEntries(Object.entries(stored.data).filter(([key]) => !this.secretKeys.has(key))),
|
|
13410
|
+
enabled: stored.data.enabled !== false,
|
|
13411
|
+
secrets: "unavailable"
|
|
13412
|
+
};
|
|
13413
|
+
}
|
|
13414
|
+
}
|
|
13415
|
+
/** The body of {@link readResolved} alone; a locked keychain yields the row without secrets. */
|
|
13416
|
+
async getResolved(id) {
|
|
13417
|
+
return (await this.readResolved(id))?.body ?? null;
|
|
13418
|
+
}
|
|
13419
|
+
/** Every instance, secrets resolved where the keychain allows; an absent one is skipped. */
|
|
13420
|
+
async listResolved() {
|
|
13421
|
+
const page = await this.handle.query({ limit: INSTANCES_LIST_MAX });
|
|
13422
|
+
const out = [];
|
|
13423
|
+
for (const record of page.records) {
|
|
13424
|
+
const read = await this.readResolved(record.id);
|
|
13425
|
+
if (read !== null) out.push(read);
|
|
13426
|
+
}
|
|
13427
|
+
return out;
|
|
13428
|
+
}
|
|
13429
|
+
viewOf(record) {
|
|
13430
|
+
const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
|
|
13431
|
+
const rawName = record.data[this.nameField];
|
|
13432
|
+
const kind = this.hooks.kindOf?.(record.data);
|
|
13433
|
+
const health = this.hooks.health?.(record.data, record.id);
|
|
13434
|
+
return {
|
|
13435
|
+
id: record.id,
|
|
13436
|
+
name: typeof rawName === "string" ? rawName : record.id,
|
|
13437
|
+
configured: this.hooks.configured(record.data),
|
|
13438
|
+
enabled: record.data.enabled !== false,
|
|
13439
|
+
formValues: shown,
|
|
13440
|
+
...kind !== void 0 ? { kind } : {},
|
|
13441
|
+
...health?.status !== void 0 ? { status: health.status } : {},
|
|
13442
|
+
...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
|
|
13443
|
+
};
|
|
13444
|
+
}
|
|
13445
|
+
describe = async () => this.hooks.mintId === void 0 ? this.hooks.description : {
|
|
13446
|
+
...this.hooks.description,
|
|
13447
|
+
mintsId: true
|
|
13448
|
+
};
|
|
13449
|
+
instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
|
|
13450
|
+
listInstances = async () => {
|
|
13451
|
+
return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
|
|
13452
|
+
};
|
|
13453
|
+
getInstance = async ({ id }) => {
|
|
13454
|
+
const record = await this.handle.get(id);
|
|
13455
|
+
return record === null ? null : this.viewOf(record);
|
|
13456
|
+
};
|
|
13457
|
+
/** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
|
|
13458
|
+
createInstance = async ({ id, name, kind }) => {
|
|
13459
|
+
const minted = id ?? this.hooks.mintId?.({
|
|
13460
|
+
name,
|
|
13461
|
+
...kind !== void 0 ? { kind } : {}
|
|
13462
|
+
});
|
|
13463
|
+
if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
|
|
13464
|
+
await this.handle.put({
|
|
13465
|
+
id: minted,
|
|
13466
|
+
data: this.hooks.initialBody(name, kind)
|
|
13467
|
+
}, { ifRev: 0 });
|
|
13468
|
+
await this.hooks.onChanged?.(minted);
|
|
13469
|
+
return { id: minted };
|
|
13470
|
+
};
|
|
13471
|
+
updateInstance = async ({ id, name, values }) => {
|
|
13472
|
+
const patch = {};
|
|
13473
|
+
for (const [key, value] of Object.entries(values)) {
|
|
13474
|
+
if (value === void 0) continue;
|
|
13475
|
+
if (!this.secretKeys.has(key)) patch[key] = value;
|
|
13476
|
+
else if (isSecretClear(value)) patch[key] = null;
|
|
13477
|
+
else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
|
|
13478
|
+
}
|
|
13479
|
+
if (name !== void 0) patch[this.nameField] = name;
|
|
13480
|
+
await this.handle.patch(id, patch);
|
|
13481
|
+
await this.hooks.onChanged?.(id);
|
|
13482
|
+
return { ok: true };
|
|
13483
|
+
};
|
|
13484
|
+
/** The switch lives in the record body; the addon is told so it applies it (D996). */
|
|
13485
|
+
setInstanceEnabled = async ({ id, enabled }) => {
|
|
13486
|
+
await this.handle.patch(id, { enabled });
|
|
13487
|
+
await this.hooks.onChanged?.(id);
|
|
13488
|
+
return { ok: true };
|
|
13489
|
+
};
|
|
13490
|
+
/** The row and its keychain refs go together (the records door). */
|
|
13491
|
+
deleteInstance = async ({ id }) => {
|
|
13492
|
+
await this.handle.delete(id);
|
|
13493
|
+
await this.hooks.onChanged?.(id);
|
|
13494
|
+
return { ok: true };
|
|
13495
|
+
};
|
|
13496
|
+
testInstance = async (input) => {
|
|
13497
|
+
if (this.hooks.test === void 0) return {
|
|
13498
|
+
success: false,
|
|
13499
|
+
status: "unsupported",
|
|
13500
|
+
error: "This integration declares no connection test"
|
|
13501
|
+
};
|
|
13502
|
+
return this.hooks.test(input);
|
|
13503
|
+
};
|
|
13504
|
+
/** The flat provider object to hand to `registerProvider`. */
|
|
13505
|
+
toProvider() {
|
|
13506
|
+
return {
|
|
13507
|
+
describe: this.describe,
|
|
13508
|
+
instanceFormSchema: this.instanceFormSchema,
|
|
13509
|
+
listInstances: this.listInstances,
|
|
13510
|
+
getInstance: this.getInstance,
|
|
13511
|
+
createInstance: this.createInstance,
|
|
13512
|
+
updateInstance: this.updateInstance,
|
|
13513
|
+
setInstanceEnabled: this.setInstanceEnabled,
|
|
13514
|
+
deleteInstance: this.deleteInstance,
|
|
13515
|
+
testInstance: this.testInstance
|
|
13516
|
+
};
|
|
13517
|
+
}
|
|
13518
|
+
};
|
|
13116
13519
|
var EncodeProfileSchema = object({
|
|
13117
13520
|
video: object({
|
|
13118
13521
|
codec: _enum([
|
|
@@ -14088,6 +14491,16 @@ var MigrationPlanSchema = object({ edits: array(discriminatedUnion("op", [
|
|
|
14088
14491
|
precondition: PreconditionSchema
|
|
14089
14492
|
})
|
|
14090
14493
|
])) });
|
|
14494
|
+
/** `JSON.stringify` with object keys sorted, so equal values spell the same. */
|
|
14495
|
+
function canonicalJson(value) {
|
|
14496
|
+
if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
|
|
14497
|
+
if (typeof value === "object" && value !== null) return `{${Object.entries(value).filter(([, item]) => item !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`).join(",")}}`;
|
|
14498
|
+
return JSON.stringify(value) ?? "null";
|
|
14499
|
+
}
|
|
14500
|
+
/** The precondition string for a value a plan read (`null` when it read nothing). */
|
|
14501
|
+
function preconditionOf(value) {
|
|
14502
|
+
return canonicalJson(value ?? null);
|
|
14503
|
+
}
|
|
14091
14504
|
/**
|
|
14092
14505
|
* What a `procedure` body reports. `already-done` means the rerun observed the
|
|
14093
14506
|
* work finished (a crash between the work and the ledger row, or a manual fix)
|
|
@@ -19266,24 +19679,52 @@ var APPLIANCE_CAP_VOCABULARY = {
|
|
|
19266
19679
|
args: { on: false }
|
|
19267
19680
|
},
|
|
19268
19681
|
{
|
|
19269
|
-
id: "petFountain.
|
|
19270
|
-
method: "
|
|
19682
|
+
id: "petFountain.resetDailyConsumption",
|
|
19683
|
+
method: "resetDailyConsumption",
|
|
19684
|
+
label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
|
|
19685
|
+
phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
|
|
19686
|
+
icon: "rotate-ccw"
|
|
19687
|
+
}
|
|
19688
|
+
],
|
|
19689
|
+
state: [{
|
|
19690
|
+
kind: "boolean",
|
|
19691
|
+
field: "empty",
|
|
19692
|
+
label: t("Tank empty", "Serbatoio vuoto"),
|
|
19693
|
+
whenTrue: t("When the tank of {device} is empty", "Quando il serbatoio di {device} è vuoto"),
|
|
19694
|
+
whenFalse: t("When the tank of {device} is refilled", "Quando il serbatoio di {device} viene riempito"),
|
|
19695
|
+
icon: "droplet"
|
|
19696
|
+
}, {
|
|
19697
|
+
kind: "numeric",
|
|
19698
|
+
field: "waterLevel",
|
|
19699
|
+
quantity: t("the water level", "il livello dell’acqua"),
|
|
19700
|
+
label: t("Water level", "Livello dell’acqua"),
|
|
19701
|
+
unit: "%",
|
|
19702
|
+
icon: "droplets"
|
|
19703
|
+
}]
|
|
19704
|
+
},
|
|
19705
|
+
"pump-cycle": {
|
|
19706
|
+
scope: "device",
|
|
19707
|
+
noun: t("Pump cycle", "Ciclo della pompa"),
|
|
19708
|
+
actions: [
|
|
19709
|
+
{
|
|
19710
|
+
id: "pumpCycle.on",
|
|
19711
|
+
method: "setEnabled",
|
|
19271
19712
|
label: t("Pump cycle on", "Ciclo pompa attivo"),
|
|
19272
19713
|
phrase: t("Run the pump of {device} on its on/off cycle", "Fai lavorare la pompa di {device} a ciclo acceso/spento"),
|
|
19273
19714
|
icon: "timer",
|
|
19274
19715
|
args: { on: true }
|
|
19275
19716
|
},
|
|
19276
19717
|
{
|
|
19277
|
-
id: "
|
|
19278
|
-
method: "
|
|
19718
|
+
id: "pumpCycle.off",
|
|
19719
|
+
method: "setEnabled",
|
|
19279
19720
|
label: t("Pump cycle off", "Ciclo pompa spento"),
|
|
19280
19721
|
phrase: t("Run the pump of {device} continuously", "Fai lavorare la pompa di {device} in continuo"),
|
|
19281
19722
|
icon: "timer-off",
|
|
19282
19723
|
args: { on: false }
|
|
19283
19724
|
},
|
|
19284
19725
|
{
|
|
19285
|
-
id: "
|
|
19286
|
-
method: "
|
|
19726
|
+
id: "pumpCycle.onMinutes",
|
|
19727
|
+
method: "setOnMinutes",
|
|
19287
19728
|
label: t("Set pump run time", "Imposta il tempo di lavoro della pompa"),
|
|
19288
19729
|
phrase: t("Set the pump of {device} to run {minutes} minutes per cycle", "Imposta la pompa di {device} a lavorare {minutes} minuti per ciclo"),
|
|
19289
19730
|
icon: "timer",
|
|
@@ -19299,8 +19740,8 @@ var APPLIANCE_CAP_VOCABULARY = {
|
|
|
19299
19740
|
}]
|
|
19300
19741
|
},
|
|
19301
19742
|
{
|
|
19302
|
-
id: "
|
|
19303
|
-
method: "
|
|
19743
|
+
id: "pumpCycle.offMinutes",
|
|
19744
|
+
method: "setOffMinutes",
|
|
19304
19745
|
label: t("Set pump rest time", "Imposta il tempo di riposo della pompa"),
|
|
19305
19746
|
phrase: t("Set the pump of {device} to rest {minutes} minutes per cycle", "Imposta la pompa di {device} a riposare {minutes} minuti per ciclo"),
|
|
19306
19747
|
icon: "timer-off",
|
|
@@ -19314,35 +19755,63 @@ var APPLIANCE_CAP_VOCABULARY = {
|
|
|
19314
19755
|
unit: "min"
|
|
19315
19756
|
}
|
|
19316
19757
|
}]
|
|
19317
|
-
},
|
|
19318
|
-
{
|
|
19319
|
-
id: "petFountain.resetDailyConsumption",
|
|
19320
|
-
method: "resetDailyConsumption",
|
|
19321
|
-
label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
|
|
19322
|
-
phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
|
|
19323
|
-
icon: "rotate-ccw"
|
|
19324
19758
|
}
|
|
19325
19759
|
],
|
|
19326
19760
|
state: [{
|
|
19327
19761
|
kind: "boolean",
|
|
19328
|
-
field: "
|
|
19329
|
-
label: t("
|
|
19330
|
-
whenTrue: t("When
|
|
19331
|
-
whenFalse: t("When
|
|
19332
|
-
icon: "
|
|
19333
|
-
}, {
|
|
19334
|
-
kind: "numeric",
|
|
19335
|
-
field: "waterLevel",
|
|
19336
|
-
quantity: t("the water level", "il livello dell’acqua"),
|
|
19337
|
-
label: t("Water level", "Livello dell’acqua"),
|
|
19338
|
-
unit: "%",
|
|
19339
|
-
icon: "droplets"
|
|
19762
|
+
field: "enabled",
|
|
19763
|
+
label: t("Pump cycle", "Ciclo pompa"),
|
|
19764
|
+
whenTrue: t("When {device} starts running its pump on a cycle", "Quando {device} inizia a far lavorare la pompa a ciclo"),
|
|
19765
|
+
whenFalse: t("When {device} runs its pump continuously", "Quando {device} fa lavorare la pompa in continuo"),
|
|
19766
|
+
icon: "timer"
|
|
19340
19767
|
}]
|
|
19341
19768
|
},
|
|
19342
19769
|
"media-player": {
|
|
19343
19770
|
scope: "device",
|
|
19344
19771
|
noun: t("Media player", "Lettore multimediale"),
|
|
19345
19772
|
actions: [
|
|
19773
|
+
{
|
|
19774
|
+
id: "media.turnOn",
|
|
19775
|
+
method: "turnOn",
|
|
19776
|
+
label: t("Turn on", "Accendi"),
|
|
19777
|
+
phrase: t("Turn on {device}", "Accendi {device}"),
|
|
19778
|
+
icon: "power"
|
|
19779
|
+
},
|
|
19780
|
+
{
|
|
19781
|
+
id: "media.turnOff",
|
|
19782
|
+
method: "turnOff",
|
|
19783
|
+
label: t("Turn off", "Spegni"),
|
|
19784
|
+
phrase: t("Turn off {device}", "Spegni {device}"),
|
|
19785
|
+
icon: "power"
|
|
19786
|
+
},
|
|
19787
|
+
{
|
|
19788
|
+
id: "media.volumeUp",
|
|
19789
|
+
method: "volumeUp",
|
|
19790
|
+
label: t("Volume up", "Alza il volume"),
|
|
19791
|
+
phrase: t("Raise the volume of {device}", "Alza il volume di {device}"),
|
|
19792
|
+
icon: "volume-2"
|
|
19793
|
+
},
|
|
19794
|
+
{
|
|
19795
|
+
id: "media.volumeDown",
|
|
19796
|
+
method: "volumeDown",
|
|
19797
|
+
label: t("Volume down", "Abbassa il volume"),
|
|
19798
|
+
phrase: t("Lower the volume of {device}", "Abbassa il volume di {device}"),
|
|
19799
|
+
icon: "volume-2"
|
|
19800
|
+
},
|
|
19801
|
+
{
|
|
19802
|
+
id: "media.channelUp",
|
|
19803
|
+
method: "channelUp",
|
|
19804
|
+
label: t("Channel up", "Canale successivo"),
|
|
19805
|
+
phrase: t("Go to the next channel on {device}", "Passa al canale successivo su {device}"),
|
|
19806
|
+
icon: "skip-forward"
|
|
19807
|
+
},
|
|
19808
|
+
{
|
|
19809
|
+
id: "media.channelDown",
|
|
19810
|
+
method: "channelDown",
|
|
19811
|
+
label: t("Channel down", "Canale precedente"),
|
|
19812
|
+
phrase: t("Go to the previous channel on {device}", "Torna al canale precedente su {device}"),
|
|
19813
|
+
icon: "skip-back"
|
|
19814
|
+
},
|
|
19346
19815
|
{
|
|
19347
19816
|
id: "media.play",
|
|
19348
19817
|
method: "play",
|
|
@@ -21229,6 +21698,7 @@ var SYSTEM_CAP_VOCABULARY = {
|
|
|
21229
21698
|
"audio-analyzer": system(PIPELINE),
|
|
21230
21699
|
"audio-codec": system(INTERNAL),
|
|
21231
21700
|
"auth-provider": system(IDENTITY),
|
|
21701
|
+
"board-key-source": system(INTERNAL),
|
|
21232
21702
|
backup: system("system backups — run and restored from the Backups page"),
|
|
21233
21703
|
broker: system("message-broker connections — configured, not actuated"),
|
|
21234
21704
|
"calendar-source": system(INTERNAL),
|
|
@@ -45691,6 +46161,49 @@ object({
|
|
|
45691
46161
|
lastChangedAt: number()
|
|
45692
46162
|
});
|
|
45693
46163
|
DeviceType.Sensor;
|
|
46164
|
+
/**
|
|
46165
|
+
* Why a board key could not be read. Typed so a caller reports the cause and
|
|
46166
|
+
* never treats "could not read" as "has no key" (D49).
|
|
46167
|
+
*/
|
|
46168
|
+
var BoardKeyFailureReasonSchema = _enum([
|
|
46169
|
+
"unknown-source",
|
|
46170
|
+
"not-admin",
|
|
46171
|
+
"no-builder",
|
|
46172
|
+
"no-ingress-url",
|
|
46173
|
+
"unreachable",
|
|
46174
|
+
"unknown-board",
|
|
46175
|
+
"no-encryption-key",
|
|
46176
|
+
"unreadable-config"
|
|
46177
|
+
]);
|
|
46178
|
+
var BoardKeySourceSchema = object({
|
|
46179
|
+
sourceId: string(),
|
|
46180
|
+
name: string()
|
|
46181
|
+
});
|
|
46182
|
+
var BoardKeyBoardSchema = object({
|
|
46183
|
+
/** The builder's file name, e.g. `kitchen.yaml`: the handle `getBoardKey` takes. */
|
|
46184
|
+
configuration: string(),
|
|
46185
|
+
/** The node name the builder gives the board. */
|
|
46186
|
+
name: string(),
|
|
46187
|
+
/** The address the builder knows the board by, when it has one. */
|
|
46188
|
+
address: string().nullable()
|
|
46189
|
+
});
|
|
46190
|
+
var BoardKeyFailureSchema = object({
|
|
46191
|
+
ok: literal(false),
|
|
46192
|
+
reason: BoardKeyFailureReasonSchema,
|
|
46193
|
+
detail: string().optional()
|
|
46194
|
+
});
|
|
46195
|
+
var ListBoardsResultSchema = discriminatedUnion("ok", [object({
|
|
46196
|
+
ok: literal(true),
|
|
46197
|
+
boards: array(BoardKeyBoardSchema).readonly()
|
|
46198
|
+
}), BoardKeyFailureSchema]);
|
|
46199
|
+
var GetBoardKeyResultSchema = discriminatedUnion("ok", [object({
|
|
46200
|
+
ok: literal(true),
|
|
46201
|
+
key: string()
|
|
46202
|
+
}), BoardKeyFailureSchema]);
|
|
46203
|
+
method(_void(), array(BoardKeySourceSchema).readonly(), { auth: "admin" }), method(object({ sourceId: string().min(1) }), ListBoardsResultSchema, { auth: "admin" }), method(object({
|
|
46204
|
+
sourceId: string().min(1),
|
|
46205
|
+
configuration: string().min(1)
|
|
46206
|
+
}), GetBoardKeyResultSchema, { auth: "admin" });
|
|
45694
46207
|
object({
|
|
45695
46208
|
/** Current level as 0..100 inclusive. Firmware-reported. */
|
|
45696
46209
|
level: number().min(0).max(100).meta(readingMeta("percentage")),
|
|
@@ -47827,6 +48340,188 @@ DeviceType.Control, method(object({
|
|
|
47827
48340
|
kind: "mutation",
|
|
47828
48341
|
auth: "protected"
|
|
47829
48342
|
});
|
|
48343
|
+
/**
|
|
48344
|
+
* Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
|
|
48345
|
+
* Apple TV, Spotify, Roku, generic OTT receivers, …) on
|
|
48346
|
+
* `DeviceType.MediaPlayer`. Disjoint from the camera streaming
|
|
48347
|
+
* surface — those use `DeviceType.Camera` + the broker pipeline.
|
|
48348
|
+
*
|
|
48349
|
+
* The cap declares a wide method surface; not every backend supports
|
|
48350
|
+
* every method. Drivers signal sub-capability via the
|
|
48351
|
+
* `DeviceFeature.MediaPlayerXxx` flags (paired one-to-one with the
|
|
48352
|
+
* corresponding method) so the UI gates buttons without a slice
|
|
48353
|
+
* fetch — same pattern as `PanTiltZoom` for cameras.
|
|
48354
|
+
*/
|
|
48355
|
+
var MediaPlayerStateSchema = _enum([
|
|
48356
|
+
"off",
|
|
48357
|
+
"on",
|
|
48358
|
+
"idle",
|
|
48359
|
+
"playing",
|
|
48360
|
+
"paused",
|
|
48361
|
+
"buffering",
|
|
48362
|
+
"standby"
|
|
48363
|
+
]);
|
|
48364
|
+
var MediaPlayerRepeatSchema = _enum([
|
|
48365
|
+
"off",
|
|
48366
|
+
"all",
|
|
48367
|
+
"one"
|
|
48368
|
+
]);
|
|
48369
|
+
var MediaInfoSchema = object({
|
|
48370
|
+
/** Free-form media kind (`music`, `tvshow`, `movie`, `app`, `channel`,
|
|
48371
|
+
* `podcast`, …). */
|
|
48372
|
+
type: string(),
|
|
48373
|
+
/** Human-readable title. */
|
|
48374
|
+
title: string(),
|
|
48375
|
+
/** Optional artist / channel / station label. */
|
|
48376
|
+
artist: string().optional(),
|
|
48377
|
+
/** Optional album / season / show name. */
|
|
48378
|
+
album: string().optional(),
|
|
48379
|
+
/** Optional cover-art / thumbnail URL. */
|
|
48380
|
+
imageUrl: string().optional()
|
|
48381
|
+
});
|
|
48382
|
+
/**
|
|
48383
|
+
* Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
|
|
48384
|
+
* the device said so. `assumed`: the driver has no feedback (infrared) and
|
|
48385
|
+
* tracks what it last SENT; it moves only after a send was accepted, and a
|
|
48386
|
+
* human at the real remote makes it wrong. A UI shows an assumed value as a
|
|
48387
|
+
* guess, never as a reading.
|
|
48388
|
+
*/
|
|
48389
|
+
var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
|
|
48390
|
+
/** The fields of a media-player status a driver can only guess. */
|
|
48391
|
+
var MediaPlayerAssumedFieldSchema = _enum([
|
|
48392
|
+
"state",
|
|
48393
|
+
"volumeLevel",
|
|
48394
|
+
"isMuted",
|
|
48395
|
+
"source"
|
|
48396
|
+
]);
|
|
48397
|
+
var MediaPlayerStatusSchema = object({
|
|
48398
|
+
/** Playback lifecycle state. */
|
|
48399
|
+
state: MediaPlayerStateSchema,
|
|
48400
|
+
/**
|
|
48401
|
+
* Whether the state fields are readings or the driver's own bookkeeping.
|
|
48402
|
+
* A slice written before this field existed parses as `reported`.
|
|
48403
|
+
*/
|
|
48404
|
+
stateSource: MediaPlayerStateSourceSchema.default("reported"),
|
|
48405
|
+
/**
|
|
48406
|
+
* Which fields are the driver's own guess. `stateSource` is `assumed` when
|
|
48407
|
+
* this is not empty; a field not listed here is a reading even then (power
|
|
48408
|
+
* from a running-state source, mute guessed from the last send).
|
|
48409
|
+
*/
|
|
48410
|
+
assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
|
|
48411
|
+
/** Volume as 0..100 inclusive. Null when the device has no volume
|
|
48412
|
+
* surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
|
|
48413
|
+
volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
|
|
48414
|
+
/** Mute toggle distinct from volume=0. Null when no mute surface.
|
|
48415
|
+
* Pair with `DeviceFeature.MediaPlayerMute`. */
|
|
48416
|
+
isMuted: boolean().nullable(),
|
|
48417
|
+
/** Active source / input. Empty when no source surface. */
|
|
48418
|
+
source: string(),
|
|
48419
|
+
/** Selectable sources. Empty when no source surface.
|
|
48420
|
+
* Pair with `DeviceFeature.MediaPlayerSelectSource`. */
|
|
48421
|
+
availableSources: array(string()),
|
|
48422
|
+
/** Currently-playing media info. Null when nothing is playing. */
|
|
48423
|
+
currentMedia: MediaInfoSchema.nullable(),
|
|
48424
|
+
/** Current playback position in ms. Null when not seekable or
|
|
48425
|
+
* nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
|
|
48426
|
+
position: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
|
|
48427
|
+
/** Total duration of the current media in ms. Null when unknown
|
|
48428
|
+
* (live stream) or nothing is playing. */
|
|
48429
|
+
duration: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
|
|
48430
|
+
/** Shuffle toggle. Null when no shuffle surface.
|
|
48431
|
+
* Pair with `DeviceFeature.MediaPlayerShuffle`. */
|
|
48432
|
+
shuffle: boolean().nullable(),
|
|
48433
|
+
/** Repeat mode. Null when no repeat surface.
|
|
48434
|
+
* Pair with `DeviceFeature.MediaPlayerRepeat`. */
|
|
48435
|
+
repeat: MediaPlayerRepeatSchema.nullable(),
|
|
48436
|
+
/** Ms epoch when the slice was last updated. */
|
|
48437
|
+
lastChangedAt: number()
|
|
48438
|
+
});
|
|
48439
|
+
var stepInput = object({
|
|
48440
|
+
deviceId: number().int().nonnegative(),
|
|
48441
|
+
/** How many presses: the caller's count (Alexa `volumeSteps`, `channelCount`). */
|
|
48442
|
+
steps: number().int().min(1).max(50).default(1)
|
|
48443
|
+
});
|
|
48444
|
+
var deviceOnly = object({ deviceId: number().int().nonnegative() });
|
|
48445
|
+
DeviceType.MediaPlayer, method(deviceOnly, _void(), {
|
|
48446
|
+
kind: "mutation",
|
|
48447
|
+
auth: "protected"
|
|
48448
|
+
}), method(deviceOnly, _void(), {
|
|
48449
|
+
kind: "mutation",
|
|
48450
|
+
auth: "protected"
|
|
48451
|
+
}), method(stepInput, _void(), {
|
|
48452
|
+
kind: "mutation",
|
|
48453
|
+
auth: "protected"
|
|
48454
|
+
}), method(stepInput, _void(), {
|
|
48455
|
+
kind: "mutation",
|
|
48456
|
+
auth: "protected"
|
|
48457
|
+
}), method(stepInput, _void(), {
|
|
48458
|
+
kind: "mutation",
|
|
48459
|
+
auth: "protected"
|
|
48460
|
+
}), method(stepInput, _void(), {
|
|
48461
|
+
kind: "mutation",
|
|
48462
|
+
auth: "protected"
|
|
48463
|
+
}), method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
48464
|
+
kind: "mutation",
|
|
48465
|
+
auth: "protected"
|
|
48466
|
+
}), method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
48467
|
+
kind: "mutation",
|
|
48468
|
+
auth: "protected"
|
|
48469
|
+
}), method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
48470
|
+
kind: "mutation",
|
|
48471
|
+
auth: "protected"
|
|
48472
|
+
}), method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
48473
|
+
kind: "mutation",
|
|
48474
|
+
auth: "protected"
|
|
48475
|
+
}), method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
48476
|
+
kind: "mutation",
|
|
48477
|
+
auth: "protected"
|
|
48478
|
+
}), method(object({
|
|
48479
|
+
deviceId: number().int().nonnegative(),
|
|
48480
|
+
position: number().int().nonnegative()
|
|
48481
|
+
}), _void(), {
|
|
48482
|
+
kind: "mutation",
|
|
48483
|
+
auth: "protected"
|
|
48484
|
+
}), method(object({
|
|
48485
|
+
deviceId: number().int().nonnegative(),
|
|
48486
|
+
volumeLevel: number().min(0).max(100)
|
|
48487
|
+
}), _void(), {
|
|
48488
|
+
kind: "mutation",
|
|
48489
|
+
auth: "protected"
|
|
48490
|
+
}), method(object({
|
|
48491
|
+
deviceId: number().int().nonnegative(),
|
|
48492
|
+
muted: boolean()
|
|
48493
|
+
}), _void(), {
|
|
48494
|
+
kind: "mutation",
|
|
48495
|
+
auth: "protected"
|
|
48496
|
+
}), method(object({
|
|
48497
|
+
deviceId: number().int().nonnegative(),
|
|
48498
|
+
shuffle: boolean()
|
|
48499
|
+
}), _void(), {
|
|
48500
|
+
kind: "mutation",
|
|
48501
|
+
auth: "protected"
|
|
48502
|
+
}), method(object({
|
|
48503
|
+
deviceId: number().int().nonnegative(),
|
|
48504
|
+
repeat: MediaPlayerRepeatSchema
|
|
48505
|
+
}), _void(), {
|
|
48506
|
+
kind: "mutation",
|
|
48507
|
+
auth: "protected"
|
|
48508
|
+
}), method(object({
|
|
48509
|
+
deviceId: number().int().nonnegative(),
|
|
48510
|
+
source: string().min(1)
|
|
48511
|
+
}), _void(), {
|
|
48512
|
+
kind: "mutation",
|
|
48513
|
+
auth: "protected"
|
|
48514
|
+
}), method(object({
|
|
48515
|
+
deviceId: number().int().nonnegative(),
|
|
48516
|
+
/** Media identifier / URL. */
|
|
48517
|
+
mediaId: string().min(1),
|
|
48518
|
+
/** Media kind (`music`, `tvshow`, `movie`, `app`, …) — provider
|
|
48519
|
+
* passes it through to the upstream service. */
|
|
48520
|
+
mediaType: string().min(1)
|
|
48521
|
+
}), _void(), {
|
|
48522
|
+
kind: "mutation",
|
|
48523
|
+
auth: "protected"
|
|
48524
|
+
});
|
|
47830
48525
|
DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.PetFountain, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Heater, DeviceType.Dryer, DeviceType.Gateway, DeviceType.Camera, DeviceType.Fan, DeviceType.Lock, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Heater, DeviceType.Gateway, DeviceType.Hub, DeviceType.Camera, DeviceType.Sensor, DeviceType.MediaPlayer, DeviceType.Vacuum, DeviceType.Generic, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.PetFountain, DeviceType.Fan, DeviceType.Humidifier, DeviceType.Climate, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder, DeviceType.Heater;
|
|
47831
48526
|
var NUMBER = {
|
|
47832
48527
|
kind: "fixed",
|
|
@@ -48274,49 +48969,6 @@ function actuator(input) {
|
|
|
48274
48969
|
};
|
|
48275
48970
|
}
|
|
48276
48971
|
new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
|
|
48277
|
-
var PowerSourceRefusalReasonSchema = _enum([
|
|
48278
|
-
"no-power-source",
|
|
48279
|
-
"switch-missing",
|
|
48280
|
-
"switch-offline",
|
|
48281
|
-
"switch-unknown",
|
|
48282
|
-
"restart-in-progress",
|
|
48283
|
-
"switch-call-failed"
|
|
48284
|
-
]);
|
|
48285
|
-
var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
|
|
48286
|
-
accepted: literal(false),
|
|
48287
|
-
reason: PowerSourceRefusalReasonSchema,
|
|
48288
|
-
/** One line for the operator, naming the switch. */
|
|
48289
|
-
message: string()
|
|
48290
|
-
})]);
|
|
48291
|
-
var PowerSourceInfoSchema = object({
|
|
48292
|
-
/** False when the device has no power source linked; the rest is then empty. */
|
|
48293
|
-
linked: boolean(),
|
|
48294
|
-
switchDeviceId: number().nullable(),
|
|
48295
|
-
switchName: string().nullable(),
|
|
48296
|
-
/** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
|
|
48297
|
-
switchOn: boolean().nullable(),
|
|
48298
|
-
switchOnline: boolean().nullable(),
|
|
48299
|
-
restartInProgress: boolean(),
|
|
48300
|
-
offSeconds: number().int()
|
|
48301
|
-
});
|
|
48302
|
-
object({
|
|
48303
|
-
/** Null while the switch has not reported: unknown is never `false` (D393). */
|
|
48304
|
-
switchOn: boolean().nullable(),
|
|
48305
|
-
offSeconds: number().int(),
|
|
48306
|
-
lastFetchedAt: number()
|
|
48307
|
-
});
|
|
48308
|
-
DeviceType.Gateway, DeviceType.Hub, DeviceType.Camera, DeviceType.Sensor, DeviceType.MediaPlayer, DeviceType.Vacuum, DeviceType.Generic, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.PetFountain, DeviceType.Fan, DeviceType.Humidifier, DeviceType.Climate, DeviceType.WaterHeater;
|
|
48309
|
-
var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
|
|
48310
|
-
systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "admin" }), systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
48311
|
-
kind: "mutation",
|
|
48312
|
-
auth: "admin"
|
|
48313
|
-
}), systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
48314
|
-
kind: "mutation",
|
|
48315
|
-
auth: "admin"
|
|
48316
|
-
}), systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
48317
|
-
kind: "mutation",
|
|
48318
|
-
auth: "admin"
|
|
48319
|
-
});
|
|
48320
48972
|
DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
|
|
48321
48973
|
DeviceType.Camera;
|
|
48322
48974
|
`${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
|
|
@@ -48344,6 +48996,165 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
|
|
|
48344
48996
|
}).optional()
|
|
48345
48997
|
})) });
|
|
48346
48998
|
/**
|
|
48999
|
+
* A saved Customize assignment (D936). The schema is the type: callers use
|
|
49000
|
+
* `z.infer`, they do not restate the shape.
|
|
49001
|
+
*/
|
|
49002
|
+
var OverrideSourceSchema = object({
|
|
49003
|
+
addonId: string().min(1),
|
|
49004
|
+
stableId: string().min(1),
|
|
49005
|
+
cap: string().min(1),
|
|
49006
|
+
fieldPath: string().min(1)
|
|
49007
|
+
});
|
|
49008
|
+
var IdentityTransformDraftSchema = object({ kind: literal("identity") });
|
|
49009
|
+
var InvertTransformDraftSchema = object({ kind: literal("invert") });
|
|
49010
|
+
var ThresholdTransformDraftSchema = object({
|
|
49011
|
+
kind: literal("threshold"),
|
|
49012
|
+
comparison: _enum(["gte", "lte"]),
|
|
49013
|
+
threshold: number(),
|
|
49014
|
+
startDwellMs: number().nonnegative(),
|
|
49015
|
+
stopDwellMs: number().nonnegative()
|
|
49016
|
+
});
|
|
49017
|
+
var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
|
|
49018
|
+
IdentityTransformDraftSchema,
|
|
49019
|
+
InvertTransformDraftSchema,
|
|
49020
|
+
ThresholdTransformDraftSchema,
|
|
49021
|
+
object({
|
|
49022
|
+
kind: literal("linear"),
|
|
49023
|
+
scale: number(),
|
|
49024
|
+
offset: number(),
|
|
49025
|
+
clampMin: number().optional(),
|
|
49026
|
+
clampMax: number().optional()
|
|
49027
|
+
}),
|
|
49028
|
+
object({
|
|
49029
|
+
kind: literal("enum-map"),
|
|
49030
|
+
mapping: record(string(), string().nullable())
|
|
49031
|
+
}),
|
|
49032
|
+
object({ kind: literal("number") })
|
|
49033
|
+
]);
|
|
49034
|
+
var ScalarOverrideDraftSchema = object({
|
|
49035
|
+
kind: literal("scalar"),
|
|
49036
|
+
cap: string().min(1),
|
|
49037
|
+
targetId: string().min(1),
|
|
49038
|
+
source: OverrideSourceSchema,
|
|
49039
|
+
transform: CustomizeTransformDraftSchema
|
|
49040
|
+
});
|
|
49041
|
+
var KeyedArrayOverrideDraftSchema = object({
|
|
49042
|
+
kind: literal("keyedArray"),
|
|
49043
|
+
cap: string().min(1),
|
|
49044
|
+
targetId: string().min(1),
|
|
49045
|
+
key: string().min(1),
|
|
49046
|
+
label: string().min(1),
|
|
49047
|
+
fieldPath: string().min(1),
|
|
49048
|
+
source: OverrideSourceSchema,
|
|
49049
|
+
transform: CustomizeTransformDraftSchema
|
|
49050
|
+
});
|
|
49051
|
+
var PulseOverrideDraftSchema = object({
|
|
49052
|
+
kind: literal("pulse"),
|
|
49053
|
+
cap: string().min(1),
|
|
49054
|
+
targetId: string().min(1),
|
|
49055
|
+
enabled: boolean(),
|
|
49056
|
+
debounceMs: number().nonnegative(),
|
|
49057
|
+
edges: array(_enum([
|
|
49058
|
+
"rising",
|
|
49059
|
+
"button",
|
|
49060
|
+
"native-event"
|
|
49061
|
+
])).min(1).optional(),
|
|
49062
|
+
sources: array(OverrideSourceSchema).min(1)
|
|
49063
|
+
});
|
|
49064
|
+
var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
|
|
49065
|
+
"in-addition",
|
|
49066
|
+
"confirm",
|
|
49067
|
+
"replace"
|
|
49068
|
+
]).optional() });
|
|
49069
|
+
discriminatedUnion("kind", [
|
|
49070
|
+
ScalarOverrideDraftSchema,
|
|
49071
|
+
KeyedArrayOverrideDraftSchema,
|
|
49072
|
+
PulseOverrideDraftSchema,
|
|
49073
|
+
object({
|
|
49074
|
+
kind: literal("span"),
|
|
49075
|
+
cap: string().min(1),
|
|
49076
|
+
targetId: string().min(1),
|
|
49077
|
+
timeoutMs: number().nonnegative(),
|
|
49078
|
+
cooldownMs: number().nonnegative().optional(),
|
|
49079
|
+
sources: array(SpanSourceDraftSchema)
|
|
49080
|
+
})
|
|
49081
|
+
]);
|
|
49082
|
+
object({
|
|
49083
|
+
targetId: string().min(1),
|
|
49084
|
+
configured: boolean(),
|
|
49085
|
+
source: _enum(["native", "assigned"]),
|
|
49086
|
+
state: _enum([
|
|
49087
|
+
"active",
|
|
49088
|
+
"stale",
|
|
49089
|
+
"released",
|
|
49090
|
+
"unavailable"
|
|
49091
|
+
]),
|
|
49092
|
+
reason: string().nullable()
|
|
49093
|
+
});
|
|
49094
|
+
/**
|
|
49095
|
+
* What a running-threshold proposal says (planning#405). The schema is the
|
|
49096
|
+
* type: the algorithm, the capability method and the UI all read `z.infer`.
|
|
49097
|
+
*
|
|
49098
|
+
* A proposal is read-only advice: it carries the numbers it was derived from
|
|
49099
|
+
* (`evidence`) so the operator can judge it, and an `insufficient` answer names
|
|
49100
|
+
* WHY with a typed reason and the catalog defaults, never an invented number.
|
|
49101
|
+
*/
|
|
49102
|
+
var RunningProposalTierSchema = _enum(["raw", "5m"]);
|
|
49103
|
+
var RunningProposalConfidenceSchema = _enum([
|
|
49104
|
+
"high",
|
|
49105
|
+
"medium",
|
|
49106
|
+
"low"
|
|
49107
|
+
]);
|
|
49108
|
+
var RunningProposalInsufficientReasonSchema = _enum([
|
|
49109
|
+
"no-history",
|
|
49110
|
+
"span-too-short",
|
|
49111
|
+
"truncated",
|
|
49112
|
+
"no-idle-floor",
|
|
49113
|
+
"no-runs",
|
|
49114
|
+
"no-separation"
|
|
49115
|
+
]);
|
|
49116
|
+
var RunningProposalReplaySchema = object({
|
|
49117
|
+
/** Intervals the real dwell expression held true over the window. */
|
|
49118
|
+
intervals: number().int().nonnegative(),
|
|
49119
|
+
/** Intervals outside every run, or a second interval inside one. */
|
|
49120
|
+
extra: number().int().nonnegative(),
|
|
49121
|
+
/** Runs without an interval of their own (none, or merged into a neighbour). */
|
|
49122
|
+
missed: number().int().nonnegative()
|
|
49123
|
+
});
|
|
49124
|
+
var RunningProposalEvidenceSchema = object({
|
|
49125
|
+
tier: RunningProposalTierSchema,
|
|
49126
|
+
spanMs: number().nonnegative(),
|
|
49127
|
+
from: number(),
|
|
49128
|
+
to: number(),
|
|
49129
|
+
points: number().int().nonnegative(),
|
|
49130
|
+
runsFound: number().int().nonnegative(),
|
|
49131
|
+
pausesFound: number().int().nonnegative(),
|
|
49132
|
+
longestPauseMs: number().nonnegative(),
|
|
49133
|
+
idleFloorW: number(),
|
|
49134
|
+
noiseCeilingW: number(),
|
|
49135
|
+
plateauW: number(),
|
|
49136
|
+
replay: RunningProposalReplaySchema
|
|
49137
|
+
});
|
|
49138
|
+
var RunningProposalInsufficientEvidenceSchema = object({
|
|
49139
|
+
spanMs: number().nonnegative(),
|
|
49140
|
+
points: number().int().nonnegative(),
|
|
49141
|
+
runsFound: number().int().nonnegative()
|
|
49142
|
+
});
|
|
49143
|
+
var RunningThresholdProposalSchema = discriminatedUnion("state", [object({
|
|
49144
|
+
state: literal("proposal"),
|
|
49145
|
+
transform: ThresholdTransformDraftSchema,
|
|
49146
|
+
evidence: RunningProposalEvidenceSchema,
|
|
49147
|
+
confidence: RunningProposalConfidenceSchema,
|
|
49148
|
+
/** Why the confidence is not higher; empty when nothing held it back. */
|
|
49149
|
+
notes: array(string())
|
|
49150
|
+
}), object({
|
|
49151
|
+
state: literal("insufficient"),
|
|
49152
|
+
reason: RunningProposalInsufficientReasonSchema,
|
|
49153
|
+
evidence: RunningProposalInsufficientEvidenceSchema,
|
|
49154
|
+
/** The catalog defaults, offered as what stands when nothing better is known. */
|
|
49155
|
+
fallback: ThresholdTransformDraftSchema
|
|
49156
|
+
})]);
|
|
49157
|
+
/**
|
|
48347
49158
|
* The report of a one-shot "move this owner's plaintext secrets into the
|
|
48348
49159
|
* keychain" run (D878). COUNTS only - never a value, never a ref.
|
|
48349
49160
|
*
|
|
@@ -48633,6 +49444,14 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
|
|
|
48633
49444
|
kind: "mutation",
|
|
48634
49445
|
auth: "admin",
|
|
48635
49446
|
access: "view"
|
|
49447
|
+
}), method(object({
|
|
49448
|
+
deviceId: number().int(),
|
|
49449
|
+
cap: string().min(1),
|
|
49450
|
+
fieldPath: string().min(1),
|
|
49451
|
+
deviceType: _enum(DeviceType).optional()
|
|
49452
|
+
}), RunningThresholdProposalSchema, {
|
|
49453
|
+
auth: "admin",
|
|
49454
|
+
access: "view"
|
|
48636
49455
|
}), method(object({
|
|
48637
49456
|
blockId: string().min(1),
|
|
48638
49457
|
trace: AutomationRunTraceSchema.optional(),
|
|
@@ -49441,7 +50260,27 @@ method(object({
|
|
|
49441
50260
|
* exposes only the COMMON surface: link state, supported device
|
|
49442
50261
|
* kinds (so the UI can filter the device picker), and per-device
|
|
49443
50262
|
* expose/unexpose.
|
|
49444
|
-
|
|
50263
|
+
*
|
|
50264
|
+
* INSTANCES. One export addon can serve several independent targets (a
|
|
50265
|
+
* HomeKit bridge, a Home Assistant broker). `listInstances` names them.
|
|
50266
|
+
* `instanceId` on a READ is optional: absent = the whole addon (linked if any
|
|
50267
|
+
* enabled instance is, `exposedDeviceCount` = distinct devices, `setup` = the
|
|
50268
|
+
* default instance's); present = that instance; an id the provider does not
|
|
50269
|
+
* serve is refused with `ExportInstanceUnknownError` (`export-instance-unknown`),
|
|
50270
|
+
* never answered empty. `instanceId` on a WRITE is REQUIRED: a membership
|
|
50271
|
+
* change always names the target it changes. A single-instance provider
|
|
50272
|
+
* accepts an absent id on a read or its own id, and refuses any other.
|
|
50273
|
+
*/
|
|
50274
|
+
/** Which instance of an export addon a call addresses. Absent on a read = the whole addon. */
|
|
50275
|
+
var InstanceIdSchema$1 = string().min(1);
|
|
50276
|
+
var InstancePinSchema = object({ instanceId: InstanceIdSchema$1.optional() });
|
|
50277
|
+
/** One independent export target of an addon. */
|
|
50278
|
+
var ExportInstanceSchema = object({
|
|
50279
|
+
instanceId: InstanceIdSchema$1,
|
|
50280
|
+
name: string(),
|
|
50281
|
+
enabled: boolean(),
|
|
50282
|
+
kind: string().optional()
|
|
50283
|
+
});
|
|
49445
50284
|
var LinkStateSchema = _enum([
|
|
49446
50285
|
"unlinked",
|
|
49447
50286
|
"linked",
|
|
@@ -49492,10 +50331,14 @@ var ExposedDeviceSchema = object({
|
|
|
49492
50331
|
capabilities: array(string()).optional()
|
|
49493
50332
|
});
|
|
49494
50333
|
var ExposeInputSchema = object({
|
|
50334
|
+
instanceId: InstanceIdSchema$1,
|
|
49495
50335
|
deviceId: string(),
|
|
49496
50336
|
capabilities: array(string()).optional()
|
|
49497
50337
|
});
|
|
49498
|
-
var UnexposeInputSchema = object({
|
|
50338
|
+
var UnexposeInputSchema = object({
|
|
50339
|
+
instanceId: InstanceIdSchema$1,
|
|
50340
|
+
deviceId: string()
|
|
50341
|
+
});
|
|
49499
50342
|
/**
|
|
49500
50343
|
* What an exporter may expose besides devices (D968). An action and a
|
|
49501
50344
|
* notification target are LIST ENTRIES, never devices (D954): they enter an
|
|
@@ -49524,15 +50367,21 @@ var ExposedEntitySchema = object({
|
|
|
49524
50367
|
entity: ExportEntityRefSchema,
|
|
49525
50368
|
exposedAs: string().optional()
|
|
49526
50369
|
});
|
|
49527
|
-
var ExposeEntityInputSchema = object({
|
|
49528
|
-
|
|
49529
|
-
|
|
50370
|
+
var ExposeEntityInputSchema = object({
|
|
50371
|
+
instanceId: InstanceIdSchema$1,
|
|
50372
|
+
entity: ExportEntityRefSchema
|
|
50373
|
+
});
|
|
50374
|
+
var UnexposeEntityInputSchema = object({
|
|
50375
|
+
instanceId: InstanceIdSchema$1,
|
|
50376
|
+
entity: ExportEntityRefSchema
|
|
50377
|
+
});
|
|
50378
|
+
method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(ExportInstanceSchema)), method(InstancePinSchema, array(DeviceKindSchema)), method(InstancePinSchema, array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
|
|
49530
50379
|
kind: "mutation",
|
|
49531
50380
|
auth: "admin"
|
|
49532
50381
|
}), method(UnexposeInputSchema, _void(), {
|
|
49533
50382
|
kind: "mutation",
|
|
49534
50383
|
auth: "admin"
|
|
49535
|
-
}), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(
|
|
50384
|
+
}), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(InstancePinSchema, array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
|
|
49536
50385
|
kind: "mutation",
|
|
49537
50386
|
auth: "admin"
|
|
49538
50387
|
}), optionalMethod(UnexposeEntityInputSchema, _void(), {
|
|
@@ -51412,7 +52261,7 @@ object({
|
|
|
51412
52261
|
/** Ms epoch of the last value received from the appliance; 0 = never. */
|
|
51413
52262
|
lastFetchedAt: number()
|
|
51414
52263
|
});
|
|
51415
|
-
var DeviceIdSchema$
|
|
52264
|
+
var DeviceIdSchema$2 = number().int().nonnegative();
|
|
51416
52265
|
/** A value a program option or setting can take. */
|
|
51417
52266
|
var DishwasherValueSchema = union([
|
|
51418
52267
|
boolean(),
|
|
@@ -51476,77 +52325,77 @@ var DishwasherOptionsSchema = object({
|
|
|
51476
52325
|
options: array(DishwasherOptionDescriptorSchema)
|
|
51477
52326
|
});
|
|
51478
52327
|
DeviceType.Dishwasher, method(object({
|
|
51479
|
-
deviceId: DeviceIdSchema$
|
|
52328
|
+
deviceId: DeviceIdSchema$2,
|
|
51480
52329
|
program: string().min(1).optional(),
|
|
51481
52330
|
options: record(string(), DishwasherValueSchema).optional()
|
|
51482
52331
|
}), _void(), {
|
|
51483
52332
|
kind: "mutation",
|
|
51484
52333
|
auth: "protected"
|
|
51485
|
-
}), method(object({ deviceId: DeviceIdSchema$
|
|
52334
|
+
}), method(object({ deviceId: DeviceIdSchema$2 }), _void(), {
|
|
51486
52335
|
kind: "mutation",
|
|
51487
52336
|
auth: "protected"
|
|
51488
|
-
}), method(object({ deviceId: DeviceIdSchema$
|
|
52337
|
+
}), method(object({ deviceId: DeviceIdSchema$2 }), _void(), {
|
|
51489
52338
|
kind: "mutation",
|
|
51490
52339
|
auth: "protected"
|
|
51491
|
-
}), method(object({ deviceId: DeviceIdSchema$
|
|
52340
|
+
}), method(object({ deviceId: DeviceIdSchema$2 }), _void(), {
|
|
51492
52341
|
kind: "mutation",
|
|
51493
52342
|
auth: "protected"
|
|
51494
52343
|
}), method(object({
|
|
51495
|
-
deviceId: DeviceIdSchema$
|
|
52344
|
+
deviceId: DeviceIdSchema$2,
|
|
51496
52345
|
program: string().min(1)
|
|
51497
52346
|
}), _void(), {
|
|
51498
52347
|
kind: "mutation",
|
|
51499
52348
|
auth: "protected"
|
|
51500
52349
|
}), method(object({
|
|
51501
|
-
deviceId: DeviceIdSchema$
|
|
52350
|
+
deviceId: DeviceIdSchema$2,
|
|
51502
52351
|
on: boolean()
|
|
51503
52352
|
}), _void(), {
|
|
51504
52353
|
kind: "mutation",
|
|
51505
52354
|
auth: "protected"
|
|
51506
52355
|
}), method(object({
|
|
51507
|
-
deviceId: DeviceIdSchema$
|
|
52356
|
+
deviceId: DeviceIdSchema$2,
|
|
51508
52357
|
on: boolean()
|
|
51509
52358
|
}), _void(), {
|
|
51510
52359
|
kind: "mutation",
|
|
51511
52360
|
auth: "protected"
|
|
51512
52361
|
}), method(object({
|
|
51513
|
-
deviceId: DeviceIdSchema$
|
|
52362
|
+
deviceId: DeviceIdSchema$2,
|
|
51514
52363
|
on: boolean()
|
|
51515
52364
|
}), _void(), {
|
|
51516
52365
|
kind: "mutation",
|
|
51517
52366
|
auth: "protected"
|
|
51518
52367
|
}), method(object({
|
|
51519
|
-
deviceId: DeviceIdSchema$
|
|
52368
|
+
deviceId: DeviceIdSchema$2,
|
|
51520
52369
|
on: boolean()
|
|
51521
52370
|
}), _void(), {
|
|
51522
52371
|
kind: "mutation",
|
|
51523
52372
|
auth: "protected"
|
|
51524
52373
|
}), method(object({
|
|
51525
|
-
deviceId: DeviceIdSchema$
|
|
52374
|
+
deviceId: DeviceIdSchema$2,
|
|
51526
52375
|
on: boolean()
|
|
51527
52376
|
}), _void(), {
|
|
51528
52377
|
kind: "mutation",
|
|
51529
52378
|
auth: "protected"
|
|
51530
52379
|
}), method(object({
|
|
51531
|
-
deviceId: DeviceIdSchema$
|
|
52380
|
+
deviceId: DeviceIdSchema$2,
|
|
51532
52381
|
on: boolean()
|
|
51533
52382
|
}), _void(), {
|
|
51534
52383
|
kind: "mutation",
|
|
51535
52384
|
auth: "protected"
|
|
51536
52385
|
}), method(object({
|
|
51537
|
-
deviceId: DeviceIdSchema$
|
|
52386
|
+
deviceId: DeviceIdSchema$2,
|
|
51538
52387
|
on: boolean()
|
|
51539
52388
|
}), _void(), {
|
|
51540
52389
|
kind: "mutation",
|
|
51541
52390
|
auth: "protected"
|
|
51542
52391
|
}), method(object({
|
|
51543
|
-
deviceId: DeviceIdSchema$
|
|
52392
|
+
deviceId: DeviceIdSchema$2,
|
|
51544
52393
|
key: string().min(1),
|
|
51545
52394
|
value: DishwasherValueSchema
|
|
51546
52395
|
}), _void(), {
|
|
51547
52396
|
kind: "mutation",
|
|
51548
52397
|
auth: "protected"
|
|
51549
|
-
}), method(object({ deviceId: DeviceIdSchema$
|
|
52398
|
+
}), method(object({ deviceId: DeviceIdSchema$2 }), DishwasherOptionsSchema, { auth: "protected" });
|
|
51550
52399
|
/**
|
|
51551
52400
|
* Palette entry ids (D939). Audio is the one gradient; `audio-*` leaves are
|
|
51552
52401
|
* not entries. `heatmapScrub` is a mark, not an event kind.
|
|
@@ -52733,15 +53582,15 @@ var FanPresetCaptureResultSchema = object({
|
|
|
52733
53582
|
items: array(FanCaptureItemSchema),
|
|
52734
53583
|
omitted: array(FanCaptureOmissionSchema)
|
|
52735
53584
|
});
|
|
52736
|
-
var deviceInput$
|
|
53585
|
+
var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
|
|
52737
53586
|
DeviceType.Fan;
|
|
52738
|
-
method(deviceInput$
|
|
53587
|
+
method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
|
|
52739
53588
|
kind: "mutation",
|
|
52740
53589
|
auth: "admin"
|
|
52741
|
-
}), method(deviceInput$
|
|
53590
|
+
}), method(deviceInput$2.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
|
|
52742
53591
|
kind: "mutation",
|
|
52743
53592
|
auth: "protected"
|
|
52744
|
-
}), method(deviceInput$
|
|
53593
|
+
}), method(deviceInput$2.extend({
|
|
52745
53594
|
fields: array(FanCaptureFieldSchema).optional(),
|
|
52746
53595
|
steps: array(ActionStepSchema).max(20).default([])
|
|
52747
53596
|
}), FanPresetCaptureResultSchema, { auth: "protected" });
|
|
@@ -53265,13 +54114,50 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
53265
54114
|
*/
|
|
53266
54115
|
var ConfigSchemaPassthrough$3 = custom();
|
|
53267
54116
|
var IntegrationInstanceIdSchema = string().min(1);
|
|
54117
|
+
var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
|
|
54118
|
+
/**
|
|
54119
|
+
* One kind of instance a provider can create (an adapter, an account type). The sign-in and
|
|
54120
|
+
* connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
|
|
54121
|
+
* hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
|
|
54122
|
+
* interactive account link (popup) and may carry `{{brokerId}}`, which the UI replaces with the
|
|
54123
|
+
* instance id.
|
|
54124
|
+
*/
|
|
54125
|
+
var IntegrationInstanceKindSchema = object({
|
|
54126
|
+
/** Stable id of the kind, sent back as `createInstance.kind`. */
|
|
54127
|
+
id: string().min(1),
|
|
54128
|
+
label: string().min(1),
|
|
54129
|
+
/** The kind signs in through a server-held interactive session. */
|
|
54130
|
+
signIn: object({
|
|
54131
|
+
basePath: string().min(1),
|
|
54132
|
+
methods: array(_enum(["device-code", "proxy"])).optional()
|
|
54133
|
+
}).optional(),
|
|
54134
|
+
/** Relative hub path to start an interactive account link of an instance of this kind. */
|
|
54135
|
+
connectPath: string().min(1).optional(),
|
|
54136
|
+
/** Relative hub path to disconnect that account. */
|
|
54137
|
+
disconnectPath: string().min(1).optional()
|
|
54138
|
+
});
|
|
53268
54139
|
var IntegrationInstanceDescriptionSchema = object({
|
|
53269
54140
|
/** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
|
|
53270
|
-
instanceMode:
|
|
54141
|
+
instanceMode: IntegrationInstanceModeSchema,
|
|
53271
54142
|
/** What the instance is, for the UI (`destination`, `relay`, `account`). */
|
|
53272
54143
|
kind: string(),
|
|
53273
54144
|
/** True when `testInstance` really probes the remote. */
|
|
53274
|
-
canTest: boolean()
|
|
54145
|
+
canTest: boolean(),
|
|
54146
|
+
/**
|
|
54147
|
+
* The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
|
|
54148
|
+
* Add flow asks which first and sends it as `createInstance.kind`.
|
|
54149
|
+
*/
|
|
54150
|
+
kinds: array(IntegrationInstanceKindSchema).optional(),
|
|
54151
|
+
/**
|
|
54152
|
+
* The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
|
|
54153
|
+
* devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
|
|
54154
|
+
*/
|
|
54155
|
+
allowRestoreId: boolean().optional(),
|
|
54156
|
+
/**
|
|
54157
|
+
* `createInstance` without an `id` works: the provider mints it. Absent means the caller must
|
|
54158
|
+
* send an id (the UI then mints an opaque one).
|
|
54159
|
+
*/
|
|
54160
|
+
mintsId: boolean().optional()
|
|
53275
54161
|
});
|
|
53276
54162
|
var IntegrationInstanceStatusSchema = object({
|
|
53277
54163
|
state: _enum([
|
|
@@ -53280,16 +54166,27 @@ var IntegrationInstanceStatusSchema = object({
|
|
|
53280
54166
|
"error",
|
|
53281
54167
|
"unknown"
|
|
53282
54168
|
]),
|
|
53283
|
-
detail: string().optional()
|
|
54169
|
+
detail: string().optional(),
|
|
54170
|
+
/** A short human word for the state ("Signed in", "Token expired"); shown instead of `state`. */
|
|
54171
|
+
label: string().optional()
|
|
53284
54172
|
});
|
|
53285
54173
|
var IntegrationInstanceSchema = object({
|
|
53286
54174
|
id: IntegrationInstanceIdSchema,
|
|
53287
54175
|
name: string(),
|
|
53288
54176
|
/** The addon's own rule (an URL is present, a client id is present...). */
|
|
53289
54177
|
configured: boolean(),
|
|
54178
|
+
/**
|
|
54179
|
+
* The instance's own on/off switch (D996): always present, derived from the record body
|
|
54180
|
+
* (`enabled !== false`, so a row written before the field existed is on).
|
|
54181
|
+
*/
|
|
54182
|
+
enabled: boolean(),
|
|
54183
|
+
/** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
|
|
54184
|
+
kind: string().optional(),
|
|
53290
54185
|
/** Values for the instance form; a secret is a `SecretFieldView`, never text. */
|
|
53291
54186
|
formValues: record(string(), unknown()),
|
|
53292
|
-
status: IntegrationInstanceStatusSchema.optional()
|
|
54187
|
+
status: IntegrationInstanceStatusSchema.optional(),
|
|
54188
|
+
/** Epoch ms of the last probe of the remote that produced `status`; absent when never checked. */
|
|
54189
|
+
lastCheckedAt: number().optional()
|
|
53293
54190
|
});
|
|
53294
54191
|
var IntegrationInstancePatchSchema = object({
|
|
53295
54192
|
id: IntegrationInstanceIdSchema,
|
|
@@ -53297,25 +54194,57 @@ var IntegrationInstancePatchSchema = object({
|
|
|
53297
54194
|
/** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
|
|
53298
54195
|
values: record(string(), unknown())
|
|
53299
54196
|
});
|
|
53300
|
-
|
|
53301
|
-
|
|
53302
|
-
|
|
53303
|
-
|
|
53304
|
-
|
|
53305
|
-
|
|
53306
|
-
|
|
53307
|
-
|
|
53308
|
-
|
|
53309
|
-
|
|
53310
|
-
|
|
53311
|
-
|
|
53312
|
-
|
|
53313
|
-
|
|
53314
|
-
|
|
53315
|
-
|
|
53316
|
-
|
|
53317
|
-
|
|
53318
|
-
})
|
|
54197
|
+
var integrationInstancesCapability = {
|
|
54198
|
+
name: "integration-instances",
|
|
54199
|
+
scope: "system",
|
|
54200
|
+
mode: "collection",
|
|
54201
|
+
methods: {
|
|
54202
|
+
describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
|
|
54203
|
+
/** The form; `id: null` is the create form. */
|
|
54204
|
+
instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
|
|
54205
|
+
listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
|
|
54206
|
+
getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
|
|
54207
|
+
/**
|
|
54208
|
+
* Insert the typed record. With `id` it is stored under that id (a duplicate id is refused);
|
|
54209
|
+
* without one the PROVIDER mints it and answers it. `kind` picks one of `describe().kinds`.
|
|
54210
|
+
*/
|
|
54211
|
+
createInstance: method(object({
|
|
54212
|
+
id: IntegrationInstanceIdSchema.optional(),
|
|
54213
|
+
name: string().min(1),
|
|
54214
|
+
kind: string().min(1).optional()
|
|
54215
|
+
}), object({ id: string() }), {
|
|
54216
|
+
kind: "mutation",
|
|
54217
|
+
auth: "admin"
|
|
54218
|
+
}),
|
|
54219
|
+
updateInstance: method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
|
|
54220
|
+
kind: "mutation",
|
|
54221
|
+
auth: "admin"
|
|
54222
|
+
}),
|
|
54223
|
+
/**
|
|
54224
|
+
* Switch ONE instance on or off. A row control, not a form field: `updateInstance.values` is the
|
|
54225
|
+
* form patch (D62, D996). An off instance keeps its record and secrets and does no work.
|
|
54226
|
+
*/
|
|
54227
|
+
setInstanceEnabled: method(object({
|
|
54228
|
+
id: IntegrationInstanceIdSchema,
|
|
54229
|
+
enabled: boolean()
|
|
54230
|
+
}), object({ ok: literal(true) }), {
|
|
54231
|
+
kind: "mutation",
|
|
54232
|
+
auth: "admin"
|
|
54233
|
+
}),
|
|
54234
|
+
/** Delete the record and its keychain refs together. */
|
|
54235
|
+
deleteInstance: method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
|
|
54236
|
+
kind: "mutation",
|
|
54237
|
+
auth: "admin"
|
|
54238
|
+
}),
|
|
54239
|
+
testInstance: optionalMethod(object({
|
|
54240
|
+
id: IntegrationInstanceIdSchema.nullable(),
|
|
54241
|
+
values: record(string(), unknown())
|
|
54242
|
+
}), TestConnectionResultSchema$1, {
|
|
54243
|
+
kind: "mutation",
|
|
54244
|
+
auth: "admin"
|
|
54245
|
+
})
|
|
54246
|
+
}
|
|
54247
|
+
};
|
|
53319
54248
|
/**
|
|
53320
54249
|
* Two-way audio / talk-back cap. Clients open a WebRTC audio-only
|
|
53321
54250
|
* session (server-initiated offer pattern, identical to the main
|
|
@@ -55114,6 +56043,11 @@ var LogEntrySchema = object({
|
|
|
55114
56043
|
* flight, and not asking for the next slice ends the scan.
|
|
55115
56044
|
*/
|
|
55116
56045
|
var LogReadQuerySchema = object({
|
|
56046
|
+
/**
|
|
56047
|
+
* Which destination INSTANCE answers (a destination addon may hold several records). Absent ⇒
|
|
56048
|
+
* the provider's first readable one, so a caller written before instances existed is unchanged.
|
|
56049
|
+
*/
|
|
56050
|
+
instanceId: string().min(1).max(200).optional(),
|
|
55117
56051
|
/** Correlates `cancelRead`. Caller-chosen, unique per in-flight read. */
|
|
55118
56052
|
requestId: string().min(1).max(64).optional(),
|
|
55119
56053
|
/** The device the line is about (`tags.deviceId`, numeric). */
|
|
@@ -55176,6 +56110,8 @@ var LogReadResultSchema = object({
|
|
|
55176
56110
|
/** True when `cancelRead` ended this slice early. */
|
|
55177
56111
|
cancelled: boolean().optional()
|
|
55178
56112
|
});
|
|
56113
|
+
/** Names one destination instance; absent ⇒ the provider's first readable one. */
|
|
56114
|
+
var LogInstanceSelectorSchema = object({ instanceId: string().min(1).max(200).optional() });
|
|
55179
56115
|
var LogReadCapabilitiesSchema = object({
|
|
55180
56116
|
/** False ⇒ this destination only ships lines (console, forwarder, an unconfigured Loki). */
|
|
55181
56117
|
readable: boolean(),
|
|
@@ -55194,7 +56130,7 @@ var LogReadCapabilitiesSchema = object({
|
|
|
55194
56130
|
/** How far back the store reaches, when the provider knows (days). */
|
|
55195
56131
|
retentionDays: number().nullable()
|
|
55196
56132
|
});
|
|
55197
|
-
method(LogEntrySchema, _void(), { kind: "mutation" }), optionalMethod(
|
|
56133
|
+
method(LogEntrySchema, _void(), { kind: "mutation" }), optionalMethod(LogInstanceSelectorSchema, LogReadCapabilitiesSchema, { auth: "admin" }), optionalMethod(LogReadQuerySchema, LogReadResultSchema, { auth: "admin" }), optionalMethod(object({ requestId: string().min(1).max(64) }), object({ cancelled: boolean() }), {
|
|
55198
56134
|
kind: "mutation",
|
|
55199
56135
|
auth: "admin"
|
|
55200
56136
|
});
|
|
@@ -55298,164 +56234,6 @@ var LoginMethodContributionSchema = discriminatedUnion("kind", [
|
|
|
55298
56234
|
]);
|
|
55299
56235
|
method(_void(), array(LoginMethodContributionSchema).readonly());
|
|
55300
56236
|
/**
|
|
55301
|
-
* Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
|
|
55302
|
-
* Apple TV, Spotify, Roku, generic OTT receivers, …) on
|
|
55303
|
-
* `DeviceType.MediaPlayer`. Disjoint from the camera streaming
|
|
55304
|
-
* surface — those use `DeviceType.Camera` + the broker pipeline.
|
|
55305
|
-
*
|
|
55306
|
-
* The cap declares a wide method surface; not every backend supports
|
|
55307
|
-
* every method. Drivers signal sub-capability via the
|
|
55308
|
-
* `DeviceFeature.MediaPlayerXxx` flags (paired one-to-one with the
|
|
55309
|
-
* corresponding method) so the UI gates buttons without a slice
|
|
55310
|
-
* fetch — same pattern as `PanTiltZoom` for cameras.
|
|
55311
|
-
*/
|
|
55312
|
-
var MediaPlayerStateSchema = _enum([
|
|
55313
|
-
"off",
|
|
55314
|
-
"on",
|
|
55315
|
-
"idle",
|
|
55316
|
-
"playing",
|
|
55317
|
-
"paused",
|
|
55318
|
-
"buffering",
|
|
55319
|
-
"standby"
|
|
55320
|
-
]);
|
|
55321
|
-
var MediaPlayerRepeatSchema = _enum([
|
|
55322
|
-
"off",
|
|
55323
|
-
"all",
|
|
55324
|
-
"one"
|
|
55325
|
-
]);
|
|
55326
|
-
var MediaInfoSchema = object({
|
|
55327
|
-
/** Free-form media kind (`music`, `tvshow`, `movie`, `app`, `channel`,
|
|
55328
|
-
* `podcast`, …). */
|
|
55329
|
-
type: string(),
|
|
55330
|
-
/** Human-readable title. */
|
|
55331
|
-
title: string(),
|
|
55332
|
-
/** Optional artist / channel / station label. */
|
|
55333
|
-
artist: string().optional(),
|
|
55334
|
-
/** Optional album / season / show name. */
|
|
55335
|
-
album: string().optional(),
|
|
55336
|
-
/** Optional cover-art / thumbnail URL. */
|
|
55337
|
-
imageUrl: string().optional()
|
|
55338
|
-
});
|
|
55339
|
-
/**
|
|
55340
|
-
* Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
|
|
55341
|
-
* the device said so. `assumed`: the driver has no feedback (infrared) and
|
|
55342
|
-
* tracks what it last SENT; it moves only after a send was accepted, and a
|
|
55343
|
-
* human at the real remote makes it wrong. A UI shows an assumed value as a
|
|
55344
|
-
* guess, never as a reading.
|
|
55345
|
-
*/
|
|
55346
|
-
var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
|
|
55347
|
-
/** The fields of a media-player status a driver can only guess. */
|
|
55348
|
-
var MediaPlayerAssumedFieldSchema = _enum([
|
|
55349
|
-
"state",
|
|
55350
|
-
"volumeLevel",
|
|
55351
|
-
"isMuted",
|
|
55352
|
-
"source"
|
|
55353
|
-
]);
|
|
55354
|
-
object({
|
|
55355
|
-
/** Playback lifecycle state. */
|
|
55356
|
-
state: MediaPlayerStateSchema,
|
|
55357
|
-
/**
|
|
55358
|
-
* Whether the state fields are readings or the driver's own bookkeeping.
|
|
55359
|
-
* A slice written before this field existed parses as `reported`.
|
|
55360
|
-
*/
|
|
55361
|
-
stateSource: MediaPlayerStateSourceSchema.default("reported"),
|
|
55362
|
-
/**
|
|
55363
|
-
* Which fields are the driver's own guess. `stateSource` is `assumed` when
|
|
55364
|
-
* this is not empty; a field not listed here is a reading even then (power
|
|
55365
|
-
* from a running-state source, mute guessed from the last send).
|
|
55366
|
-
*/
|
|
55367
|
-
assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
|
|
55368
|
-
/** Volume as 0..100 inclusive. Null when the device has no volume
|
|
55369
|
-
* surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
|
|
55370
|
-
volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
|
|
55371
|
-
/** Mute toggle distinct from volume=0. Null when no mute surface.
|
|
55372
|
-
* Pair with `DeviceFeature.MediaPlayerMute`. */
|
|
55373
|
-
isMuted: boolean().nullable(),
|
|
55374
|
-
/** Active source / input. Empty when no source surface. */
|
|
55375
|
-
source: string(),
|
|
55376
|
-
/** Selectable sources. Empty when no source surface.
|
|
55377
|
-
* Pair with `DeviceFeature.MediaPlayerSelectSource`. */
|
|
55378
|
-
availableSources: array(string()),
|
|
55379
|
-
/** Currently-playing media info. Null when nothing is playing. */
|
|
55380
|
-
currentMedia: MediaInfoSchema.nullable(),
|
|
55381
|
-
/** Current playback position in ms. Null when not seekable or
|
|
55382
|
-
* nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
|
|
55383
|
-
position: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
|
|
55384
|
-
/** Total duration of the current media in ms. Null when unknown
|
|
55385
|
-
* (live stream) or nothing is playing. */
|
|
55386
|
-
duration: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
|
|
55387
|
-
/** Shuffle toggle. Null when no shuffle surface.
|
|
55388
|
-
* Pair with `DeviceFeature.MediaPlayerShuffle`. */
|
|
55389
|
-
shuffle: boolean().nullable(),
|
|
55390
|
-
/** Repeat mode. Null when no repeat surface.
|
|
55391
|
-
* Pair with `DeviceFeature.MediaPlayerRepeat`. */
|
|
55392
|
-
repeat: MediaPlayerRepeatSchema.nullable(),
|
|
55393
|
-
/** Ms epoch when the slice was last updated. */
|
|
55394
|
-
lastChangedAt: number()
|
|
55395
|
-
});
|
|
55396
|
-
DeviceType.MediaPlayer, method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
55397
|
-
kind: "mutation",
|
|
55398
|
-
auth: "protected"
|
|
55399
|
-
}), method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
55400
|
-
kind: "mutation",
|
|
55401
|
-
auth: "protected"
|
|
55402
|
-
}), method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
55403
|
-
kind: "mutation",
|
|
55404
|
-
auth: "protected"
|
|
55405
|
-
}), method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
55406
|
-
kind: "mutation",
|
|
55407
|
-
auth: "protected"
|
|
55408
|
-
}), method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
55409
|
-
kind: "mutation",
|
|
55410
|
-
auth: "protected"
|
|
55411
|
-
}), method(object({
|
|
55412
|
-
deviceId: number().int().nonnegative(),
|
|
55413
|
-
position: number().int().nonnegative()
|
|
55414
|
-
}), _void(), {
|
|
55415
|
-
kind: "mutation",
|
|
55416
|
-
auth: "protected"
|
|
55417
|
-
}), method(object({
|
|
55418
|
-
deviceId: number().int().nonnegative(),
|
|
55419
|
-
volumeLevel: number().min(0).max(100)
|
|
55420
|
-
}), _void(), {
|
|
55421
|
-
kind: "mutation",
|
|
55422
|
-
auth: "protected"
|
|
55423
|
-
}), method(object({
|
|
55424
|
-
deviceId: number().int().nonnegative(),
|
|
55425
|
-
muted: boolean()
|
|
55426
|
-
}), _void(), {
|
|
55427
|
-
kind: "mutation",
|
|
55428
|
-
auth: "protected"
|
|
55429
|
-
}), method(object({
|
|
55430
|
-
deviceId: number().int().nonnegative(),
|
|
55431
|
-
shuffle: boolean()
|
|
55432
|
-
}), _void(), {
|
|
55433
|
-
kind: "mutation",
|
|
55434
|
-
auth: "protected"
|
|
55435
|
-
}), method(object({
|
|
55436
|
-
deviceId: number().int().nonnegative(),
|
|
55437
|
-
repeat: MediaPlayerRepeatSchema
|
|
55438
|
-
}), _void(), {
|
|
55439
|
-
kind: "mutation",
|
|
55440
|
-
auth: "protected"
|
|
55441
|
-
}), method(object({
|
|
55442
|
-
deviceId: number().int().nonnegative(),
|
|
55443
|
-
source: string().min(1)
|
|
55444
|
-
}), _void(), {
|
|
55445
|
-
kind: "mutation",
|
|
55446
|
-
auth: "protected"
|
|
55447
|
-
}), method(object({
|
|
55448
|
-
deviceId: number().int().nonnegative(),
|
|
55449
|
-
/** Media identifier / URL. */
|
|
55450
|
-
mediaId: string().min(1),
|
|
55451
|
-
/** Media kind (`music`, `tvshow`, `movie`, `app`, …) — provider
|
|
55452
|
-
* passes it through to the upstream service. */
|
|
55453
|
-
mediaType: string().min(1)
|
|
55454
|
-
}), _void(), {
|
|
55455
|
-
kind: "mutation",
|
|
55456
|
-
auth: "protected"
|
|
55457
|
-
});
|
|
55458
|
-
/**
|
|
55459
56237
|
* mesh-network — collection cap for mesh-VPN providers.
|
|
55460
56238
|
*
|
|
55461
56239
|
* Different shape from `network-access` (public tunnels):
|
|
@@ -63349,7 +64127,14 @@ var NcMediaPolicySchema = object({
|
|
|
63349
64127
|
*/
|
|
63350
64128
|
profile: CamProfileSchema.optional(),
|
|
63351
64129
|
/** See {@link NcLinkedCameraModeSchema}. */
|
|
63352
|
-
linkedCameras: NcLinkedCameraModeSchema.optional()
|
|
64130
|
+
linkedCameras: NcLinkedCameraModeSchema.optional(),
|
|
64131
|
+
/**
|
|
64132
|
+
* Sensor rules only: may this rule wake a SLEEPING battery camera to take
|
|
64133
|
+
* the linked still it attaches (D998)? Absent means yes, an explicit `false`
|
|
64134
|
+
* opts out. Deliberately not a Zod default: a rule that never chose keeps
|
|
64135
|
+
* following the hub's. One bounded wake per event; nothing is attached later.
|
|
64136
|
+
*/
|
|
64137
|
+
wakeSleepingCamera: boolean().optional()
|
|
63353
64138
|
});
|
|
63354
64139
|
/**
|
|
63355
64140
|
* Cooldown GRANULARITY over the subject's class — how much a fired
|
|
@@ -63741,6 +64526,21 @@ var NcTemplateVarKindSchema = _enum([
|
|
|
63741
64526
|
"sound",
|
|
63742
64527
|
"system"
|
|
63743
64528
|
]);
|
|
64529
|
+
/**
|
|
64530
|
+
* The default title/body a rule of one SITUATION is delivered with when the
|
|
64531
|
+
* operator wrote no template — served so an editor can pre-fill it (and the
|
|
64532
|
+
* operator can rebuild it from placeholders) instead of starting from a blank
|
|
64533
|
+
* box that, once saved, silently replaces it. `id` names the situation inside
|
|
64534
|
+
* a rule `kind`: `appliance-cycle` is a `sensor` rule whose device is an
|
|
64535
|
+
* appliance finishing a cycle. Texts are raw templates (`{{var}}` intact), in
|
|
64536
|
+
* the hub's current language and with its overrides applied.
|
|
64537
|
+
*/
|
|
64538
|
+
var NcTemplateDefaultSchema = object({
|
|
64539
|
+
id: _enum(["appliance-cycle"]),
|
|
64540
|
+
kind: NcRuleKindSchema,
|
|
64541
|
+
title: string(),
|
|
64542
|
+
body: string()
|
|
64543
|
+
});
|
|
63744
64544
|
/** Which producer renders a template: an ordinary rule, a timelapse, a digest. */
|
|
63745
64545
|
var NcTemplateFamilySchema = _enum([
|
|
63746
64546
|
"rule",
|
|
@@ -64596,7 +65396,11 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
|
|
|
64596
65396
|
}), method(object({}), object({
|
|
64597
65397
|
catalog: array(NcConditionDescriptorSchema),
|
|
64598
65398
|
taxonomy: NcTaxonomySchema.optional()
|
|
64599
|
-
})), method(object({}), object({
|
|
65399
|
+
})), method(object({}), object({
|
|
65400
|
+
vars: array(NcTemplateVarDescriptorSchema),
|
|
65401
|
+
/** Per-situation default texts; absent on a hub that predates them. */
|
|
65402
|
+
defaults: array(NcTemplateDefaultSchema)
|
|
65403
|
+
})), method(NcTemplatePreviewInputSchema, NcTemplatePreviewSchema, {
|
|
64600
65404
|
kind: "mutation",
|
|
64601
65405
|
access: "view"
|
|
64602
65406
|
}), method(object({ filter: NcHistoryFilterSchema.default({ limit: 100 }) }), object({ entries: array(NcHistoryEntrySchema) }), { auth: "admin" }), method(object({}), NcRuleLastRunsSchema, { auth: "admin" }), method(object({ artifactId: string().min(1) }), object({ url: string().nullable() }), { auth: "admin" }), method(object({}), object({ snoozes: array(NcSnoozeSchema) }), { caller: "required" }), method(object({ snooze: NcSnoozeInputSchema }), object({ snooze: NcSnoozeSchema }), {
|
|
@@ -65344,52 +66148,27 @@ object({
|
|
|
65344
66148
|
empty: boolean().nullable(),
|
|
65345
66149
|
/** True while the pump is running. Null while unknown. */
|
|
65346
66150
|
pumpRunning: boolean().nullable(),
|
|
65347
|
-
/** True when the pump works on an on/off duty cycle rather than continuously. */
|
|
65348
|
-
pumpIntervalEnabled: boolean().nullable(),
|
|
65349
|
-
/** How long the pump runs per cycle, in seconds. */
|
|
65350
|
-
pumpOnTime: number().min(0).nullable().meta(readingMeta("duration")),
|
|
65351
|
-
/** How long the pump rests per cycle, in seconds. */
|
|
65352
|
-
pumpOffTime: number().min(0).nullable().meta(readingMeta("duration")),
|
|
65353
66151
|
/** Water drunk in the last drink, in litres. */
|
|
65354
66152
|
lastDrink: number().min(0).nullable().meta(readingMeta("volume")),
|
|
65355
66153
|
/** Water drunk in the current hour, in litres. */
|
|
65356
66154
|
currentHourDrink: number().min(0).nullable().meta(readingMeta("volume")),
|
|
65357
66155
|
/** Water drunk over the last 24 hours (rolling), in litres. */
|
|
65358
66156
|
last24hDrink: number().min(0).nullable().meta(readingMeta("volume")),
|
|
65359
|
-
/** Water drunk since local midnight, in litres. */
|
|
66157
|
+
/** Water drunk since local midnight, in litres. Null when the fountain has no such sensor. */
|
|
65360
66158
|
todayDrink: number().min(0).nullable().meta(readingMeta("volume")),
|
|
65361
66159
|
/** Decoded fault message. Null means healthy; a non-null string is an active fault. */
|
|
65362
66160
|
error: string().nullable(),
|
|
65363
66161
|
/** Ms epoch when the slice was last refreshed. */
|
|
65364
66162
|
lastFetchedAt: number()
|
|
65365
66163
|
});
|
|
65366
|
-
var DeviceIdSchema = number().int().nonnegative();
|
|
65367
|
-
var MinutesSchema = number().int().min(0);
|
|
66164
|
+
var DeviceIdSchema$1 = number().int().nonnegative();
|
|
65368
66165
|
DeviceType.PetFountain, method(object({
|
|
65369
|
-
deviceId: DeviceIdSchema,
|
|
65370
|
-
on: boolean()
|
|
65371
|
-
}), _void(), {
|
|
65372
|
-
kind: "mutation",
|
|
65373
|
-
auth: "protected"
|
|
65374
|
-
}), method(object({
|
|
65375
|
-
deviceId: DeviceIdSchema,
|
|
66166
|
+
deviceId: DeviceIdSchema$1,
|
|
65376
66167
|
on: boolean()
|
|
65377
66168
|
}), _void(), {
|
|
65378
66169
|
kind: "mutation",
|
|
65379
66170
|
auth: "protected"
|
|
65380
|
-
}), method(object({
|
|
65381
|
-
deviceId: DeviceIdSchema,
|
|
65382
|
-
minutes: MinutesSchema
|
|
65383
|
-
}), _void(), {
|
|
65384
|
-
kind: "mutation",
|
|
65385
|
-
auth: "protected"
|
|
65386
|
-
}), method(object({
|
|
65387
|
-
deviceId: DeviceIdSchema,
|
|
65388
|
-
minutes: MinutesSchema
|
|
65389
|
-
}), _void(), {
|
|
65390
|
-
kind: "mutation",
|
|
65391
|
-
auth: "protected"
|
|
65392
|
-
}), method(object({ deviceId: DeviceIdSchema }), _void(), {
|
|
66171
|
+
}), method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
|
|
65393
66172
|
kind: "mutation",
|
|
65394
66173
|
auth: "protected"
|
|
65395
66174
|
});
|
|
@@ -66923,6 +67702,49 @@ object({
|
|
|
66923
67702
|
DeviceType.PetFeeder,
|
|
66924
67703
|
DeviceType.Heater
|
|
66925
67704
|
].filter((type) => type !== DeviceType.Sensor);
|
|
67705
|
+
var PowerSourceRefusalReasonSchema = _enum([
|
|
67706
|
+
"no-power-source",
|
|
67707
|
+
"switch-missing",
|
|
67708
|
+
"switch-offline",
|
|
67709
|
+
"switch-unknown",
|
|
67710
|
+
"restart-in-progress",
|
|
67711
|
+
"switch-call-failed"
|
|
67712
|
+
]);
|
|
67713
|
+
var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
|
|
67714
|
+
accepted: literal(false),
|
|
67715
|
+
reason: PowerSourceRefusalReasonSchema,
|
|
67716
|
+
/** One line for the operator, naming the switch. */
|
|
67717
|
+
message: string()
|
|
67718
|
+
})]);
|
|
67719
|
+
var PowerSourceInfoSchema = object({
|
|
67720
|
+
/** False when the device has no power source linked; the rest is then empty. */
|
|
67721
|
+
linked: boolean(),
|
|
67722
|
+
switchDeviceId: number().nullable(),
|
|
67723
|
+
switchName: string().nullable(),
|
|
67724
|
+
/** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
|
|
67725
|
+
switchOn: boolean().nullable(),
|
|
67726
|
+
switchOnline: boolean().nullable(),
|
|
67727
|
+
restartInProgress: boolean(),
|
|
67728
|
+
offSeconds: number().int()
|
|
67729
|
+
});
|
|
67730
|
+
object({
|
|
67731
|
+
/** Null while the switch has not reported: unknown is never `false` (D393). */
|
|
67732
|
+
switchOn: boolean().nullable(),
|
|
67733
|
+
offSeconds: number().int(),
|
|
67734
|
+
lastFetchedAt: number()
|
|
67735
|
+
});
|
|
67736
|
+
DeviceType.Gateway, DeviceType.Hub, DeviceType.Camera, DeviceType.Sensor, DeviceType.MediaPlayer, DeviceType.Vacuum, DeviceType.Generic, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.PetFountain, DeviceType.Fan, DeviceType.Humidifier, DeviceType.Climate, DeviceType.WaterHeater;
|
|
67737
|
+
var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
|
|
67738
|
+
systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "admin" }), systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
67739
|
+
kind: "mutation",
|
|
67740
|
+
auth: "admin"
|
|
67741
|
+
}), systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
67742
|
+
kind: "mutation",
|
|
67743
|
+
auth: "admin"
|
|
67744
|
+
}), systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
67745
|
+
kind: "mutation",
|
|
67746
|
+
auth: "admin"
|
|
67747
|
+
});
|
|
66926
67748
|
/**
|
|
66927
67749
|
* Presence cap. Models HA `person.*` and `device_tracker.*` entities
|
|
66928
67750
|
* on `DeviceType.Presence`. Read-only — no setters: presence is
|
|
@@ -67349,6 +68171,37 @@ DeviceType.Camera, method(object({ deviceId: number() }), PtzAutotrackStatusSche
|
|
|
67349
68171
|
deviceId: number(),
|
|
67350
68172
|
status: PtzAutotrackStatusSchema
|
|
67351
68173
|
});
|
|
68174
|
+
object({
|
|
68175
|
+
/** True when the pump works on the on/off cycle, false when it runs continuously. Null while unknown. */
|
|
68176
|
+
enabled: boolean().nullable(),
|
|
68177
|
+
/** How long the pump runs per cycle, in seconds. Null while unknown. */
|
|
68178
|
+
onTime: number().min(0).nullable().meta(readingMeta("duration")),
|
|
68179
|
+
/** How long the pump rests per cycle, in seconds. Null while unknown. */
|
|
68180
|
+
offTime: number().min(0).nullable().meta(readingMeta("duration")),
|
|
68181
|
+
/** Ms epoch when the slice was last refreshed. */
|
|
68182
|
+
lastFetchedAt: number()
|
|
68183
|
+
});
|
|
68184
|
+
var DeviceIdSchema = number().int().nonnegative();
|
|
68185
|
+
var MinutesSchema = number().int().min(0);
|
|
68186
|
+
DeviceType.PetFountain, method(object({
|
|
68187
|
+
deviceId: DeviceIdSchema,
|
|
68188
|
+
on: boolean()
|
|
68189
|
+
}), _void(), {
|
|
68190
|
+
kind: "mutation",
|
|
68191
|
+
auth: "protected"
|
|
68192
|
+
}), method(object({
|
|
68193
|
+
deviceId: DeviceIdSchema,
|
|
68194
|
+
minutes: MinutesSchema
|
|
68195
|
+
}), _void(), {
|
|
68196
|
+
kind: "mutation",
|
|
68197
|
+
auth: "protected"
|
|
68198
|
+
}), method(object({
|
|
68199
|
+
deviceId: DeviceIdSchema,
|
|
68200
|
+
minutes: MinutesSchema
|
|
68201
|
+
}), _void(), {
|
|
68202
|
+
kind: "mutation",
|
|
68203
|
+
auth: "protected"
|
|
68204
|
+
});
|
|
67352
68205
|
DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, DeviceType.Gateway, method(object({ deviceId: number().int().nonnegative() }), object({ success: literal(true) }), {
|
|
67353
68206
|
kind: "mutation",
|
|
67354
68207
|
auth: "admin"
|
|
@@ -70062,7 +70915,13 @@ var RemoteMediaCommandSchema = _enum([
|
|
|
70062
70915
|
"next",
|
|
70063
70916
|
"previous",
|
|
70064
70917
|
"setMute",
|
|
70065
|
-
"selectSource"
|
|
70918
|
+
"selectSource",
|
|
70919
|
+
"turnOn",
|
|
70920
|
+
"turnOff",
|
|
70921
|
+
"volumeUp",
|
|
70922
|
+
"volumeDown",
|
|
70923
|
+
"channelUp",
|
|
70924
|
+
"channelDown"
|
|
70066
70925
|
]);
|
|
70067
70926
|
/** A send result, or `not-routed`: the role is not bound, so the native cap answers. */
|
|
70068
70927
|
var RemoteMediaCommandResultSchema = discriminatedUnion("status", [...RemoteSendResultSchema.options, object({ status: literal("not-routed") })]);
|
|
@@ -70083,7 +70942,7 @@ method(deviceInput, RemoteConfigSchema, { auth: "admin" }), method(deviceInput.e
|
|
|
70083
70942
|
}), RemoteSendResultSchema, {
|
|
70084
70943
|
kind: "mutation",
|
|
70085
70944
|
auth: "protected"
|
|
70086
|
-
}), method(deviceInput.extend({
|
|
70945
|
+
}), method(deviceInput, MediaPlayerStatusSchema.nullable(), { auth: "protected" }), method(deviceInput.extend({
|
|
70087
70946
|
command: RemoteMediaCommandSchema,
|
|
70088
70947
|
muted: boolean().optional(),
|
|
70089
70948
|
source: string().optional(),
|
|
@@ -71111,10 +71970,12 @@ DeviceType.Camera, method(object({
|
|
|
71111
71970
|
* even when the cache is well within the device's
|
|
71112
71971
|
* `snapshotMaxAgeS` window.
|
|
71113
71972
|
*
|
|
71114
|
-
* **`force` is
|
|
71973
|
+
* **`force` is a demand with a named principal, not a freshness
|
|
71974
|
+
* preference** — an operator button, the scheduled refresh (D847), or a
|
|
71975
|
+
* notification rule that left the camera's wake allowed (D998). On a
|
|
71115
71976
|
* battery camera it is the one thing that walks past the wrapper's
|
|
71116
|
-
* sleep gate and wakes the camera, so a
|
|
71117
|
-
*
|
|
71977
|
+
* sleep gate and wakes the camera, so a poller, a thumbnail or a
|
|
71978
|
+
* prestream must NEVER set it. Every such caller
|
|
71118
71979
|
* gets the cached frame, which on a sleeping battery camera is the
|
|
71119
71980
|
* correct answer: stale but honest beats woken.
|
|
71120
71981
|
*/
|
|
@@ -71922,7 +72783,11 @@ object({
|
|
|
71922
72783
|
});
|
|
71923
72784
|
Object.values(DeviceType), method(_void(), array(StreamSignalSchema).readonly());
|
|
71924
72785
|
object({
|
|
71925
|
-
|
|
72786
|
+
/**
|
|
72787
|
+
* `null` = the device has never reported its state (a measurement that
|
|
72788
|
+
* has not happened is unknown, never `false`).
|
|
72789
|
+
*/
|
|
72790
|
+
on: boolean().nullable().meta({ label: "On" }),
|
|
71926
72791
|
/** Ms epoch of the last state change. Useful for UI "X minutes ago". */
|
|
71927
72792
|
lastChangedAt: number()
|
|
71928
72793
|
});
|
|
@@ -71934,7 +72799,7 @@ DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceTyp
|
|
|
71934
72799
|
auth: "protected"
|
|
71935
72800
|
}), object({
|
|
71936
72801
|
deviceId: number(),
|
|
71937
|
-
on: boolean(),
|
|
72802
|
+
on: boolean().nullable(),
|
|
71938
72803
|
lastChangedAt: number()
|
|
71939
72804
|
});
|
|
71940
72805
|
/** C0 controls, DEL and the C1 range: none of them belongs in a name. */
|
|
@@ -74946,6 +75811,26 @@ function buildDescriptor(capName, kind) {
|
|
|
74946
75811
|
* contributes one entry per bound cap present in this map.
|
|
74947
75812
|
*/
|
|
74948
75813
|
var EVENT_KIND_BY_CAP = Object.freeze(Object.fromEntries(Object.entries(CAP_TO_KIND).map(([capName, kind]) => [capName, buildDescriptor(capName, kind)])));
|
|
75814
|
+
/**
|
|
75815
|
+
* What a remote key is CALLED and what it LOOKS like, for every role (D963),
|
|
75816
|
+
* shared by the admin UI and the viewer. A role is data on the wire; each
|
|
75817
|
+
* client resolves its name from `label` (English and Italian side by side) and
|
|
75818
|
+
* its glyph from `icon`, a lucide id from the shared icon catalogue — the
|
|
75819
|
+
* server sends an id, a client maps it to its own icon set.
|
|
75820
|
+
*
|
|
75821
|
+
* `legend` is text printed on the key INSTEAD of a glyph (a digit has no
|
|
75822
|
+
* pictogram): a client that can draw text shows it, one that cannot falls back
|
|
75823
|
+
* to `icon`.
|
|
75824
|
+
*/
|
|
75825
|
+
function digit(n) {
|
|
75826
|
+
return {
|
|
75827
|
+
label: t(n, n),
|
|
75828
|
+
icon: "hash",
|
|
75829
|
+
legend: n
|
|
75830
|
+
};
|
|
75831
|
+
}
|
|
75832
|
+
digit("0"), digit("1"), digit("2"), digit("3"), digit("4"), digit("5"), digit("6"), digit("7"), digit("8"), digit("9");
|
|
75833
|
+
DeviceFeature.MediaPlayerPower, DeviceFeature.MediaPlayerVolumeStep, DeviceFeature.MediaPlayerChannelStep, DeviceFeature.MediaPlayerMute, DeviceFeature.MediaPlayerStop, DeviceFeature.MediaPlayerNext, DeviceFeature.MediaPlayerPrevious;
|
|
74949
75834
|
var btn = (idSuffix, row, col, init) => ({
|
|
74950
75835
|
idSuffix,
|
|
74951
75836
|
row,
|
|
@@ -75072,7 +75957,35 @@ discriminatedUnion("kind", [object({
|
|
|
75072
75957
|
kind: literal("pronto"),
|
|
75073
75958
|
code: string()
|
|
75074
75959
|
})]);
|
|
75075
|
-
|
|
75960
|
+
/**
|
|
75961
|
+
* Propose a running threshold from a device's own power history (planning#405).
|
|
75962
|
+
*
|
|
75963
|
+
* Pure and deterministic: the same series gives the same answer. The series is
|
|
75964
|
+
* a HELD-VALUE step series — a point's value stands until the next point — which
|
|
75965
|
+
* is how device-history stores a gauge, so "time at a level" is exact, not a
|
|
75966
|
+
* count of samples.
|
|
75967
|
+
*
|
|
75968
|
+
* 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
|
|
75969
|
+
* near F. A device that is not near one level most of the time has no floor.
|
|
75970
|
+
* 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
|
|
75971
|
+
* 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
|
|
75972
|
+
* the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
|
|
75973
|
+
* 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
|
|
75974
|
+
* grid a person would type; stop dwell covers the longest pause; start dwell
|
|
75975
|
+
* covers the longest standby blip above T.
|
|
75976
|
+
* 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
|
|
75977
|
+
* run must hold exactly one true interval and nothing may flap outside.
|
|
75978
|
+
*/
|
|
75979
|
+
var SECOND_MS = 1e3;
|
|
75980
|
+
var MINUTE_MS$1 = 60 * SECOND_MS;
|
|
75981
|
+
24 * (60 * MINUTE_MS$1);
|
|
75982
|
+
10 * MINUTE_MS$1;
|
|
75983
|
+
30 * SECOND_MS;
|
|
75984
|
+
60 * SECOND_MS;
|
|
75985
|
+
20 * MINUTE_MS$1;
|
|
75986
|
+
10 * SECOND_MS;
|
|
75987
|
+
10 * SECOND_MS;
|
|
75988
|
+
DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
|
|
75076
75989
|
SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
|
|
75077
75990
|
String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
|
|
75078
75991
|
DeviceRole.BinarySensor, DeviceRole.MotionSensor;
|
|
@@ -76053,6 +76966,27 @@ Object.freeze({
|
|
|
76053
76966
|
access: "view",
|
|
76054
76967
|
role: "viewer"
|
|
76055
76968
|
},
|
|
76969
|
+
"boardKeySource.getBoardKey": {
|
|
76970
|
+
capName: "board-key-source",
|
|
76971
|
+
capScope: "system",
|
|
76972
|
+
addonId: null,
|
|
76973
|
+
access: "view",
|
|
76974
|
+
role: "admin"
|
|
76975
|
+
},
|
|
76976
|
+
"boardKeySource.listBoards": {
|
|
76977
|
+
capName: "board-key-source",
|
|
76978
|
+
capScope: "system",
|
|
76979
|
+
addonId: null,
|
|
76980
|
+
access: "view",
|
|
76981
|
+
role: "admin"
|
|
76982
|
+
},
|
|
76983
|
+
"boardKeySource.listSources": {
|
|
76984
|
+
capName: "board-key-source",
|
|
76985
|
+
capScope: "system",
|
|
76986
|
+
addonId: null,
|
|
76987
|
+
access: "view",
|
|
76988
|
+
role: "admin"
|
|
76989
|
+
},
|
|
76056
76990
|
"brightness.getStatus": {
|
|
76057
76991
|
capName: "brightness",
|
|
76058
76992
|
capScope: "device",
|
|
@@ -76592,6 +77526,13 @@ Object.freeze({
|
|
|
76592
77526
|
access: "create",
|
|
76593
77527
|
role: "admin"
|
|
76594
77528
|
},
|
|
77529
|
+
"coreBlocks.proposeRunningThreshold": {
|
|
77530
|
+
capName: "core-blocks",
|
|
77531
|
+
capScope: "system",
|
|
77532
|
+
addonId: null,
|
|
77533
|
+
access: "view",
|
|
77534
|
+
role: "admin"
|
|
77535
|
+
},
|
|
76595
77536
|
"coreBlocks.reportAutomationRun": {
|
|
76596
77537
|
capName: "core-blocks",
|
|
76597
77538
|
capScope: "system",
|
|
@@ -77103,6 +78044,13 @@ Object.freeze({
|
|
|
77103
78044
|
access: "view",
|
|
77104
78045
|
role: "admin"
|
|
77105
78046
|
},
|
|
78047
|
+
"deviceExport.listInstances": {
|
|
78048
|
+
capName: "device-export",
|
|
78049
|
+
capScope: "system",
|
|
78050
|
+
addonId: null,
|
|
78051
|
+
access: "view",
|
|
78052
|
+
role: "admin"
|
|
78053
|
+
},
|
|
77106
78054
|
"deviceExport.listSupportedDeviceKinds": {
|
|
77107
78055
|
capName: "device-export",
|
|
77108
78056
|
capScope: "system",
|
|
@@ -78734,6 +79682,13 @@ Object.freeze({
|
|
|
78734
79682
|
access: "view",
|
|
78735
79683
|
role: "admin"
|
|
78736
79684
|
},
|
|
79685
|
+
"integrationInstances.setInstanceEnabled": {
|
|
79686
|
+
capName: "integration-instances",
|
|
79687
|
+
capScope: "system",
|
|
79688
|
+
addonId: null,
|
|
79689
|
+
access: "create",
|
|
79690
|
+
role: "admin"
|
|
79691
|
+
},
|
|
78737
79692
|
"integrationInstances.testInstance": {
|
|
78738
79693
|
capName: "integration-instances",
|
|
78739
79694
|
capScope: "system",
|
|
@@ -79532,6 +80487,20 @@ Object.freeze({
|
|
|
79532
80487
|
access: "view",
|
|
79533
80488
|
role: "admin"
|
|
79534
80489
|
},
|
|
80490
|
+
"mediaPlayer.channelDown": {
|
|
80491
|
+
capName: "media-player",
|
|
80492
|
+
capScope: "device",
|
|
80493
|
+
addonId: null,
|
|
80494
|
+
access: "create",
|
|
80495
|
+
role: "viewer"
|
|
80496
|
+
},
|
|
80497
|
+
"mediaPlayer.channelUp": {
|
|
80498
|
+
capName: "media-player",
|
|
80499
|
+
capScope: "device",
|
|
80500
|
+
addonId: null,
|
|
80501
|
+
access: "create",
|
|
80502
|
+
role: "viewer"
|
|
80503
|
+
},
|
|
79535
80504
|
"mediaPlayer.getStatus": {
|
|
79536
80505
|
capName: "media-player",
|
|
79537
80506
|
capScope: "device",
|
|
@@ -79623,6 +80592,34 @@ Object.freeze({
|
|
|
79623
80592
|
access: "create",
|
|
79624
80593
|
role: "viewer"
|
|
79625
80594
|
},
|
|
80595
|
+
"mediaPlayer.turnOff": {
|
|
80596
|
+
capName: "media-player",
|
|
80597
|
+
capScope: "device",
|
|
80598
|
+
addonId: null,
|
|
80599
|
+
access: "create",
|
|
80600
|
+
role: "viewer"
|
|
80601
|
+
},
|
|
80602
|
+
"mediaPlayer.turnOn": {
|
|
80603
|
+
capName: "media-player",
|
|
80604
|
+
capScope: "device",
|
|
80605
|
+
addonId: null,
|
|
80606
|
+
access: "create",
|
|
80607
|
+
role: "viewer"
|
|
80608
|
+
},
|
|
80609
|
+
"mediaPlayer.volumeDown": {
|
|
80610
|
+
capName: "media-player",
|
|
80611
|
+
capScope: "device",
|
|
80612
|
+
addonId: null,
|
|
80613
|
+
access: "create",
|
|
80614
|
+
role: "viewer"
|
|
80615
|
+
},
|
|
80616
|
+
"mediaPlayer.volumeUp": {
|
|
80617
|
+
capName: "media-player",
|
|
80618
|
+
capScope: "device",
|
|
80619
|
+
addonId: null,
|
|
80620
|
+
access: "create",
|
|
80621
|
+
role: "viewer"
|
|
80622
|
+
},
|
|
79626
80623
|
"meshNetwork.getStatus": {
|
|
79627
80624
|
capName: "mesh-network",
|
|
79628
80625
|
capScope: "system",
|
|
@@ -80869,27 +81866,6 @@ Object.freeze({
|
|
|
80869
81866
|
access: "create",
|
|
80870
81867
|
role: "viewer"
|
|
80871
81868
|
},
|
|
80872
|
-
"petFountain.setPumpInterval": {
|
|
80873
|
-
capName: "pet-fountain",
|
|
80874
|
-
capScope: "device",
|
|
80875
|
-
addonId: null,
|
|
80876
|
-
access: "create",
|
|
80877
|
-
role: "viewer"
|
|
80878
|
-
},
|
|
80879
|
-
"petFountain.setPumpOffMinutes": {
|
|
80880
|
-
capName: "pet-fountain",
|
|
80881
|
-
capScope: "device",
|
|
80882
|
-
addonId: null,
|
|
80883
|
-
access: "create",
|
|
80884
|
-
role: "viewer"
|
|
80885
|
-
},
|
|
80886
|
-
"petFountain.setPumpOnMinutes": {
|
|
80887
|
-
capName: "pet-fountain",
|
|
80888
|
-
capScope: "device",
|
|
80889
|
-
addonId: null,
|
|
80890
|
-
access: "create",
|
|
80891
|
-
role: "viewer"
|
|
80892
|
-
},
|
|
80893
81869
|
"pipelineAnalytics.addRetrainFrameFromRecording": {
|
|
80894
81870
|
capName: "pipeline-analytics",
|
|
80895
81871
|
capScope: "device",
|
|
@@ -82668,6 +83644,34 @@ Object.freeze({
|
|
|
82668
83644
|
access: "create",
|
|
82669
83645
|
role: "admin"
|
|
82670
83646
|
},
|
|
83647
|
+
"pumpCycle.getStatus": {
|
|
83648
|
+
capName: "pump-cycle",
|
|
83649
|
+
capScope: "device",
|
|
83650
|
+
addonId: null,
|
|
83651
|
+
access: "view",
|
|
83652
|
+
role: "viewer"
|
|
83653
|
+
},
|
|
83654
|
+
"pumpCycle.setEnabled": {
|
|
83655
|
+
capName: "pump-cycle",
|
|
83656
|
+
capScope: "device",
|
|
83657
|
+
addonId: null,
|
|
83658
|
+
access: "create",
|
|
83659
|
+
role: "viewer"
|
|
83660
|
+
},
|
|
83661
|
+
"pumpCycle.setOffMinutes": {
|
|
83662
|
+
capName: "pump-cycle",
|
|
83663
|
+
capScope: "device",
|
|
83664
|
+
addonId: null,
|
|
83665
|
+
access: "create",
|
|
83666
|
+
role: "viewer"
|
|
83667
|
+
},
|
|
83668
|
+
"pumpCycle.setOnMinutes": {
|
|
83669
|
+
capName: "pump-cycle",
|
|
83670
|
+
capScope: "device",
|
|
83671
|
+
addonId: null,
|
|
83672
|
+
access: "create",
|
|
83673
|
+
role: "viewer"
|
|
83674
|
+
},
|
|
82671
83675
|
"reboot.reboot": {
|
|
82672
83676
|
capName: "reboot",
|
|
82673
83677
|
capScope: "device",
|
|
@@ -83172,6 +84176,13 @@ Object.freeze({
|
|
|
83172
84176
|
access: "create",
|
|
83173
84177
|
role: "admin"
|
|
83174
84178
|
},
|
|
84179
|
+
"remote.getAssumedStatus": {
|
|
84180
|
+
capName: "remote",
|
|
84181
|
+
capScope: "system",
|
|
84182
|
+
addonId: null,
|
|
84183
|
+
access: "view",
|
|
84184
|
+
role: "admin"
|
|
84185
|
+
},
|
|
83175
84186
|
"remote.getConfig": {
|
|
83176
84187
|
capName: "remote",
|
|
83177
84188
|
capScope: "system",
|
|
@@ -85987,6 +86998,11 @@ Object.freeze({
|
|
|
85987
86998
|
form: "single",
|
|
85988
86999
|
optional: false
|
|
85989
87000
|
}],
|
|
87001
|
+
"coreBlocks.proposeRunningThreshold": [{
|
|
87002
|
+
name: "deviceId",
|
|
87003
|
+
form: "single",
|
|
87004
|
+
optional: false
|
|
87005
|
+
}],
|
|
85990
87006
|
"cover.close": [{
|
|
85991
87007
|
name: "deviceId",
|
|
85992
87008
|
form: "single",
|
|
@@ -86838,6 +87854,16 @@ Object.freeze({
|
|
|
86838
87854
|
form: "single",
|
|
86839
87855
|
optional: true
|
|
86840
87856
|
}],
|
|
87857
|
+
"mediaPlayer.channelDown": [{
|
|
87858
|
+
name: "deviceId",
|
|
87859
|
+
form: "single",
|
|
87860
|
+
optional: false
|
|
87861
|
+
}],
|
|
87862
|
+
"mediaPlayer.channelUp": [{
|
|
87863
|
+
name: "deviceId",
|
|
87864
|
+
form: "single",
|
|
87865
|
+
optional: false
|
|
87866
|
+
}],
|
|
86841
87867
|
"mediaPlayer.getStatus": [{
|
|
86842
87868
|
name: "deviceId",
|
|
86843
87869
|
form: "single",
|
|
@@ -86903,6 +87929,26 @@ Object.freeze({
|
|
|
86903
87929
|
form: "single",
|
|
86904
87930
|
optional: false
|
|
86905
87931
|
}],
|
|
87932
|
+
"mediaPlayer.turnOff": [{
|
|
87933
|
+
name: "deviceId",
|
|
87934
|
+
form: "single",
|
|
87935
|
+
optional: false
|
|
87936
|
+
}],
|
|
87937
|
+
"mediaPlayer.turnOn": [{
|
|
87938
|
+
name: "deviceId",
|
|
87939
|
+
form: "single",
|
|
87940
|
+
optional: false
|
|
87941
|
+
}],
|
|
87942
|
+
"mediaPlayer.volumeDown": [{
|
|
87943
|
+
name: "deviceId",
|
|
87944
|
+
form: "single",
|
|
87945
|
+
optional: false
|
|
87946
|
+
}],
|
|
87947
|
+
"mediaPlayer.volumeUp": [{
|
|
87948
|
+
name: "deviceId",
|
|
87949
|
+
form: "single",
|
|
87950
|
+
optional: false
|
|
87951
|
+
}],
|
|
86906
87952
|
"meter.getStatus": [{
|
|
86907
87953
|
name: "deviceId",
|
|
86908
87954
|
form: "single",
|
|
@@ -87212,21 +88258,6 @@ Object.freeze({
|
|
|
87212
88258
|
form: "single",
|
|
87213
88259
|
optional: false
|
|
87214
88260
|
}],
|
|
87215
|
-
"petFountain.setPumpInterval": [{
|
|
87216
|
-
name: "deviceId",
|
|
87217
|
-
form: "single",
|
|
87218
|
-
optional: false
|
|
87219
|
-
}],
|
|
87220
|
-
"petFountain.setPumpOffMinutes": [{
|
|
87221
|
-
name: "deviceId",
|
|
87222
|
-
form: "single",
|
|
87223
|
-
optional: false
|
|
87224
|
-
}],
|
|
87225
|
-
"petFountain.setPumpOnMinutes": [{
|
|
87226
|
-
name: "deviceId",
|
|
87227
|
-
form: "single",
|
|
87228
|
-
optional: false
|
|
87229
|
-
}],
|
|
87230
88261
|
"pipelineAnalytics.addRetrainFrameFromRecording": [{
|
|
87231
88262
|
name: "deviceId",
|
|
87232
88263
|
form: "single",
|
|
@@ -87862,6 +88893,26 @@ Object.freeze({
|
|
|
87862
88893
|
form: "single",
|
|
87863
88894
|
optional: false
|
|
87864
88895
|
}],
|
|
88896
|
+
"pumpCycle.getStatus": [{
|
|
88897
|
+
name: "deviceId",
|
|
88898
|
+
form: "single",
|
|
88899
|
+
optional: false
|
|
88900
|
+
}],
|
|
88901
|
+
"pumpCycle.setEnabled": [{
|
|
88902
|
+
name: "deviceId",
|
|
88903
|
+
form: "single",
|
|
88904
|
+
optional: false
|
|
88905
|
+
}],
|
|
88906
|
+
"pumpCycle.setOffMinutes": [{
|
|
88907
|
+
name: "deviceId",
|
|
88908
|
+
form: "single",
|
|
88909
|
+
optional: false
|
|
88910
|
+
}],
|
|
88911
|
+
"pumpCycle.setOnMinutes": [{
|
|
88912
|
+
name: "deviceId",
|
|
88913
|
+
form: "single",
|
|
88914
|
+
optional: false
|
|
88915
|
+
}],
|
|
87865
88916
|
"reboot.reboot": [{
|
|
87866
88917
|
name: "deviceId",
|
|
87867
88918
|
form: "single",
|
|
@@ -88063,6 +89114,11 @@ Object.freeze({
|
|
|
88063
89114
|
form: "single",
|
|
88064
89115
|
optional: true
|
|
88065
89116
|
}],
|
|
89117
|
+
"remote.getAssumedStatus": [{
|
|
89118
|
+
name: "deviceId",
|
|
89119
|
+
form: "single",
|
|
89120
|
+
optional: false
|
|
89121
|
+
}],
|
|
88066
89122
|
"remote.getConfig": [{
|
|
88067
89123
|
name: "deviceId",
|
|
88068
89124
|
form: "single",
|
|
@@ -88610,99 +89666,6 @@ Object.freeze({
|
|
|
88610
89666
|
"network-access": "ingress",
|
|
88611
89667
|
"smtp-provider": "email"
|
|
88612
89668
|
});
|
|
88613
|
-
/**
|
|
88614
|
-
* A saved Customize assignment (D936). The schema is the type: callers use
|
|
88615
|
-
* `z.infer`, they do not restate the shape.
|
|
88616
|
-
*/
|
|
88617
|
-
var OverrideSourceSchema = object({
|
|
88618
|
-
addonId: string().min(1),
|
|
88619
|
-
stableId: string().min(1),
|
|
88620
|
-
cap: string().min(1),
|
|
88621
|
-
fieldPath: string().min(1)
|
|
88622
|
-
});
|
|
88623
|
-
var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
|
|
88624
|
-
object({ kind: literal("identity") }),
|
|
88625
|
-
object({ kind: literal("invert") }),
|
|
88626
|
-
object({
|
|
88627
|
-
kind: literal("threshold"),
|
|
88628
|
-
comparison: _enum(["gte", "lte"]),
|
|
88629
|
-
threshold: number(),
|
|
88630
|
-
startDwellMs: number().nonnegative(),
|
|
88631
|
-
stopDwellMs: number().nonnegative()
|
|
88632
|
-
}),
|
|
88633
|
-
object({
|
|
88634
|
-
kind: literal("linear"),
|
|
88635
|
-
scale: number(),
|
|
88636
|
-
offset: number(),
|
|
88637
|
-
clampMin: number().optional(),
|
|
88638
|
-
clampMax: number().optional()
|
|
88639
|
-
}),
|
|
88640
|
-
object({
|
|
88641
|
-
kind: literal("enum-map"),
|
|
88642
|
-
mapping: record(string(), string().nullable())
|
|
88643
|
-
}),
|
|
88644
|
-
object({ kind: literal("number") })
|
|
88645
|
-
]);
|
|
88646
|
-
var ScalarOverrideDraftSchema = object({
|
|
88647
|
-
kind: literal("scalar"),
|
|
88648
|
-
cap: string().min(1),
|
|
88649
|
-
targetId: string().min(1),
|
|
88650
|
-
source: OverrideSourceSchema,
|
|
88651
|
-
transform: CustomizeTransformDraftSchema
|
|
88652
|
-
});
|
|
88653
|
-
var KeyedArrayOverrideDraftSchema = object({
|
|
88654
|
-
kind: literal("keyedArray"),
|
|
88655
|
-
cap: string().min(1),
|
|
88656
|
-
targetId: string().min(1),
|
|
88657
|
-
key: string().min(1),
|
|
88658
|
-
label: string().min(1),
|
|
88659
|
-
fieldPath: string().min(1),
|
|
88660
|
-
source: OverrideSourceSchema,
|
|
88661
|
-
transform: CustomizeTransformDraftSchema
|
|
88662
|
-
});
|
|
88663
|
-
var PulseOverrideDraftSchema = object({
|
|
88664
|
-
kind: literal("pulse"),
|
|
88665
|
-
cap: string().min(1),
|
|
88666
|
-
targetId: string().min(1),
|
|
88667
|
-
enabled: boolean(),
|
|
88668
|
-
debounceMs: number().nonnegative(),
|
|
88669
|
-
edges: array(_enum([
|
|
88670
|
-
"rising",
|
|
88671
|
-
"button",
|
|
88672
|
-
"native-event"
|
|
88673
|
-
])).min(1).optional(),
|
|
88674
|
-
sources: array(OverrideSourceSchema).min(1)
|
|
88675
|
-
});
|
|
88676
|
-
var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
|
|
88677
|
-
"in-addition",
|
|
88678
|
-
"confirm",
|
|
88679
|
-
"replace"
|
|
88680
|
-
]).optional() });
|
|
88681
|
-
discriminatedUnion("kind", [
|
|
88682
|
-
ScalarOverrideDraftSchema,
|
|
88683
|
-
KeyedArrayOverrideDraftSchema,
|
|
88684
|
-
PulseOverrideDraftSchema,
|
|
88685
|
-
object({
|
|
88686
|
-
kind: literal("span"),
|
|
88687
|
-
cap: string().min(1),
|
|
88688
|
-
targetId: string().min(1),
|
|
88689
|
-
timeoutMs: number().nonnegative(),
|
|
88690
|
-
cooldownMs: number().nonnegative().optional(),
|
|
88691
|
-
sources: array(SpanSourceDraftSchema)
|
|
88692
|
-
})
|
|
88693
|
-
]);
|
|
88694
|
-
object({
|
|
88695
|
-
targetId: string().min(1),
|
|
88696
|
-
configured: boolean(),
|
|
88697
|
-
source: _enum(["native", "assigned"]),
|
|
88698
|
-
state: _enum([
|
|
88699
|
-
"active",
|
|
88700
|
-
"stale",
|
|
88701
|
-
"released",
|
|
88702
|
-
"unavailable"
|
|
88703
|
-
]),
|
|
88704
|
-
reason: string().nullable()
|
|
88705
|
-
});
|
|
88706
89669
|
var G711_SCALE_CORRECTION_DB = {
|
|
88707
89670
|
PCMU: 20 * Math.log10(4),
|
|
88708
89671
|
PCMA: 20 * Math.log10(8)
|
|
@@ -89318,6 +90281,7 @@ var STATE_SUMMARY_VARS = [
|
|
|
89318
90281
|
[...RULE_TEXT],
|
|
89319
90282
|
([...RULE_TEXT], [...DEVICE_EVENTS]),
|
|
89320
90283
|
([...RULE_TEXT], [...COMBINED_ALARM]),
|
|
90284
|
+
([...RULE_TEXT], [...COMBINED_ALARM]),
|
|
89321
90285
|
([...RULE_TEXT], [...DEVICE_EVENTS]),
|
|
89322
90286
|
([...RULE_TEXT], [...BATTERY, ...CONSUMABLE]),
|
|
89323
90287
|
([...RULE_TEXT], [...CONSUMABLE]),
|
|
@@ -90079,13 +91043,20 @@ var GoogleBrokerEntrySchema = object({
|
|
|
90079
91043
|
calendarFilter: string().default(""),
|
|
90080
91044
|
/** Default From for Gmail send — usually the connected account email. */
|
|
90081
91045
|
fromAddress: string().default(""),
|
|
90082
|
-
fromName: string().default("")
|
|
91046
|
+
fromName: string().default(""),
|
|
91047
|
+
/**
|
|
91048
|
+
* The stamped calendar ids (`<brokerId>:<googleCalendarId>`) this broker imports. `undefined`
|
|
91049
|
+
* means "not moved here yet": the reader falls back to the shared `calendarIds` setting of
|
|
91050
|
+
* the addon until the migration copied it. Empty = import none.
|
|
91051
|
+
*/
|
|
91052
|
+
calendarIds: array(string()).optional()
|
|
90083
91053
|
});
|
|
90084
91054
|
var GoogleServicesConfigSchema = object({
|
|
90085
91055
|
brokers: array(GoogleBrokerEntrySchema).default([]),
|
|
90086
91056
|
/**
|
|
90087
|
-
*
|
|
90088
|
-
*
|
|
91057
|
+
* @deprecated The shared set (`brokerId:googleCalendarId`). Each broker owns its own
|
|
91058
|
+
* `calendarIds` now; this key is read only while a broker has none, and the migration
|
|
91059
|
+
* `google-services/2026-11-03-drop-shared-calendar-ids` deletes it.
|
|
90089
91060
|
*/
|
|
90090
91061
|
calendarIds: array(string()).default([]),
|
|
90091
91062
|
/** @deprecated Pre-broker singleton — cleared after migrateLegacyToBrokers. */
|
|
@@ -90163,14 +91134,15 @@ function settingsToBrokerFields(settings) {
|
|
|
90163
91134
|
clientSecret: parsed.clientSecret,
|
|
90164
91135
|
calendarFilter: calendarIdsToFilter(parsed),
|
|
90165
91136
|
fromAddress: parsed.fromAddress ?? "",
|
|
90166
|
-
fromName: parsed.fromName ?? ""
|
|
91137
|
+
fromName: parsed.fromName ?? "",
|
|
91138
|
+
...parsed.calendarIds !== void 0 ? { calendarIds: [...parsed.calendarIds] } : {}
|
|
90167
91139
|
};
|
|
90168
91140
|
}
|
|
90169
91141
|
function brokerEntryToSettings(entry) {
|
|
90170
91142
|
return {
|
|
90171
91143
|
clientId: entry.clientId,
|
|
90172
91144
|
clientSecret: entry.clientSecret,
|
|
90173
|
-
calendarIds: [...parseCalendarFilter(entry.calendarFilter)],
|
|
91145
|
+
calendarIds: [...entry.calendarIds ?? parseCalendarFilter(entry.calendarFilter)],
|
|
90174
91146
|
fromAddress: entry.fromAddress,
|
|
90175
91147
|
fromName: entry.fromName
|
|
90176
91148
|
};
|
|
@@ -90221,6 +91193,13 @@ function migrateLegacyToBrokers(rawConfig, legacyOauth) {
|
|
|
90221
91193
|
didMigrate: true
|
|
90222
91194
|
};
|
|
90223
91195
|
}
|
|
91196
|
+
/**
|
|
91197
|
+
* The stamped calendar ids one broker imports: its own set, or — while the migration has not
|
|
91198
|
+
* reached it — the shared setting older versions wrote, so the move never changes what is imported.
|
|
91199
|
+
*/
|
|
91200
|
+
function calendarIdsOfEntry(entry, shared) {
|
|
91201
|
+
return new Set(entry?.calendarIds ?? shared);
|
|
91202
|
+
}
|
|
90224
91203
|
function parseCalendarFilter(raw) {
|
|
90225
91204
|
const trimmed = raw.trim();
|
|
90226
91205
|
if (!trimmed) return /* @__PURE__ */ new Set();
|
|
@@ -90307,6 +91286,7 @@ var GoogleBrokerRegistry = class {
|
|
|
90307
91286
|
const entry = GoogleBrokerEntrySchema.parse({
|
|
90308
91287
|
id,
|
|
90309
91288
|
name,
|
|
91289
|
+
calendarIds: [],
|
|
90310
91290
|
...fields
|
|
90311
91291
|
});
|
|
90312
91292
|
this.entries = [...this.entries, entry];
|
|
@@ -90320,6 +91300,7 @@ var GoogleBrokerRegistry = class {
|
|
|
90320
91300
|
const next = GoogleBrokerEntrySchema.parse({
|
|
90321
91301
|
id: prev.id,
|
|
90322
91302
|
name: prev.name,
|
|
91303
|
+
...prev.calendarIds !== void 0 ? { calendarIds: prev.calendarIds } : {},
|
|
90323
91304
|
...fields
|
|
90324
91305
|
});
|
|
90325
91306
|
this.entries = this.entries.map((e, i) => i === idx ? next : e);
|
|
@@ -90525,6 +91506,24 @@ function buildGoogleBrokerSettingsSchema(publicOrigin) {
|
|
|
90525
91506
|
showToggle: true
|
|
90526
91507
|
}]
|
|
90527
91508
|
},
|
|
91509
|
+
{
|
|
91510
|
+
id: "google-calendars-import",
|
|
91511
|
+
title: "Calendars to import",
|
|
91512
|
+
titleKey: "google-services:googleBrokerSettings.s_google-calendars-import.title",
|
|
91513
|
+
description: "Nothing selected = no calendars imported from this account. Options appear once the account is signed in.",
|
|
91514
|
+
descriptionKey: "google-services:googleBrokerSettings.s_google-calendars-import.description",
|
|
91515
|
+
columns: 1,
|
|
91516
|
+
fields: [{
|
|
91517
|
+
type: "multiselect",
|
|
91518
|
+
key: "calendarIds",
|
|
91519
|
+
label: "Calendars",
|
|
91520
|
+
labelKey: "google-services:googleBrokerSettings.s_google-calendars-import.calendarIds.label",
|
|
91521
|
+
options: [],
|
|
91522
|
+
optionsFrom: "_calendarCatalog",
|
|
91523
|
+
description: "The calendars of this Google account that CamStack imports.",
|
|
91524
|
+
descriptionKey: "google-services:googleBrokerSettings.s_google-calendars-import.calendarIds.description"
|
|
91525
|
+
}]
|
|
91526
|
+
},
|
|
90528
91527
|
{
|
|
90529
91528
|
id: "gmail-send",
|
|
90530
91529
|
title: "Gmail send",
|
|
@@ -90548,7 +91547,7 @@ function buildGoogleBrokerSettingsSchema(publicOrigin) {
|
|
|
90548
91547
|
}
|
|
90549
91548
|
] };
|
|
90550
91549
|
}
|
|
90551
|
-
/** Connectivity Calendars / Email pages — calendar import picker
|
|
91550
|
+
/** Connectivity Calendars / Email pages — the calendar import picker is on each broker. */
|
|
90552
91551
|
function buildGoogleFeatureSettingsSchema(cap, publicOrigin) {
|
|
90553
91552
|
const redirect = escapeHtml(redirectUriDisplay(publicOrigin));
|
|
90554
91553
|
if (cap === "calendar-source") return { sections: [{
|
|
@@ -90564,27 +91563,10 @@ function buildGoogleFeatureSettingsSchema(cap, publicOrigin) {
|
|
|
90564
91563
|
format: "html",
|
|
90565
91564
|
content: [
|
|
90566
91565
|
"<p>Link a Google account under Connectivity → <strong>Brokers</strong> ",
|
|
90567
|
-
"(Client ID / secret + Sign in).
|
|
91566
|
+
"(Client ID / secret + Sign in). Pick the calendars to import on each broker. ",
|
|
90568
91567
|
`OAuth redirect URI: <code>${redirect}</code>.</p>`
|
|
90569
91568
|
].join("")
|
|
90570
91569
|
}]
|
|
90571
|
-
}, {
|
|
90572
|
-
id: "google-calendars-import",
|
|
90573
|
-
title: "Calendars to import",
|
|
90574
|
-
titleKey: "google-services:googleFeatureSettings.s_google-calendars-import.title",
|
|
90575
|
-
description: "Nothing selected = no calendars imported. Options appear after at least one Google broker is signed in.",
|
|
90576
|
-
descriptionKey: "google-services:googleFeatureSettings.s_google-calendars-import.description",
|
|
90577
|
-
columns: 1,
|
|
90578
|
-
fields: [{
|
|
90579
|
-
type: "multiselect",
|
|
90580
|
-
key: "calendarIds",
|
|
90581
|
-
label: "Calendars",
|
|
90582
|
-
labelKey: "google-services:googleFeatureSettings.s_google-calendars-import.calendarIds.label",
|
|
90583
|
-
options: [],
|
|
90584
|
-
optionsFrom: "_calendarCatalog",
|
|
90585
|
-
description: "Ids are stamped as brokerId:googleCalendarId across every Google broker.",
|
|
90586
|
-
descriptionKey: "google-services:googleFeatureSettings.s_google-calendars-import.calendarIds.description"
|
|
90587
|
-
}]
|
|
90588
91570
|
}] };
|
|
90589
91571
|
return { sections: [{
|
|
90590
91572
|
id: "setup-intro",
|
|
@@ -90674,11 +91656,11 @@ function createGoogleCalendarApi(getAccessToken, http = { fetch }) {
|
|
|
90674
91656
|
function createGoogleCalendarSource(deps) {
|
|
90675
91657
|
return {
|
|
90676
91658
|
listCalendars: async () => {
|
|
90677
|
-
const selected = deps.selectedCalendarIds();
|
|
90678
|
-
if (selected.size === 0) return [];
|
|
90679
91659
|
const out = [];
|
|
90680
91660
|
for (const account of deps.accounts()) {
|
|
90681
91661
|
if (!account.connected) continue;
|
|
91662
|
+
const selected = deps.selectedCalendarIds(account.brokerId);
|
|
91663
|
+
if (selected.size === 0) continue;
|
|
90682
91664
|
try {
|
|
90683
91665
|
const list = await account.api.listCalendars();
|
|
90684
91666
|
for (const c of list) {
|
|
@@ -90701,20 +91683,18 @@ function createGoogleCalendarSource(deps) {
|
|
|
90701
91683
|
return out;
|
|
90702
91684
|
},
|
|
90703
91685
|
listEvents: async (input) => {
|
|
90704
|
-
const selected = deps.selectedCalendarIds();
|
|
90705
|
-
if (selected.size === 0) return [];
|
|
90706
91686
|
const timeMin = new Date(input.startMs).toISOString();
|
|
90707
91687
|
const timeMax = new Date(input.endMs).toISOString();
|
|
90708
91688
|
const accounts = deps.accounts().filter((a) => a.connected);
|
|
90709
91689
|
const byBroker = new Map(accounts.map((a) => [a.brokerId, a]));
|
|
90710
|
-
const requestIds = input.calendarIds !== void 0 && input.calendarIds.length > 0 ? input.calendarIds : [...
|
|
91690
|
+
const requestIds = input.calendarIds !== void 0 && input.calendarIds.length > 0 ? input.calendarIds : [...new Set(deps.accounts().flatMap((account) => [...deps.selectedCalendarIds(account.brokerId)].filter((stamped) => parseStampedCalendarId(stamped)?.brokerId === account.brokerId)))];
|
|
90711
91691
|
/** brokerId → google calendar ids to fetch */
|
|
90712
91692
|
const wanted = /* @__PURE__ */ new Map();
|
|
90713
91693
|
for (const stamped of requestIds) {
|
|
90714
|
-
if (!selected.has(stamped)) continue;
|
|
90715
91694
|
const parsed = parseStampedCalendarId(stamped);
|
|
90716
91695
|
if (parsed === null) continue;
|
|
90717
91696
|
if (!byBroker.has(parsed.brokerId)) continue;
|
|
91697
|
+
if (!deps.selectedCalendarIds(parsed.brokerId).has(stamped)) continue;
|
|
90718
91698
|
const list = wanted.get(parsed.brokerId) ?? [];
|
|
90719
91699
|
list.push(parsed.googleCalendarId);
|
|
90720
91700
|
wanted.set(parsed.brokerId, list);
|
|
@@ -91063,11 +92043,233 @@ var googleBrokersEntryKind = defineEntryKind("brokers", {
|
|
|
91063
92043
|
clientSecret: GoogleBrokerEntrySchema.shape.clientSecret,
|
|
91064
92044
|
calendarFilter: GoogleBrokerEntrySchema.shape.calendarFilter,
|
|
91065
92045
|
fromAddress: GoogleBrokerEntrySchema.shape.fromAddress,
|
|
91066
|
-
fromName: GoogleBrokerEntrySchema.shape.fromName
|
|
92046
|
+
fromName: GoogleBrokerEntrySchema.shape.fromName,
|
|
92047
|
+
calendarIds: GoogleBrokerEntrySchema.shape.calendarIds
|
|
91067
92048
|
}),
|
|
91068
92049
|
writeTrigger: "an operator adds or edits a Google OAuth broker"
|
|
91069
92050
|
});
|
|
91070
92051
|
//#endregion
|
|
92052
|
+
//#region src/google-instances.ts
|
|
92053
|
+
/**
|
|
92054
|
+
* The Google accounts as `integration-instances` (planning#408 P3): one instance per broker
|
|
92055
|
+
* record, over the SAME `brokers` records and ids the `broker` cap serves (`google_NNN`), so
|
|
92056
|
+
* nothing moves. The OAuth session lives beside the record (`oauthAccounts`), so an instance's
|
|
92057
|
+
* status is the registry's answer and a change re-seeds the registry.
|
|
92058
|
+
*/
|
|
92059
|
+
function asTestResult(r) {
|
|
92060
|
+
return r.ok ? {
|
|
92061
|
+
success: true,
|
|
92062
|
+
status: "validated",
|
|
92063
|
+
...r.latencyMs !== void 0 ? { latencyMs: r.latencyMs } : {}
|
|
92064
|
+
} : {
|
|
92065
|
+
success: false,
|
|
92066
|
+
status: "rejected",
|
|
92067
|
+
error: r.error ?? "Google rejected the check"
|
|
92068
|
+
};
|
|
92069
|
+
}
|
|
92070
|
+
function buildGoogleInstances(records, deps) {
|
|
92071
|
+
return new RecordIntegrationInstances(records, googleBrokersEntryKind, {
|
|
92072
|
+
description: {
|
|
92073
|
+
instanceMode: "multiple",
|
|
92074
|
+
kind: "account",
|
|
92075
|
+
canTest: true,
|
|
92076
|
+
kinds: [{
|
|
92077
|
+
id: GOOGLE_BROKER_KIND,
|
|
92078
|
+
label: GOOGLE_BROKER_KIND_LABEL,
|
|
92079
|
+
connectPath: deps.connectPath,
|
|
92080
|
+
disconnectPath: deps.disconnectPath
|
|
92081
|
+
}],
|
|
92082
|
+
allowRestoreId: true
|
|
92083
|
+
},
|
|
92084
|
+
formSchema: () => {
|
|
92085
|
+
const full = buildGoogleBrokerSettingsSchema(deps.publicOrigin());
|
|
92086
|
+
return {
|
|
92087
|
+
...full,
|
|
92088
|
+
sections: full.sections.filter((s) => s.id !== "google-calendars-import")
|
|
92089
|
+
};
|
|
92090
|
+
},
|
|
92091
|
+
initialBody: (name) => ({
|
|
92092
|
+
name,
|
|
92093
|
+
clientId: "",
|
|
92094
|
+
clientSecret: "",
|
|
92095
|
+
calendarFilter: "",
|
|
92096
|
+
fromAddress: "",
|
|
92097
|
+
fromName: "",
|
|
92098
|
+
calendarIds: []
|
|
92099
|
+
}),
|
|
92100
|
+
mintId: deps.mintId,
|
|
92101
|
+
kindOf: () => GOOGLE_BROKER_KIND,
|
|
92102
|
+
health: (_body, id) => {
|
|
92103
|
+
const session = deps.session(id);
|
|
92104
|
+
if (session === "connected") return { status: {
|
|
92105
|
+
state: "ok",
|
|
92106
|
+
label: "Signed in"
|
|
92107
|
+
} };
|
|
92108
|
+
if (session === "reauth") return { status: {
|
|
92109
|
+
state: "error",
|
|
92110
|
+
label: "Sign in again",
|
|
92111
|
+
detail: "Google session expired"
|
|
92112
|
+
} };
|
|
92113
|
+
return { status: {
|
|
92114
|
+
state: "unknown",
|
|
92115
|
+
label: "Not signed in"
|
|
92116
|
+
} };
|
|
92117
|
+
},
|
|
92118
|
+
configured: (body) => body.clientId.length > 0 && (body.clientSecret ?? "").length > 0,
|
|
92119
|
+
onChanged: deps.onChanged,
|
|
92120
|
+
test: async ({ id, values }) => {
|
|
92121
|
+
const clientId = values["clientId"];
|
|
92122
|
+
const secret = values["clientSecret"];
|
|
92123
|
+
if (typeof clientId === "string" && clientId.length > 0 && typeof secret === "string" && secret.length > 0 && secret !== "__camstack_redacted__") return asTestResult(await deps.testClient(clientId, secret));
|
|
92124
|
+
if (id === null) return asTestResult({
|
|
92125
|
+
ok: false,
|
|
92126
|
+
error: "Enter the Client ID and Client secret first"
|
|
92127
|
+
});
|
|
92128
|
+
return asTestResult(await deps.testSaved(id));
|
|
92129
|
+
}
|
|
92130
|
+
});
|
|
92131
|
+
}
|
|
92132
|
+
//#endregion
|
|
92133
|
+
//#region src/migrations/calendar-ids-to-brokers.ts
|
|
92134
|
+
/**
|
|
92135
|
+
* The calendars a Google account imports are a fact of THAT account (D985): each broker record
|
|
92136
|
+
* carries its own `calendarIds`, and the addon-level `calendarIds` setting (one shared set of
|
|
92137
|
+
* `<brokerId>:<googleCalendarId>` ids) goes away.
|
|
92138
|
+
*
|
|
92139
|
+
* Two migrations, in this order:
|
|
92140
|
+
*
|
|
92141
|
+
* 1. `calendar-ids-to-brokers` (procedure) copies the shared set to EVERY broker record that has
|
|
92142
|
+
* no `calendarIds` yet. A record that already has one is never overwritten; a broker deleted
|
|
92143
|
+
* meanwhile is skipped, never recreated; a stored value that is not a list of strings is an
|
|
92144
|
+
* ambiguity and throws (nothing is guessed).
|
|
92145
|
+
* 2. `drop-shared-calendar-ids` (plan) deletes the shared setting, but only after a read-back:
|
|
92146
|
+
* every live broker must show its own `calendarIds`. When one of them holds a set that differs
|
|
92147
|
+
* from the shared one (an operator choice made before the copy), the shared value is the
|
|
92148
|
+
* loser and stays PARKED where it is, never deleted.
|
|
92149
|
+
*
|
|
92150
|
+
* Both are idempotent: with the copy landed the first reports `already-done`, and with the row
|
|
92151
|
+
* gone the second plans nothing.
|
|
92152
|
+
*/
|
|
92153
|
+
var GOOGLE_ADDON_ID = "google-services";
|
|
92154
|
+
/** The canonical settings table: one row per key, id `<addonId>.<key>`. */
|
|
92155
|
+
var ADDON_SETTINGS_TABLE = "addon-settings";
|
|
92156
|
+
/** The legacy shared set. */
|
|
92157
|
+
var SHARED_CALENDAR_IDS_KEY = "calendarIds";
|
|
92158
|
+
async function readSharedSet(ports) {
|
|
92159
|
+
const answer = await ports.readStored(SHARED_CALENDAR_IDS_KEY);
|
|
92160
|
+
if (answer.kind === "unreadable") throw new Error(`google-services: the shared calendarIds cannot be read (${answer.reason})`);
|
|
92161
|
+
if (answer.kind === "absent") return null;
|
|
92162
|
+
const value = answer.value;
|
|
92163
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) throw new Error("google-services: the shared calendarIds is not a list of ids — nothing copied");
|
|
92164
|
+
return value;
|
|
92165
|
+
}
|
|
92166
|
+
async function copyCalendarIdsToBrokers(ports) {
|
|
92167
|
+
const shared = await readSharedSet(ports);
|
|
92168
|
+
if (shared === null) return {
|
|
92169
|
+
outcome: "already-done",
|
|
92170
|
+
summary: { reason: "no-shared-set" }
|
|
92171
|
+
};
|
|
92172
|
+
let copied = 0;
|
|
92173
|
+
let kept = 0;
|
|
92174
|
+
let gone = 0;
|
|
92175
|
+
for (const broker of await ports.listBrokers()) {
|
|
92176
|
+
if (broker.calendarIds !== void 0) {
|
|
92177
|
+
kept += 1;
|
|
92178
|
+
continue;
|
|
92179
|
+
}
|
|
92180
|
+
if (await ports.setBrokerCalendarIds(broker.id, shared)) copied += 1;
|
|
92181
|
+
else gone += 1;
|
|
92182
|
+
}
|
|
92183
|
+
return {
|
|
92184
|
+
outcome: copied === 0 ? "already-done" : "done",
|
|
92185
|
+
summary: {
|
|
92186
|
+
copied,
|
|
92187
|
+
kept,
|
|
92188
|
+
gone
|
|
92189
|
+
}
|
|
92190
|
+
};
|
|
92191
|
+
}
|
|
92192
|
+
var sameSet = (a, b) => {
|
|
92193
|
+
const left = new Set(a);
|
|
92194
|
+
const right = new Set(b);
|
|
92195
|
+
return left.size === right.size && [...left].every((id) => right.has(id));
|
|
92196
|
+
};
|
|
92197
|
+
async function planDropSharedCalendarIds(ports) {
|
|
92198
|
+
const shared = await readSharedSet(ports);
|
|
92199
|
+
if (shared === null) return { edits: [] };
|
|
92200
|
+
const brokers = await ports.listBrokers();
|
|
92201
|
+
if (brokers.length === 0) return { edits: [] };
|
|
92202
|
+
const missing = brokers.filter((broker) => broker.calendarIds === void 0);
|
|
92203
|
+
if (missing.length > 0) throw new Error(`google-services: the shared calendarIds is not dropped, ${missing.map((b) => b.id).join(", ")} has no set of its own`);
|
|
92204
|
+
if (!brokers.every((broker) => sameSet(broker.calendarIds ?? [], shared))) return { edits: [] };
|
|
92205
|
+
const rowId = `${GOOGLE_ADDON_ID}.${SHARED_CALENDAR_IDS_KEY}`;
|
|
92206
|
+
return { edits: [{
|
|
92207
|
+
op: "deleteKeys",
|
|
92208
|
+
scope: ADDON_SETTINGS_TABLE,
|
|
92209
|
+
keys: [rowId],
|
|
92210
|
+
precondition: preconditionOf({ [rowId]: {
|
|
92211
|
+
addonId: GOOGLE_ADDON_ID,
|
|
92212
|
+
key: SHARED_CALENDAR_IDS_KEY,
|
|
92213
|
+
value: shared
|
|
92214
|
+
} })
|
|
92215
|
+
}] };
|
|
92216
|
+
}
|
|
92217
|
+
//#endregion
|
|
92218
|
+
//#region src/migrations/index.ts
|
|
92219
|
+
/** The `google-services` catalogue (D987): offered to the engine when the addon initialises. */
|
|
92220
|
+
var ADDON_MIGRATIONS = [{
|
|
92221
|
+
meta: {
|
|
92222
|
+
id: "google-services/2026-11-03-calendar-ids-to-brokers",
|
|
92223
|
+
owner: "google-services",
|
|
92224
|
+
level: "addon-state",
|
|
92225
|
+
seq: 1,
|
|
92226
|
+
dependsOn: [],
|
|
92227
|
+
store: "settings",
|
|
92228
|
+
scope: "cluster",
|
|
92229
|
+
onFailure: "tolerate",
|
|
92230
|
+
bodyKind: "procedure",
|
|
92231
|
+
description: "Copy the shared calendar set to every Google broker record that has none of its own",
|
|
92232
|
+
fixtureMark: "the client secret marker of a broker record the migration does not know"
|
|
92233
|
+
},
|
|
92234
|
+
procedure: copyCalendarIdsToBrokers
|
|
92235
|
+
}, {
|
|
92236
|
+
meta: {
|
|
92237
|
+
id: "google-services/2026-11-03-drop-shared-calendar-ids",
|
|
92238
|
+
owner: "google-services",
|
|
92239
|
+
level: "addon-state",
|
|
92240
|
+
seq: 2,
|
|
92241
|
+
dependsOn: ["google-services/2026-11-03-calendar-ids-to-brokers"],
|
|
92242
|
+
store: "settings",
|
|
92243
|
+
scope: "cluster",
|
|
92244
|
+
onFailure: "tolerate",
|
|
92245
|
+
bodyKind: "plan",
|
|
92246
|
+
description: "Delete the shared calendar set once every broker shows its own",
|
|
92247
|
+
fixtureMark: "the shared set parked when a broker holds a different one"
|
|
92248
|
+
},
|
|
92249
|
+
plan: planDropSharedCalendarIds
|
|
92250
|
+
}];
|
|
92251
|
+
//#endregion
|
|
92252
|
+
//#region src/migrations/ports.ts
|
|
92253
|
+
/** The migration's ports over the `brokers` record kind and the addon's raw settings read. */
|
|
92254
|
+
function calendarIdsPortsOver(kind, readStored) {
|
|
92255
|
+
return {
|
|
92256
|
+
readStored,
|
|
92257
|
+
listBrokers: async () => {
|
|
92258
|
+
return (await kind.query({ limit: 1e3 })).records.map((record) => ({
|
|
92259
|
+
id: record.id,
|
|
92260
|
+
calendarIds: record.data.calendarIds
|
|
92261
|
+
}));
|
|
92262
|
+
},
|
|
92263
|
+
setBrokerCalendarIds: async (id, ids) => {
|
|
92264
|
+
const row = await kind.get(id);
|
|
92265
|
+
if (row === null) return false;
|
|
92266
|
+
if (row.data.calendarIds !== void 0) return true;
|
|
92267
|
+
await kind.patch(id, { calendarIds: [...ids] });
|
|
92268
|
+
return true;
|
|
92269
|
+
}
|
|
92270
|
+
};
|
|
92271
|
+
}
|
|
92272
|
+
//#endregion
|
|
91071
92273
|
//#region src/google-broker-store.ts
|
|
91072
92274
|
function rowToEntry(id, data) {
|
|
91073
92275
|
const parsed = GoogleBrokerEntrySchema.safeParse({
|
|
@@ -91105,14 +92307,15 @@ var GoogleBrokerStore = class GoogleBrokerStore {
|
|
|
91105
92307
|
const nextIds = new Set(entries.map((e) => e.id));
|
|
91106
92308
|
for (const row of page.records) if (!nextIds.has(row.id)) await this.kind.delete(row.id);
|
|
91107
92309
|
for (const entry of entries) {
|
|
91108
|
-
const { id, name, clientId, clientSecret, calendarFilter, fromAddress, fromName } = entry;
|
|
92310
|
+
const { id, name, clientId, clientSecret, calendarFilter, fromAddress, fromName, calendarIds } = entry;
|
|
91109
92311
|
const body = {
|
|
91110
92312
|
name,
|
|
91111
92313
|
clientId,
|
|
91112
92314
|
clientSecret,
|
|
91113
92315
|
calendarFilter,
|
|
91114
92316
|
fromAddress,
|
|
91115
|
-
fromName
|
|
92317
|
+
fromName,
|
|
92318
|
+
...calendarIds !== void 0 ? { calendarIds } : {}
|
|
91116
92319
|
};
|
|
91117
92320
|
const parsed = googleBrokersEntryKind.schema.safeParse(body);
|
|
91118
92321
|
if (!parsed.success) throw new Error(`invalid Google broker '${id}': ${parsed.error.message}`);
|
|
@@ -91157,6 +92360,7 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
91157
92360
|
oauthAccountsHandle = null;
|
|
91158
92361
|
registry = new GoogleBrokerRegistry();
|
|
91159
92362
|
brokerStore = null;
|
|
92363
|
+
instances = null;
|
|
91160
92364
|
activeBrokers = [];
|
|
91161
92365
|
/** CSRF state → { brokerId, issuedAt } for the in-flight OAuth consents. */
|
|
91162
92366
|
pendingStates = /* @__PURE__ */ new Map();
|
|
@@ -91204,11 +92408,13 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
91204
92408
|
if (records === void 0) throw new Error("google-services: ctx.records is required (hub-only addon)");
|
|
91205
92409
|
await records.declare({ brokers: googleBrokersEntryKind });
|
|
91206
92410
|
this.brokerStore = GoogleBrokerStore.of(records);
|
|
92411
|
+
this.instances = this.buildInstances(records);
|
|
91207
92412
|
this.activeBrokers = await this.loadPersistedBrokers();
|
|
91208
92413
|
if ((await this.brokerStore.listResolved()).length === 0 && this.config.brokers.length > 0) {
|
|
91209
92414
|
await this.brokerStore.replaceAll(this.config.brokers);
|
|
91210
92415
|
this.activeBrokers = await this.loadPersistedBrokers();
|
|
91211
92416
|
}
|
|
92417
|
+
await this.migrateCalendarIds(records);
|
|
91212
92418
|
this.registry.seedFrom(this.activeBrokers, oauthAccounts.accounts);
|
|
91213
92419
|
await this.refreshPublicOrigin();
|
|
91214
92420
|
const calendarProvider = createGoogleCalendarSource({
|
|
@@ -91218,7 +92424,7 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
91218
92424
|
connected: this.registry.isConnected(entry.id),
|
|
91219
92425
|
api: createGoogleCalendarApi(() => this.ensureAccessToken(entry.id))
|
|
91220
92426
|
})),
|
|
91221
|
-
selectedCalendarIds: () =>
|
|
92427
|
+
selectedCalendarIds: (brokerId) => this.calendarIdsOf(brokerId)
|
|
91222
92428
|
});
|
|
91223
92429
|
const smtpProvider = createGmailSmtpProvider({ accounts: () => this.registry.listEntries().map((entry) => {
|
|
91224
92430
|
const oauth = this.registry.getOauth(entry.id);
|
|
@@ -91265,6 +92471,10 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
91265
92471
|
capability: brokerCapability,
|
|
91266
92472
|
provider: this.buildBrokerProvider()
|
|
91267
92473
|
},
|
|
92474
|
+
{
|
|
92475
|
+
capability: integrationInstancesCapability,
|
|
92476
|
+
provider: this.requireInstances().toProvider()
|
|
92477
|
+
},
|
|
91268
92478
|
{
|
|
91269
92479
|
capability: calendarSourceCapability,
|
|
91270
92480
|
provider: calendarProvider
|
|
@@ -91284,6 +92494,35 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
91284
92494
|
this.activeBrokers = await this.loadPersistedBrokers();
|
|
91285
92495
|
this.registry.seedFrom(this.activeBrokers, accounts);
|
|
91286
92496
|
}
|
|
92497
|
+
requireInstances() {
|
|
92498
|
+
if (this.instances === null) throw new Error("google-services: instances used before initialize");
|
|
92499
|
+
return this.instances;
|
|
92500
|
+
}
|
|
92501
|
+
/** The accounts as `integration-instances`, over the same `brokers` records (planning#408 P3). */
|
|
92502
|
+
buildInstances(records) {
|
|
92503
|
+
const brokerProvider = this.buildBrokerProvider();
|
|
92504
|
+
return buildGoogleInstances(records, {
|
|
92505
|
+
connectPath: `/addon/${ADDON_ID}${START_PATH}?brokerId={{brokerId}}`,
|
|
92506
|
+
disconnectPath: `/addon/${ADDON_ID}${DISCONNECT_PATH}?brokerId={{brokerId}}`,
|
|
92507
|
+
publicOrigin: () => this.publicOrigin,
|
|
92508
|
+
mintId: () => this.registry.allocateId(),
|
|
92509
|
+
session: (id) => this.registry.isConnected(id) ? "connected" : this.registry.needsReauth(id) ? "reauth" : "none",
|
|
92510
|
+
onChanged: async () => {
|
|
92511
|
+
const accounts = this.registry.listOauthAccounts();
|
|
92512
|
+
this.activeBrokers = await this.loadPersistedBrokers();
|
|
92513
|
+
this.registry.seedFrom(this.activeBrokers, accounts);
|
|
92514
|
+
await this.persistOauthAccounts();
|
|
92515
|
+
},
|
|
92516
|
+
testSaved: async (id) => brokerProvider.testConnection({ id }),
|
|
92517
|
+
testClient: async (clientId, clientSecret) => brokerProvider.testSettings({
|
|
92518
|
+
kind: GOOGLE_BROKER_KIND,
|
|
92519
|
+
settings: {
|
|
92520
|
+
clientId,
|
|
92521
|
+
clientSecret
|
|
92522
|
+
}
|
|
92523
|
+
})
|
|
92524
|
+
});
|
|
92525
|
+
}
|
|
91287
92526
|
buildBrokerProvider() {
|
|
91288
92527
|
const ownerAddonId = this.ctx.id;
|
|
91289
92528
|
const owns = (id) => this.registry.getEntry(id) !== null;
|
|
@@ -91389,7 +92628,10 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
91389
92628
|
getSettings: async ({ id }) => {
|
|
91390
92629
|
const entry = this.registry.getEntry(id);
|
|
91391
92630
|
if (entry === null) return null;
|
|
91392
|
-
return
|
|
92631
|
+
return {
|
|
92632
|
+
...brokerEntryToSettings(entry),
|
|
92633
|
+
_calendarCatalog: await this.loadStampedCalendarCatalog(id)
|
|
92634
|
+
};
|
|
91393
92635
|
},
|
|
91394
92636
|
setSettings: async ({ id, settings }) => {
|
|
91395
92637
|
if (!owns(id)) return;
|
|
@@ -91402,6 +92644,7 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
91402
92644
|
const oauth = this.registry.getOauth(id);
|
|
91403
92645
|
return {
|
|
91404
92646
|
...brokerEntryToSettings(entry),
|
|
92647
|
+
_calendarCatalog: await this.loadStampedCalendarCatalog(id),
|
|
91405
92648
|
accountEmail: oauth?.accountEmail ?? "",
|
|
91406
92649
|
connected: this.registry.isConnected(id)
|
|
91407
92650
|
};
|
|
@@ -91607,6 +92850,13 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
91607
92850
|
const cutoff = Date.now() - 10 * 6e4;
|
|
91608
92851
|
for (const [k, v] of this.pendingStates) if (v.issuedAt < cutoff) this.pendingStates.delete(k);
|
|
91609
92852
|
}
|
|
92853
|
+
/**
|
|
92854
|
+
* The shared calendar set becomes each broker's own (D985, D987). Brokers are reloaded once the
|
|
92855
|
+
* copy landed, so the registry is seeded with the sets the migration wrote.
|
|
92856
|
+
*/
|
|
92857
|
+
async migrateCalendarIds(records) {
|
|
92858
|
+
if ((await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, calendarIdsPortsOver(records.kind(googleBrokersEntryKind), (key) => this.readStoredGlobalKey(key))))).applied.length > 0) this.activeBrokers = await this.loadPersistedBrokers();
|
|
92859
|
+
}
|
|
91610
92860
|
async loadPersistedBrokers() {
|
|
91611
92861
|
const store = this.brokerStore;
|
|
91612
92862
|
if (store !== null) {
|
|
@@ -91630,9 +92880,10 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
91630
92880
|
await this.oauthAccountsHandle.set({ accounts: [...this.registry.listOauthAccounts()] });
|
|
91631
92881
|
}
|
|
91632
92882
|
/** Stamped catalog for Connectivity → Calendars (`optionsFrom`). */
|
|
91633
|
-
async loadStampedCalendarCatalog() {
|
|
92883
|
+
async loadStampedCalendarCatalog(onlyBrokerId) {
|
|
91634
92884
|
const out = [];
|
|
91635
92885
|
for (const entry of this.registry.listEntries()) {
|
|
92886
|
+
if (onlyBrokerId !== void 0 && entry.id !== onlyBrokerId) continue;
|
|
91636
92887
|
if (!this.registry.isConnected(entry.id)) continue;
|
|
91637
92888
|
try {
|
|
91638
92889
|
const list = await createGoogleCalendarApi(() => this.ensureAccessToken(entry.id)).listCalendars();
|
|
@@ -91772,13 +93023,15 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
91772
93023
|
}
|
|
91773
93024
|
async getGlobalSettings(overlay, cap, nodeId) {
|
|
91774
93025
|
await this.refreshPublicOrigin();
|
|
91775
|
-
|
|
91776
|
-
|
|
91777
|
-
|
|
91778
|
-
|
|
91779
|
-
|
|
91780
|
-
|
|
91781
|
-
|
|
93026
|
+
return super.getGlobalSettings(overlay, cap, nodeId);
|
|
93027
|
+
}
|
|
93028
|
+
/**
|
|
93029
|
+
* The stamped calendar ids ONE broker imports. A broker the migration has not reached yet
|
|
93030
|
+
* (no `calendarIds` of its own) reads the shared setting older versions wrote, so the move
|
|
93031
|
+
* never changes what is imported mid-way.
|
|
93032
|
+
*/
|
|
93033
|
+
calendarIdsOf(brokerId) {
|
|
93034
|
+
return calendarIdsOfEntry(this.registry.getEntry(brokerId), this.config.calendarIds ?? []);
|
|
91782
93035
|
}
|
|
91783
93036
|
};
|
|
91784
93037
|
function asSettingsCap(cap) {
|