@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
|
@@ -785,7 +785,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
785
785
|
return EventCategory;
|
|
786
786
|
}({});
|
|
787
787
|
//#endregion
|
|
788
|
-
//#region ../types/dist/device-type-
|
|
788
|
+
//#region ../types/dist/device-type-D1fH5kaC.mjs
|
|
789
789
|
/**
|
|
790
790
|
import { errMsg } from '@camstack/types'
|
|
791
791
|
* Extract a human-readable message from an unknown error value.
|
|
@@ -974,6 +974,9 @@ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
|
|
|
974
974
|
DeviceFeature["CoverPositionable"] = "cover-positionable";
|
|
975
975
|
/** Cover exposes slat-tilt control. Gates the tilt slider UI. */
|
|
976
976
|
DeviceFeature["CoverTilt"] = "cover-tilt";
|
|
977
|
+
/** Cover has a real stop: `stop` halts the travel in place. Gates the stop
|
|
978
|
+
* control in exporters; a provider whose `stop` is a no-op never declares it. */
|
|
979
|
+
DeviceFeature["CoverStoppable"] = "cover-stoppable";
|
|
977
980
|
/** Valve exposes intermediate position control (0..100). Gates the
|
|
978
981
|
* position slider / drag surface UI. */
|
|
979
982
|
DeviceFeature["ValvePositionable"] = "valve-positionable";
|
|
@@ -1013,6 +1016,12 @@ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
|
|
|
1013
1016
|
DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
|
|
1014
1017
|
/** Media player exposes stop distinct from pause. */
|
|
1015
1018
|
DeviceFeature["MediaPlayerStop"] = "media-player-stop";
|
|
1019
|
+
/** Media player exposes volume up / down in steps (an infrared TV has no level). */
|
|
1020
|
+
DeviceFeature["MediaPlayerVolumeStep"] = "media-player-volume-step";
|
|
1021
|
+
/** Media player exposes channel up / down in steps. */
|
|
1022
|
+
DeviceFeature["MediaPlayerChannelStep"] = "media-player-channel-step";
|
|
1023
|
+
/** Media player exposes power on / off. */
|
|
1024
|
+
DeviceFeature["MediaPlayerPower"] = "media-player-power";
|
|
1016
1025
|
/** Alarm panel requires a PIN code on arm/disarm. */
|
|
1017
1026
|
DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
|
|
1018
1027
|
/** Presence device carries GPS coordinates (lat/lng/accuracy) in
|
|
@@ -5972,7 +5981,7 @@ var ZodIssueCode = {
|
|
|
5972
5981
|
var ZodFirstPartyTypeKind;
|
|
5973
5982
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
5974
5983
|
//#endregion
|
|
5975
|
-
//#region ../types/dist/sleep-
|
|
5984
|
+
//#region ../types/dist/sleep-BkzQdpzF.mjs
|
|
5976
5985
|
/**
|
|
5977
5986
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
5978
5987
|
* window to float samples (D455).
|
|
@@ -6051,6 +6060,22 @@ function buildAlawTable() {
|
|
|
6051
6060
|
}
|
|
6052
6061
|
buildUlawTable();
|
|
6053
6062
|
buildAlawTable();
|
|
6063
|
+
/**
|
|
6064
|
+
* An addon whose own migrations did not apply refuses to initialise (D987). The error is typed
|
|
6065
|
+
* so a runner hosting several addons can tell "this addon is blocked" from a crash and keep its
|
|
6066
|
+
* co-located siblings running. Recognised by NAME and shape, never `instanceof`: a forked addon
|
|
6067
|
+
* bundles its own copy of this module, so the class the runner sees is not the one that threw.
|
|
6068
|
+
*/
|
|
6069
|
+
var MIGRATIONS_BLOCKED_ERROR_NAME = "AddonMigrationsBlockedError";
|
|
6070
|
+
var AddonMigrationsBlockedError = class extends Error {
|
|
6071
|
+
/** The ledger ids that did not apply and block the owner. */
|
|
6072
|
+
migrationIds;
|
|
6073
|
+
constructor(migrationIds) {
|
|
6074
|
+
super(`migration(s) ${migrationIds.join(", ")} did not apply and block this addon — fix or retry them (migrations.retry), then restart it`);
|
|
6075
|
+
this.name = MIGRATIONS_BLOCKED_ERROR_NAME;
|
|
6076
|
+
this.migrationIds = migrationIds;
|
|
6077
|
+
}
|
|
6078
|
+
};
|
|
6054
6079
|
/** Is this field's stored value a credential? */
|
|
6055
6080
|
function isSecretConfigField(field) {
|
|
6056
6081
|
if (field.type === "password") return true;
|
|
@@ -6104,9 +6129,16 @@ function isSecretMarker(value) {
|
|
|
6104
6129
|
const ref = value["$secret"];
|
|
6105
6130
|
return typeof ref === "string" && ref.length > 0;
|
|
6106
6131
|
}
|
|
6132
|
+
/** Anything carrying a `$secret` key — a stored marker or a UI view of one. */
|
|
6133
|
+
function isSecretMarkerLike(value) {
|
|
6134
|
+
return isRecord$7(value) && Object.prototype.hasOwnProperty.call(value, "$secret");
|
|
6135
|
+
}
|
|
6107
6136
|
function isSecretClear(value) {
|
|
6108
6137
|
return isRecord$7(value) && value["$secretClear"] === true;
|
|
6109
6138
|
}
|
|
6139
|
+
function isSecretFieldView(value) {
|
|
6140
|
+
return isRecord$7(value) && value["set"] === true && isSecretMarkerLike(value);
|
|
6141
|
+
}
|
|
6110
6142
|
/**
|
|
6111
6143
|
* The UI view of a stored secret field value: a marker becomes "set", inline
|
|
6112
6144
|
* plaintext becomes "set (inline)", nothing becomes `null`. A value never
|
|
@@ -6488,6 +6520,53 @@ function emitReadiness(bus, params) {
|
|
|
6488
6520
|
ts
|
|
6489
6521
|
}));
|
|
6490
6522
|
}
|
|
6523
|
+
/** Why a catalogue entry is not a runnable body of its declared kind. */
|
|
6524
|
+
var AddonMigrationDefinitionError = class extends Error {
|
|
6525
|
+
id;
|
|
6526
|
+
constructor(id, message) {
|
|
6527
|
+
super(`${id}: ${message}`);
|
|
6528
|
+
this.id = id;
|
|
6529
|
+
this.name = "AddonMigrationDefinitionError";
|
|
6530
|
+
}
|
|
6531
|
+
};
|
|
6532
|
+
/**
|
|
6533
|
+
* Bind a catalogue to the addon's ports. Throws {@link AddonMigrationDefinitionError} when
|
|
6534
|
+
* an entry's `bodyKind` does not match the body it carries, or is `sql` (a hub-only
|
|
6535
|
+
* body). A throwing body never escapes `execute`: it is reported as a failed result.
|
|
6536
|
+
*/
|
|
6537
|
+
function defineAddonMigrations(catalogue, ports) {
|
|
6538
|
+
return catalogue.map((entry) => {
|
|
6539
|
+
const { meta } = entry;
|
|
6540
|
+
if ("plan" in entry) {
|
|
6541
|
+
if (meta.bodyKind !== "plan") throw new AddonMigrationDefinitionError(meta.id, `carries a plan body but declares "${meta.bodyKind}"`);
|
|
6542
|
+
return {
|
|
6543
|
+
meta,
|
|
6544
|
+
execute: () => guarded(async () => ({
|
|
6545
|
+
kind: "plan",
|
|
6546
|
+
plan: await entry.plan(ports)
|
|
6547
|
+
}))
|
|
6548
|
+
};
|
|
6549
|
+
}
|
|
6550
|
+
if (meta.bodyKind !== "procedure") throw new AddonMigrationDefinitionError(meta.id, `carries a procedure body but declares "${meta.bodyKind}"`);
|
|
6551
|
+
return {
|
|
6552
|
+
meta,
|
|
6553
|
+
execute: () => guarded(async () => ({
|
|
6554
|
+
kind: "procedure",
|
|
6555
|
+
result: await entry.procedure(ports)
|
|
6556
|
+
}))
|
|
6557
|
+
};
|
|
6558
|
+
});
|
|
6559
|
+
}
|
|
6560
|
+
async function guarded(work) {
|
|
6561
|
+
try {
|
|
6562
|
+
return await work();
|
|
6563
|
+
} catch (err) {
|
|
6564
|
+
return {
|
|
6565
|
+
kind: "failed",
|
|
6566
|
+
error: errMsg(err)
|
|
6567
|
+
};
|
|
6568
|
+
}
|
|
6569
|
+
}
|
|
6491
6570
|
var NOT_RUN = {
|
|
6492
6571
|
declared: false,
|
|
6493
6572
|
snapshot: null,
|
|
@@ -6709,11 +6788,36 @@ function projectStore(store, perNodeKeys, nodeId) {
|
|
|
6709
6788
|
}
|
|
6710
6789
|
return out;
|
|
6711
6790
|
}
|
|
6791
|
+
/**
|
|
6792
|
+
* The address of one keychain value: `secret://<owner>/<scope>/<fieldPath>`.
|
|
6793
|
+
*
|
|
6794
|
+
* A ref is DETERMINISTIC from where the value lives — the owner addon, the
|
|
6795
|
+
* scope (global, one node, one device, one integration, one row of a business
|
|
6796
|
+
* collection) and the field path inside that scope — so saving the same field
|
|
6797
|
+
* twice updates the same ref, and the ref itself says which settings blob
|
|
6798
|
+
* points at it. It is also the AAD of the value's ciphertext: a ciphertext
|
|
6799
|
+
* copied onto another ref does not decrypt.
|
|
6800
|
+
*
|
|
6801
|
+
* Array items are addressed by ITEM ID, never by index
|
|
6802
|
+
* (`providers[authentik].clientSecret`), so reordering a list keeps every ref.
|
|
6803
|
+
*
|
|
6804
|
+
* Pure and browser-safe: no node builtins, no I/O.
|
|
6805
|
+
*/
|
|
6806
|
+
/** The URI scheme of every keychain ref. */
|
|
6807
|
+
var SECRET_REF_SCHEME = "secret://";
|
|
6712
6808
|
var GLOBAL_SCOPE = { kind: "global" };
|
|
6713
6809
|
/** The scope every addon-wide (not per-node, not per-device) setting lives in. */
|
|
6714
6810
|
function globalSecretScope() {
|
|
6715
6811
|
return GLOBAL_SCOPE;
|
|
6716
6812
|
}
|
|
6813
|
+
/**
|
|
6814
|
+
* An addon id as a ref owner. A hub in-process builtin's context id is
|
|
6815
|
+
* `addon:<id>`; the owner of its secrets is `<id>`, exactly as for the same
|
|
6816
|
+
* addon running forked.
|
|
6817
|
+
*/
|
|
6818
|
+
function normalizeSecretOwner(addonId) {
|
|
6819
|
+
return addonId.startsWith("addon:") ? addonId.slice(6) : addonId;
|
|
6820
|
+
}
|
|
6717
6821
|
function encodeSegment(value) {
|
|
6718
6822
|
return encodeURIComponent(value);
|
|
6719
6823
|
}
|
|
@@ -6724,6 +6828,16 @@ function decodeSegment(value) {
|
|
|
6724
6828
|
return null;
|
|
6725
6829
|
}
|
|
6726
6830
|
}
|
|
6831
|
+
/** The scope as it appears inside a ref (`global`, `node/<id>`, …). */
|
|
6832
|
+
function formatSecretScope(scope) {
|
|
6833
|
+
switch (scope.kind) {
|
|
6834
|
+
case "global": return "global";
|
|
6835
|
+
case "node": return `node/${encodeSegment(scope.nodeId)}`;
|
|
6836
|
+
case "device": return `device/${scope.deviceId}`;
|
|
6837
|
+
case "integration": return `integration/${encodeSegment(scope.integrationId)}`;
|
|
6838
|
+
case "row": return `row/${encodeSegment(scope.collection)}/${encodeSegment(scope.rowId)}`;
|
|
6839
|
+
}
|
|
6840
|
+
}
|
|
6727
6841
|
/**
|
|
6728
6842
|
* The field path of a value inside an array item, addressed by the item's id:
|
|
6729
6843
|
* `arrayItemFieldPath('providers', 'authentik', 'clientSecret')` →
|
|
@@ -6732,6 +6846,13 @@ function decodeSegment(value) {
|
|
|
6732
6846
|
function arrayItemFieldPath(arrayKey, itemId, fieldKey) {
|
|
6733
6847
|
return `${arrayKey}[${encodeSegment(itemId)}].${fieldKey}`;
|
|
6734
6848
|
}
|
|
6849
|
+
/** Build a ref. Throws on an empty owner or field path — both are bugs. */
|
|
6850
|
+
function buildSecretRef(parts) {
|
|
6851
|
+
const owner = normalizeSecretOwner(parts.owner);
|
|
6852
|
+
if (owner.length === 0) throw new Error("buildSecretRef: owner is empty");
|
|
6853
|
+
if (parts.fieldPath.length === 0) throw new Error("buildSecretRef: fieldPath is empty");
|
|
6854
|
+
return `${SECRET_REF_SCHEME}${encodeSegment(owner)}/${formatSecretScope(parts.scope)}/${parts.fieldPath}`;
|
|
6855
|
+
}
|
|
6735
6856
|
function parseScope(segments) {
|
|
6736
6857
|
switch (segments[0]) {
|
|
6737
6858
|
case "global": return {
|
|
@@ -7657,7 +7778,7 @@ var BaseAddon = class {
|
|
|
7657
7778
|
startMigration: (input) => api.startMigration.mutate(input),
|
|
7658
7779
|
finishMigration: (input) => api.finishMigration.mutate(input)
|
|
7659
7780
|
}, migrations, this.ctx.logger);
|
|
7660
|
-
if (report.blockOwner.length > 0) throw new
|
|
7781
|
+
if (report.blockOwner.length > 0) throw new AddonMigrationsBlockedError(report.blockOwner);
|
|
7661
7782
|
return report;
|
|
7662
7783
|
}
|
|
7663
7784
|
/**
|
|
@@ -7997,6 +8118,10 @@ var BaseAddon = class {
|
|
|
7997
8118
|
const stored = keychain ? await this.readAddonStoreWithRetry() : {};
|
|
7998
8119
|
const patch = {};
|
|
7999
8120
|
for (const key of keys) {
|
|
8121
|
+
if (tree.get(key)?.kind === "array") {
|
|
8122
|
+
patch[key] = [];
|
|
8123
|
+
continue;
|
|
8124
|
+
}
|
|
8000
8125
|
const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
|
|
8001
8126
|
patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
|
|
8002
8127
|
}
|
|
@@ -8685,6 +8810,8 @@ var RECORD_BATCH_MAX = 1e3;
|
|
|
8685
8810
|
var RECORD_QUERY_MAX = 1e3;
|
|
8686
8811
|
/** A kind in the `settings` file may not bound itself above this (D447: high volume is `events`). */
|
|
8687
8812
|
var RECORD_SETTINGS_MAX_ROWS = 5e4;
|
|
8813
|
+
/** The `Column`-free proof prefix: a ref collection for a kind's secret fields. */
|
|
8814
|
+
var RECORD_SECRET_COLLECTION_PREFIX = "records:";
|
|
8688
8815
|
/**
|
|
8689
8816
|
* Secret fields of a record, addressed the way a Zod schema declares them
|
|
8690
8817
|
* (`collectZodSecretPaths`): dotted keys, `[]` for "each element of this array"
|
|
@@ -8721,6 +8848,112 @@ function secretArrayPrefixes(path) {
|
|
|
8721
8848
|
}
|
|
8722
8849
|
return prefixes;
|
|
8723
8850
|
}
|
|
8851
|
+
function isRecord$4(value) {
|
|
8852
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8853
|
+
}
|
|
8854
|
+
function elementId(element, idKey, declared, prefix) {
|
|
8855
|
+
const id = isRecord$4(element) ? element[idKey] : void 0;
|
|
8856
|
+
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`);
|
|
8857
|
+
return id;
|
|
8858
|
+
}
|
|
8859
|
+
/**
|
|
8860
|
+
* Visit every secret leaf PRESENT in `data` (an absent branch has nothing to
|
|
8861
|
+
* visit). `replace` returns the new leaf value, or {@link REMOVE_LEAF}; the
|
|
8862
|
+
* result is a new body, `data` is untouched.
|
|
8863
|
+
*/
|
|
8864
|
+
var REMOVE_LEAF = Symbol("remove-leaf");
|
|
8865
|
+
function mapAt(node, segments, index, walkedKeys, concretePrefix, declared, arrayKeys, visit) {
|
|
8866
|
+
if (!isRecord$4(node)) return node;
|
|
8867
|
+
const segment = segments[index];
|
|
8868
|
+
if (segment === void 0) return node;
|
|
8869
|
+
if (!Object.prototype.hasOwnProperty.call(node, segment.key)) return node;
|
|
8870
|
+
const child = node[segment.key];
|
|
8871
|
+
const keys = [...walkedKeys, segment.key];
|
|
8872
|
+
const last = index === segments.length - 1;
|
|
8873
|
+
if (segment.each) {
|
|
8874
|
+
if (!Array.isArray(child)) return node;
|
|
8875
|
+
const prefix = keys.join(".");
|
|
8876
|
+
const idKey = arrayKeys[prefix];
|
|
8877
|
+
if (idKey === void 0) throw new InvalidRecordError(`"${declared}": no id key declared for the array "${prefix}"`);
|
|
8878
|
+
const mapped = child.map((element) => {
|
|
8879
|
+
const id = elementId(element, idKey, declared, prefix);
|
|
8880
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}[${encodeURIComponent(id)}]`;
|
|
8881
|
+
return last ? element : mapAt(element, segments, index + 1, keys, concrete, declared, arrayKeys, visit);
|
|
8882
|
+
});
|
|
8883
|
+
return {
|
|
8884
|
+
...node,
|
|
8885
|
+
[segment.key]: mapped
|
|
8886
|
+
};
|
|
8887
|
+
}
|
|
8888
|
+
const concrete = `${concretePrefix}${concretePrefix.length > 0 ? "." : ""}${segment.key}`;
|
|
8889
|
+
if (last) {
|
|
8890
|
+
const replacement = visit({
|
|
8891
|
+
declared,
|
|
8892
|
+
concrete,
|
|
8893
|
+
value: child
|
|
8894
|
+
});
|
|
8895
|
+
if (replacement === REMOVE_LEAF) return Object.fromEntries(Object.entries(node).filter(([key]) => key !== segment.key));
|
|
8896
|
+
return {
|
|
8897
|
+
...node,
|
|
8898
|
+
[segment.key]: replacement
|
|
8899
|
+
};
|
|
8900
|
+
}
|
|
8901
|
+
return {
|
|
8902
|
+
...node,
|
|
8903
|
+
[segment.key]: mapAt(child, segments, index + 1, keys, concrete, declared, arrayKeys, visit)
|
|
8904
|
+
};
|
|
8905
|
+
}
|
|
8906
|
+
/** Replace/remove every present secret leaf; returns a NEW body. */
|
|
8907
|
+
function mapSecretLeaves(data, paths, arrayKeys, visit) {
|
|
8908
|
+
let current = data;
|
|
8909
|
+
for (const declared of paths) current = mapAt(current, parseSecretPath(declared), 0, [], "", declared, arrayKeys, visit);
|
|
8910
|
+
return current;
|
|
8911
|
+
}
|
|
8912
|
+
/**
|
|
8913
|
+
* The keychain collection name a kind's secrets live under. Prefixed so a
|
|
8914
|
+
* record's refs never collide with the refs a legacy collection of the same name
|
|
8915
|
+
* already holds during an adoption.
|
|
8916
|
+
*/
|
|
8917
|
+
function recordSecretCollection(kind) {
|
|
8918
|
+
return `${RECORD_SECRET_COLLECTION_PREFIX}${kind}`;
|
|
8919
|
+
}
|
|
8920
|
+
/** The ONE ref a row's secret field may carry: the engine and the facade both build it here. */
|
|
8921
|
+
function recordSecretRef(owner, kind, rowId, concretePath) {
|
|
8922
|
+
return buildSecretRef({
|
|
8923
|
+
owner,
|
|
8924
|
+
scope: {
|
|
8925
|
+
kind: "row",
|
|
8926
|
+
collection: recordSecretCollection(kind),
|
|
8927
|
+
rowId
|
|
8928
|
+
},
|
|
8929
|
+
fieldPath: concretePath
|
|
8930
|
+
});
|
|
8931
|
+
}
|
|
8932
|
+
/**
|
|
8933
|
+
* A stored secret, as an admin read shows it: a VIEW naming the row's own
|
|
8934
|
+
* keychain ref (`set: true`), never the value and no longer the bare redaction
|
|
8935
|
+
* token. The form hands it to the secret field, which reveals the value by ref
|
|
8936
|
+
* through the audited `keychain-admin.reveal`; sent back untouched it keeps the
|
|
8937
|
+
* stored secret (the records door reads a view as KEEP).
|
|
8938
|
+
*
|
|
8939
|
+
* ONE implementation for the entries service and for every addon that serves
|
|
8940
|
+
* its own instances (`RecordIntegrationInstances`).
|
|
8941
|
+
*/
|
|
8942
|
+
function isRecord$3(value) {
|
|
8943
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8944
|
+
}
|
|
8945
|
+
function secretViewsOf(owner, kind, id, decl, data) {
|
|
8946
|
+
if (decl.secretPaths.length === 0) return data;
|
|
8947
|
+
const shown = mapSecretLeaves(data, decl.secretPaths, decl.secretArrayKeys, (leaf) => {
|
|
8948
|
+
if (leaf.value !== "__camstack_redacted__") return leaf.value;
|
|
8949
|
+
return {
|
|
8950
|
+
$secret: recordSecretRef(owner, kind, id, leaf.concrete),
|
|
8951
|
+
v: 1,
|
|
8952
|
+
set: true
|
|
8953
|
+
};
|
|
8954
|
+
});
|
|
8955
|
+
return isRecord$3(shown) ? shown : data;
|
|
8956
|
+
}
|
|
8724
8957
|
/**
|
|
8725
8958
|
* The share-view scope kinds a share-link token can carry. The hub mints
|
|
8726
8959
|
* and validates them (`ShareViewGrantSchema` builds its
|
|
@@ -8943,7 +9176,7 @@ function defineRecordKinds(specs) {
|
|
|
8943
9176
|
retention: spec.retention,
|
|
8944
9177
|
deviceRemoval: spec.deviceRemoval,
|
|
8945
9178
|
maxDataBytes: spec.maxDataBytes,
|
|
8946
|
-
secretPaths: [...secretPaths],
|
|
9179
|
+
secretPaths: [...new Set([...secretPaths, ...spec.retiredSecretPaths ?? []])],
|
|
8947
9180
|
secretArrayKeys: spec.secretArrayKeys === void 0 ? void 0 : { ...spec.secretArrayKeys },
|
|
8948
9181
|
transactFamily: spec.transactFamily,
|
|
8949
9182
|
entry: spec.entry === void 0 ? void 0 : {
|
|
@@ -8988,6 +9221,7 @@ function defineEntryKind(name, spec) {
|
|
|
8988
9221
|
...spec.database === void 0 ? {} : { database: spec.database },
|
|
8989
9222
|
...spec.indexes === void 0 ? {} : { indexes: spec.indexes },
|
|
8990
9223
|
...spec.secretArrayKeys === void 0 ? {} : { secretArrayKeys: spec.secretArrayKeys },
|
|
9224
|
+
...spec.retiredSecretPaths === void 0 ? {} : { retiredSecretPaths: spec.retiredSecretPaths },
|
|
8991
9225
|
entry: {
|
|
8992
9226
|
title: spec.title,
|
|
8993
9227
|
...spec.nameField === void 0 ? {} : { nameField: spec.nameField }
|
|
@@ -9648,6 +9882,7 @@ var CAP_NAMES = Object.freeze([
|
|
|
9648
9882
|
"backup",
|
|
9649
9883
|
"battery",
|
|
9650
9884
|
"binary",
|
|
9885
|
+
"board-key-source",
|
|
9651
9886
|
"brightness",
|
|
9652
9887
|
"broker",
|
|
9653
9888
|
"button",
|
|
@@ -9769,6 +10004,7 @@ var CAP_NAMES = Object.freeze([
|
|
|
9769
10004
|
"privacy-mask",
|
|
9770
10005
|
"ptz",
|
|
9771
10006
|
"ptz-autotrack",
|
|
10007
|
+
"pump-cycle",
|
|
9772
10008
|
"reboot",
|
|
9773
10009
|
"record-entries",
|
|
9774
10010
|
"recording",
|
|
@@ -11061,18 +11297,6 @@ Object.freeze({
|
|
|
11061
11297
|
"path": "pumpRunning",
|
|
11062
11298
|
"valueType": "boolean"
|
|
11063
11299
|
},
|
|
11064
|
-
{
|
|
11065
|
-
"path": "pumpIntervalEnabled",
|
|
11066
|
-
"valueType": "boolean"
|
|
11067
|
-
},
|
|
11068
|
-
{
|
|
11069
|
-
"path": "pumpOnTime",
|
|
11070
|
-
"valueType": "number"
|
|
11071
|
-
},
|
|
11072
|
-
{
|
|
11073
|
-
"path": "pumpOffTime",
|
|
11074
|
-
"valueType": "number"
|
|
11075
|
-
},
|
|
11076
11300
|
{
|
|
11077
11301
|
"path": "lastDrink",
|
|
11078
11302
|
"valueType": "number"
|
|
@@ -11196,6 +11420,20 @@ Object.freeze({
|
|
|
11196
11420
|
"valueType": "number"
|
|
11197
11421
|
}
|
|
11198
11422
|
],
|
|
11423
|
+
"pump-cycle": [
|
|
11424
|
+
{
|
|
11425
|
+
"path": "enabled",
|
|
11426
|
+
"valueType": "boolean"
|
|
11427
|
+
},
|
|
11428
|
+
{
|
|
11429
|
+
"path": "onTime",
|
|
11430
|
+
"valueType": "number"
|
|
11431
|
+
},
|
|
11432
|
+
{
|
|
11433
|
+
"path": "offTime",
|
|
11434
|
+
"valueType": "number"
|
|
11435
|
+
}
|
|
11436
|
+
],
|
|
11199
11437
|
"recording-archive": [
|
|
11200
11438
|
{
|
|
11201
11439
|
"path": "deviceId",
|
|
@@ -11659,7 +11897,7 @@ function userError(code, params, englishFallback) {
|
|
|
11659
11897
|
return new CamstackError(code, params, englishFallback);
|
|
11660
11898
|
}
|
|
11661
11899
|
//#endregion
|
|
11662
|
-
//#region ../types/dist/composition-
|
|
11900
|
+
//#region ../types/dist/composition-CRNwcFYY.mjs
|
|
11663
11901
|
/**
|
|
11664
11902
|
* The ffmpeg build every CamStack node downloads, per platform and arch.
|
|
11665
11903
|
*
|
|
@@ -12422,38 +12660,6 @@ function validateExpressionSource(src) {
|
|
|
12422
12660
|
return null;
|
|
12423
12661
|
}
|
|
12424
12662
|
/**
|
|
12425
|
-
* One power reading becomes a washer, dryer or dishwasher (D944 helper).
|
|
12426
|
-
*
|
|
12427
|
-
* Standby of the control board is under 5 W; a motor or a heater is far
|
|
12428
|
-
* above that, so 5 W is the line. A door lock or a short pump spike is not
|
|
12429
|
-
* a cycle, so running starts only after 30 s above the line.
|
|
12430
|
-
*
|
|
12431
|
-
* A wash, dry or dishwasher pause (soak, heat-wait, heater off between
|
|
12432
|
-
* bursts) lasts several minutes. Ending the cycle on the first dip would
|
|
12433
|
-
* report "finished" in the middle of the program, so the stop dwell is
|
|
12434
|
-
* 10 minutes on every type. The number is stored per type so one machine
|
|
12435
|
-
* can move later without moving the others.
|
|
12436
|
-
*/
|
|
12437
|
-
var APPLIANCE_TEMPLATE_ID = "appliance";
|
|
12438
|
-
var APPLIANCE_KINDS = [
|
|
12439
|
-
"washer",
|
|
12440
|
-
"dryer",
|
|
12441
|
-
"dishwasher"
|
|
12442
|
-
];
|
|
12443
|
-
DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
|
|
12444
|
-
/**
|
|
12445
|
-
* A dumb plug-in stove or heater, built from a smart plug (a `Heater`).
|
|
12446
|
-
*
|
|
12447
|
-
* The plug is the actuator: `switch.setState` is a FORWARD to the plug's own
|
|
12448
|
-
* (D967), so the heater switches exactly as the plug does. Running is the
|
|
12449
|
-
* plug being on; when a power reading is also named, running is the draw
|
|
12450
|
-
* staying above the line instead (a plug left on with the stove's own
|
|
12451
|
-
* thermostat open is not heating). 50 W is far above any standby and below
|
|
12452
|
-
* the smallest radiator.
|
|
12453
|
-
*/
|
|
12454
|
-
var HEATER_TEMPLATE_ID = "heater";
|
|
12455
|
-
DeviceType.Heater;
|
|
12456
|
-
/**
|
|
12457
12663
|
* Statistics and integrals over a history read (D944).
|
|
12458
12664
|
*
|
|
12459
12665
|
* Pure: the same points always give the same answer, so a restart recomputes
|
|
@@ -12597,34 +12803,6 @@ var HelperSpecSchema = discriminatedUnion("kind", [
|
|
|
12597
12803
|
/** Actuator groups only. Readings leave this unset. */
|
|
12598
12804
|
active: ActiveCriterionSchema.optional()
|
|
12599
12805
|
}),
|
|
12600
|
-
object({
|
|
12601
|
-
kind: literal("appliance"),
|
|
12602
|
-
templateId: literal(APPLIANCE_TEMPLATE_ID),
|
|
12603
|
-
appliance: _enum(APPLIANCE_KINDS),
|
|
12604
|
-
thresholdWatts: number().positive(),
|
|
12605
|
-
startDwellMs: number().int().nonnegative(),
|
|
12606
|
-
stopDwellMs: number().int().nonnegative(),
|
|
12607
|
-
member: HelperMemberSchema,
|
|
12608
|
-
/**
|
|
12609
|
-
* The plug that powers the appliance (`switch.on`). Optional: when set, the
|
|
12610
|
-
* composition carries a `power-source` feature, so the hub can cut and restore
|
|
12611
|
-
* that plug (power-source link of a NEW-target composition).
|
|
12612
|
-
*/
|
|
12613
|
-
powerSwitch: HelperMemberSchema.optional(),
|
|
12614
|
-
/** How long a forced restart keeps the plug off; the cap's default when unset. */
|
|
12615
|
-
offSeconds: number().optional()
|
|
12616
|
-
}),
|
|
12617
|
-
object({
|
|
12618
|
-
kind: literal("heater"),
|
|
12619
|
-
templateId: literal(HEATER_TEMPLATE_ID),
|
|
12620
|
-
/** The plug, read and commanded at `switch.on`. */
|
|
12621
|
-
plug: HelperMemberSchema,
|
|
12622
|
-
/** The plug's power reading, when it has one: running is then the draw, not the switch. */
|
|
12623
|
-
meter: HelperMemberSchema.optional(),
|
|
12624
|
-
thresholdWatts: number().positive(),
|
|
12625
|
-
startDwellMs: number().int().nonnegative(),
|
|
12626
|
-
stopDwellMs: number().int().nonnegative()
|
|
12627
|
-
}),
|
|
12628
12806
|
object({
|
|
12629
12807
|
kind: literal("statistic"),
|
|
12630
12808
|
stat: _enum(STATISTIC_STATS),
|
|
@@ -12796,7 +12974,14 @@ var CompositionForwardCommandSchema = object({
|
|
|
12796
12974
|
method: string().min(1),
|
|
12797
12975
|
args: record(string().min(1), CompositionForwardArgSchema).optional()
|
|
12798
12976
|
});
|
|
12799
|
-
var CompositionCommandTargetSchema = discriminatedUnion("kind", [
|
|
12977
|
+
var CompositionCommandTargetSchema = discriminatedUnion("kind", [
|
|
12978
|
+
CompositionForwardCommandSchema,
|
|
12979
|
+
CompositionCodeSourceSchema,
|
|
12980
|
+
object({
|
|
12981
|
+
kind: literal("extension"),
|
|
12982
|
+
target: literal("remote")
|
|
12983
|
+
})
|
|
12984
|
+
]);
|
|
12800
12985
|
/** An item key inside an item-array cap (`consumables.items[<key>]`). */
|
|
12801
12986
|
var COMPOSITION_ITEM_KEY_RE = /^[a-z0-9][a-z0-9-]{0,47}$/;
|
|
12802
12987
|
/**
|
|
@@ -12841,7 +13026,7 @@ var CompositionSchema = object({
|
|
|
12841
13026
|
kind: literal("existing"),
|
|
12842
13027
|
device: CompositionSourceRefSchema
|
|
12843
13028
|
})]),
|
|
12844
|
-
features: array(CompositionFeatureSchema).min(
|
|
13029
|
+
features: array(CompositionFeatureSchema).min(0).max(16),
|
|
12845
13030
|
/**
|
|
12846
13031
|
* Present when the block is a helper template. Absent on a free
|
|
12847
13032
|
* composition (the room example, device 4603). The editor reads this,
|
|
@@ -12873,6 +13058,73 @@ var NodeColorIdSchema = _enum([
|
|
|
12873
13058
|
"node-g",
|
|
12874
13059
|
"node-h"
|
|
12875
13060
|
]);
|
|
13061
|
+
//#endregion
|
|
13062
|
+
//#region ../types/dist/index.mjs
|
|
13063
|
+
var SMTP_INBOUND_DEFAULT_PORT = 2525;
|
|
13064
|
+
var SMTP_INBOUND_DEFAULT_DOMAIN = "camstack.local";
|
|
13065
|
+
var SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES = 25 * 1024 * 1024;
|
|
13066
|
+
/** The text excerpt a message carries on the bus. */
|
|
13067
|
+
var SMTP_INBOUND_TEXT_MAX_CHARS = 4096;
|
|
13068
|
+
var PortSchema = number().int().min(1).max(65535);
|
|
13069
|
+
/** A recipient local-part: what stands before the `@`. */
|
|
13070
|
+
var SmtpRecipientSchema = string().min(1).max(64).regex(/^[A-Za-z0-9._+-]+$/);
|
|
13071
|
+
object({
|
|
13072
|
+
port: PortSchema.default(SMTP_INBOUND_DEFAULT_PORT),
|
|
13073
|
+
bindHost: string().min(1).default("0.0.0.0"),
|
|
13074
|
+
domain: string().min(1).default(SMTP_INBOUND_DEFAULT_DOMAIN),
|
|
13075
|
+
requireAuth: boolean().default(true),
|
|
13076
|
+
username: string().default(""),
|
|
13077
|
+
password: string().default("").meta({ secret: true }),
|
|
13078
|
+
starttls: boolean().default(false),
|
|
13079
|
+
maxMessageBytes: number().int().positive().default(SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES)
|
|
13080
|
+
});
|
|
13081
|
+
object({
|
|
13082
|
+
host: string().min(1).nullable(),
|
|
13083
|
+
hostReason: string().nullable(),
|
|
13084
|
+
port: PortSchema,
|
|
13085
|
+
domain: string().min(1),
|
|
13086
|
+
requireAuth: boolean(),
|
|
13087
|
+
username: string(),
|
|
13088
|
+
password: string(),
|
|
13089
|
+
starttls: boolean()
|
|
13090
|
+
});
|
|
13091
|
+
object({
|
|
13092
|
+
recipients: array(SmtpRecipientSchema).min(1),
|
|
13093
|
+
/**
|
|
13094
|
+
* Who is subscribing. A second `subscribe` with the same id REPLACES that subscriber's
|
|
13095
|
+
* recipients (the way a consumer that restarted, or lost a device, declares its set again);
|
|
13096
|
+
* a recipient held by another subscriber is refused with `smtp-inbound/recipient-claimed`.
|
|
13097
|
+
* Absent, the subscriber is anonymous and unique.
|
|
13098
|
+
*/
|
|
13099
|
+
subscriberId: string().min(1).max(64).optional()
|
|
13100
|
+
});
|
|
13101
|
+
var SmtpInboundAttachmentSchema = object({
|
|
13102
|
+
filename: string(),
|
|
13103
|
+
contentType: string(),
|
|
13104
|
+
bytes: number().int().nonnegative()
|
|
13105
|
+
});
|
|
13106
|
+
/** The payload of one `broker.message` (the key is the recipient local-part). */
|
|
13107
|
+
var SmtpInboundMessageSchema = object({
|
|
13108
|
+
/** Monotonic per mailbox; a gap tells a consumer it missed an event. */
|
|
13109
|
+
seq: number().int().positive(),
|
|
13110
|
+
messageId: string(),
|
|
13111
|
+
receivedAt: number().int().nonnegative(),
|
|
13112
|
+
from: string(),
|
|
13113
|
+
to: string(),
|
|
13114
|
+
subject: string(),
|
|
13115
|
+
text: string().max(SMTP_INBOUND_TEXT_MAX_CHARS),
|
|
13116
|
+
attachments: array(SmtpInboundAttachmentSchema)
|
|
13117
|
+
});
|
|
13118
|
+
object({
|
|
13119
|
+
/** The newest seq issued so far (0 before any mail). */
|
|
13120
|
+
lastSeq: number().int().nonnegative(),
|
|
13121
|
+
/** The oldest seq still retained; messages below it are gone. */
|
|
13122
|
+
oldestSeq: number().int().nonnegative(),
|
|
13123
|
+
messages: array(object({
|
|
13124
|
+
key: string(),
|
|
13125
|
+
message: SmtpInboundMessageSchema
|
|
13126
|
+
}))
|
|
13127
|
+
});
|
|
12876
13128
|
new Set([
|
|
12877
13129
|
"track",
|
|
12878
13130
|
"face",
|
|
@@ -13109,6 +13361,157 @@ function buildCapturingReply(envelope) {
|
|
|
13109
13361
|
};
|
|
13110
13362
|
return wrapper;
|
|
13111
13363
|
}
|
|
13364
|
+
/** The most instances one addon lists (an integrations page, not a table). */
|
|
13365
|
+
var INSTANCES_LIST_MAX = 1e3;
|
|
13366
|
+
/** The keys of the kind's secret fields that sit at the top level of the body. */
|
|
13367
|
+
function topLevelSecretKeys(paths) {
|
|
13368
|
+
return new Set(paths.filter((p) => !p.includes(".") && !p.includes("[")));
|
|
13369
|
+
}
|
|
13370
|
+
var RecordIntegrationInstances = class {
|
|
13371
|
+
records;
|
|
13372
|
+
kind;
|
|
13373
|
+
hooks;
|
|
13374
|
+
handle;
|
|
13375
|
+
secretKeys;
|
|
13376
|
+
nameField;
|
|
13377
|
+
constructor(records, kind, hooks) {
|
|
13378
|
+
this.records = records;
|
|
13379
|
+
this.kind = kind;
|
|
13380
|
+
this.hooks = hooks;
|
|
13381
|
+
this.handle = records.kind(kind);
|
|
13382
|
+
this.secretKeys = topLevelSecretKeys(kind.declaration.secretPaths);
|
|
13383
|
+
this.nameField = kind.declaration.entry?.nameField ?? "name";
|
|
13384
|
+
}
|
|
13385
|
+
/**
|
|
13386
|
+
* One instance read for the addon's own runtime; null when absent. When the keychain is
|
|
13387
|
+
* locked the row is served WITHOUT its secrets and says so (`secrets: 'unavailable'`), so the
|
|
13388
|
+
* caller can keep what it had instead of reconfiguring with an empty secret (D49).
|
|
13389
|
+
*/
|
|
13390
|
+
async readResolved(id) {
|
|
13391
|
+
try {
|
|
13392
|
+
const record = await this.handle.getResolved(id);
|
|
13393
|
+
return record === null ? null : {
|
|
13394
|
+
id,
|
|
13395
|
+
body: record.data,
|
|
13396
|
+
enabled: record.data.enabled !== false,
|
|
13397
|
+
secrets: "resolved"
|
|
13398
|
+
};
|
|
13399
|
+
} catch (err) {
|
|
13400
|
+
if (!(err instanceof RecordSecretUnavailableError)) throw err;
|
|
13401
|
+
const stored = await this.handle.get(id);
|
|
13402
|
+
if (stored === null) return null;
|
|
13403
|
+
return {
|
|
13404
|
+
id,
|
|
13405
|
+
body: Object.fromEntries(Object.entries(stored.data).filter(([key]) => !this.secretKeys.has(key))),
|
|
13406
|
+
enabled: stored.data.enabled !== false,
|
|
13407
|
+
secrets: "unavailable"
|
|
13408
|
+
};
|
|
13409
|
+
}
|
|
13410
|
+
}
|
|
13411
|
+
/** The body of {@link readResolved} alone; a locked keychain yields the row without secrets. */
|
|
13412
|
+
async getResolved(id) {
|
|
13413
|
+
return (await this.readResolved(id))?.body ?? null;
|
|
13414
|
+
}
|
|
13415
|
+
/** Every instance, secrets resolved where the keychain allows; an absent one is skipped. */
|
|
13416
|
+
async listResolved() {
|
|
13417
|
+
const page = await this.handle.query({ limit: INSTANCES_LIST_MAX });
|
|
13418
|
+
const out = [];
|
|
13419
|
+
for (const record of page.records) {
|
|
13420
|
+
const read = await this.readResolved(record.id);
|
|
13421
|
+
if (read !== null) out.push(read);
|
|
13422
|
+
}
|
|
13423
|
+
return out;
|
|
13424
|
+
}
|
|
13425
|
+
viewOf(record) {
|
|
13426
|
+
const shown = secretViewsOf(this.records.owner, this.kind.name, record.id, this.kind.declaration, record.data);
|
|
13427
|
+
const rawName = record.data[this.nameField];
|
|
13428
|
+
const kind = this.hooks.kindOf?.(record.data);
|
|
13429
|
+
const health = this.hooks.health?.(record.data, record.id);
|
|
13430
|
+
return {
|
|
13431
|
+
id: record.id,
|
|
13432
|
+
name: typeof rawName === "string" ? rawName : record.id,
|
|
13433
|
+
configured: this.hooks.configured(record.data),
|
|
13434
|
+
enabled: record.data.enabled !== false,
|
|
13435
|
+
formValues: shown,
|
|
13436
|
+
...kind !== void 0 ? { kind } : {},
|
|
13437
|
+
...health?.status !== void 0 ? { status: health.status } : {},
|
|
13438
|
+
...health?.lastCheckedAt !== void 0 ? { lastCheckedAt: health.lastCheckedAt } : {}
|
|
13439
|
+
};
|
|
13440
|
+
}
|
|
13441
|
+
describe = async () => this.hooks.mintId === void 0 ? this.hooks.description : {
|
|
13442
|
+
...this.hooks.description,
|
|
13443
|
+
mintsId: true
|
|
13444
|
+
};
|
|
13445
|
+
instanceFormSchema = async ({ id }) => this.hooks.formSchema(id);
|
|
13446
|
+
listInstances = async () => {
|
|
13447
|
+
return (await this.handle.query({ limit: INSTANCES_LIST_MAX })).records.map((record) => this.viewOf(record));
|
|
13448
|
+
};
|
|
13449
|
+
getInstance = async ({ id }) => {
|
|
13450
|
+
const record = await this.handle.get(id);
|
|
13451
|
+
return record === null ? null : this.viewOf(record);
|
|
13452
|
+
};
|
|
13453
|
+
/** Insert ONE row (`ifRev: 0`: an existing id is refused, never overwritten). */
|
|
13454
|
+
createInstance = async ({ id, name, kind }) => {
|
|
13455
|
+
const minted = id ?? this.hooks.mintId?.({
|
|
13456
|
+
name,
|
|
13457
|
+
...kind !== void 0 ? { kind } : {}
|
|
13458
|
+
});
|
|
13459
|
+
if (minted === void 0) throw new Error("createInstance needs an id: this provider does not mint one");
|
|
13460
|
+
await this.handle.put({
|
|
13461
|
+
id: minted,
|
|
13462
|
+
data: this.hooks.initialBody(name, kind)
|
|
13463
|
+
}, { ifRev: 0 });
|
|
13464
|
+
await this.hooks.onChanged?.(minted);
|
|
13465
|
+
return { id: minted };
|
|
13466
|
+
};
|
|
13467
|
+
updateInstance = async ({ id, name, values }) => {
|
|
13468
|
+
const patch = {};
|
|
13469
|
+
for (const [key, value] of Object.entries(values)) {
|
|
13470
|
+
if (value === void 0) continue;
|
|
13471
|
+
if (!this.secretKeys.has(key)) patch[key] = value;
|
|
13472
|
+
else if (isSecretClear(value)) patch[key] = null;
|
|
13473
|
+
else if (typeof value === "string" && value !== "" && value !== "__camstack_redacted__" && !isSecretFieldView(value)) patch[key] = value;
|
|
13474
|
+
}
|
|
13475
|
+
if (name !== void 0) patch[this.nameField] = name;
|
|
13476
|
+
await this.handle.patch(id, patch);
|
|
13477
|
+
await this.hooks.onChanged?.(id);
|
|
13478
|
+
return { ok: true };
|
|
13479
|
+
};
|
|
13480
|
+
/** The switch lives in the record body; the addon is told so it applies it (D996). */
|
|
13481
|
+
setInstanceEnabled = async ({ id, enabled }) => {
|
|
13482
|
+
await this.handle.patch(id, { enabled });
|
|
13483
|
+
await this.hooks.onChanged?.(id);
|
|
13484
|
+
return { ok: true };
|
|
13485
|
+
};
|
|
13486
|
+
/** The row and its keychain refs go together (the records door). */
|
|
13487
|
+
deleteInstance = async ({ id }) => {
|
|
13488
|
+
await this.handle.delete(id);
|
|
13489
|
+
await this.hooks.onChanged?.(id);
|
|
13490
|
+
return { ok: true };
|
|
13491
|
+
};
|
|
13492
|
+
testInstance = async (input) => {
|
|
13493
|
+
if (this.hooks.test === void 0) return {
|
|
13494
|
+
success: false,
|
|
13495
|
+
status: "unsupported",
|
|
13496
|
+
error: "This integration declares no connection test"
|
|
13497
|
+
};
|
|
13498
|
+
return this.hooks.test(input);
|
|
13499
|
+
};
|
|
13500
|
+
/** The flat provider object to hand to `registerProvider`. */
|
|
13501
|
+
toProvider() {
|
|
13502
|
+
return {
|
|
13503
|
+
describe: this.describe,
|
|
13504
|
+
instanceFormSchema: this.instanceFormSchema,
|
|
13505
|
+
listInstances: this.listInstances,
|
|
13506
|
+
getInstance: this.getInstance,
|
|
13507
|
+
createInstance: this.createInstance,
|
|
13508
|
+
updateInstance: this.updateInstance,
|
|
13509
|
+
setInstanceEnabled: this.setInstanceEnabled,
|
|
13510
|
+
deleteInstance: this.deleteInstance,
|
|
13511
|
+
testInstance: this.testInstance
|
|
13512
|
+
};
|
|
13513
|
+
}
|
|
13514
|
+
};
|
|
13112
13515
|
var EncodeProfileSchema = object({
|
|
13113
13516
|
video: object({
|
|
13114
13517
|
codec: _enum([
|
|
@@ -14084,6 +14487,16 @@ var MigrationPlanSchema = object({ edits: array(discriminatedUnion("op", [
|
|
|
14084
14487
|
precondition: PreconditionSchema
|
|
14085
14488
|
})
|
|
14086
14489
|
])) });
|
|
14490
|
+
/** `JSON.stringify` with object keys sorted, so equal values spell the same. */
|
|
14491
|
+
function canonicalJson(value) {
|
|
14492
|
+
if (Array.isArray(value)) return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
|
|
14493
|
+
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(",")}}`;
|
|
14494
|
+
return JSON.stringify(value) ?? "null";
|
|
14495
|
+
}
|
|
14496
|
+
/** The precondition string for a value a plan read (`null` when it read nothing). */
|
|
14497
|
+
function preconditionOf(value) {
|
|
14498
|
+
return canonicalJson(value ?? null);
|
|
14499
|
+
}
|
|
14087
14500
|
/**
|
|
14088
14501
|
* What a `procedure` body reports. `already-done` means the rerun observed the
|
|
14089
14502
|
* work finished (a crash between the work and the ledger row, or a manual fix)
|
|
@@ -19262,24 +19675,52 @@ var APPLIANCE_CAP_VOCABULARY = {
|
|
|
19262
19675
|
args: { on: false }
|
|
19263
19676
|
},
|
|
19264
19677
|
{
|
|
19265
|
-
id: "petFountain.
|
|
19266
|
-
method: "
|
|
19678
|
+
id: "petFountain.resetDailyConsumption",
|
|
19679
|
+
method: "resetDailyConsumption",
|
|
19680
|
+
label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
|
|
19681
|
+
phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
|
|
19682
|
+
icon: "rotate-ccw"
|
|
19683
|
+
}
|
|
19684
|
+
],
|
|
19685
|
+
state: [{
|
|
19686
|
+
kind: "boolean",
|
|
19687
|
+
field: "empty",
|
|
19688
|
+
label: t("Tank empty", "Serbatoio vuoto"),
|
|
19689
|
+
whenTrue: t("When the tank of {device} is empty", "Quando il serbatoio di {device} è vuoto"),
|
|
19690
|
+
whenFalse: t("When the tank of {device} is refilled", "Quando il serbatoio di {device} viene riempito"),
|
|
19691
|
+
icon: "droplet"
|
|
19692
|
+
}, {
|
|
19693
|
+
kind: "numeric",
|
|
19694
|
+
field: "waterLevel",
|
|
19695
|
+
quantity: t("the water level", "il livello dell’acqua"),
|
|
19696
|
+
label: t("Water level", "Livello dell’acqua"),
|
|
19697
|
+
unit: "%",
|
|
19698
|
+
icon: "droplets"
|
|
19699
|
+
}]
|
|
19700
|
+
},
|
|
19701
|
+
"pump-cycle": {
|
|
19702
|
+
scope: "device",
|
|
19703
|
+
noun: t("Pump cycle", "Ciclo della pompa"),
|
|
19704
|
+
actions: [
|
|
19705
|
+
{
|
|
19706
|
+
id: "pumpCycle.on",
|
|
19707
|
+
method: "setEnabled",
|
|
19267
19708
|
label: t("Pump cycle on", "Ciclo pompa attivo"),
|
|
19268
19709
|
phrase: t("Run the pump of {device} on its on/off cycle", "Fai lavorare la pompa di {device} a ciclo acceso/spento"),
|
|
19269
19710
|
icon: "timer",
|
|
19270
19711
|
args: { on: true }
|
|
19271
19712
|
},
|
|
19272
19713
|
{
|
|
19273
|
-
id: "
|
|
19274
|
-
method: "
|
|
19714
|
+
id: "pumpCycle.off",
|
|
19715
|
+
method: "setEnabled",
|
|
19275
19716
|
label: t("Pump cycle off", "Ciclo pompa spento"),
|
|
19276
19717
|
phrase: t("Run the pump of {device} continuously", "Fai lavorare la pompa di {device} in continuo"),
|
|
19277
19718
|
icon: "timer-off",
|
|
19278
19719
|
args: { on: false }
|
|
19279
19720
|
},
|
|
19280
19721
|
{
|
|
19281
|
-
id: "
|
|
19282
|
-
method: "
|
|
19722
|
+
id: "pumpCycle.onMinutes",
|
|
19723
|
+
method: "setOnMinutes",
|
|
19283
19724
|
label: t("Set pump run time", "Imposta il tempo di lavoro della pompa"),
|
|
19284
19725
|
phrase: t("Set the pump of {device} to run {minutes} minutes per cycle", "Imposta la pompa di {device} a lavorare {minutes} minuti per ciclo"),
|
|
19285
19726
|
icon: "timer",
|
|
@@ -19295,8 +19736,8 @@ var APPLIANCE_CAP_VOCABULARY = {
|
|
|
19295
19736
|
}]
|
|
19296
19737
|
},
|
|
19297
19738
|
{
|
|
19298
|
-
id: "
|
|
19299
|
-
method: "
|
|
19739
|
+
id: "pumpCycle.offMinutes",
|
|
19740
|
+
method: "setOffMinutes",
|
|
19300
19741
|
label: t("Set pump rest time", "Imposta il tempo di riposo della pompa"),
|
|
19301
19742
|
phrase: t("Set the pump of {device} to rest {minutes} minutes per cycle", "Imposta la pompa di {device} a riposare {minutes} minuti per ciclo"),
|
|
19302
19743
|
icon: "timer-off",
|
|
@@ -19310,35 +19751,63 @@ var APPLIANCE_CAP_VOCABULARY = {
|
|
|
19310
19751
|
unit: "min"
|
|
19311
19752
|
}
|
|
19312
19753
|
}]
|
|
19313
|
-
},
|
|
19314
|
-
{
|
|
19315
|
-
id: "petFountain.resetDailyConsumption",
|
|
19316
|
-
method: "resetDailyConsumption",
|
|
19317
|
-
label: t("Clear daily consumption", "Azzera il consumo giornaliero"),
|
|
19318
|
-
phrase: t("Clear the daily water consumption of {device}", "Azzera il consumo giornaliero di acqua di {device}"),
|
|
19319
|
-
icon: "rotate-ccw"
|
|
19320
19754
|
}
|
|
19321
19755
|
],
|
|
19322
19756
|
state: [{
|
|
19323
19757
|
kind: "boolean",
|
|
19324
|
-
field: "
|
|
19325
|
-
label: t("
|
|
19326
|
-
whenTrue: t("When
|
|
19327
|
-
whenFalse: t("When
|
|
19328
|
-
icon: "
|
|
19329
|
-
}, {
|
|
19330
|
-
kind: "numeric",
|
|
19331
|
-
field: "waterLevel",
|
|
19332
|
-
quantity: t("the water level", "il livello dell’acqua"),
|
|
19333
|
-
label: t("Water level", "Livello dell’acqua"),
|
|
19334
|
-
unit: "%",
|
|
19335
|
-
icon: "droplets"
|
|
19758
|
+
field: "enabled",
|
|
19759
|
+
label: t("Pump cycle", "Ciclo pompa"),
|
|
19760
|
+
whenTrue: t("When {device} starts running its pump on a cycle", "Quando {device} inizia a far lavorare la pompa a ciclo"),
|
|
19761
|
+
whenFalse: t("When {device} runs its pump continuously", "Quando {device} fa lavorare la pompa in continuo"),
|
|
19762
|
+
icon: "timer"
|
|
19336
19763
|
}]
|
|
19337
19764
|
},
|
|
19338
19765
|
"media-player": {
|
|
19339
19766
|
scope: "device",
|
|
19340
19767
|
noun: t("Media player", "Lettore multimediale"),
|
|
19341
19768
|
actions: [
|
|
19769
|
+
{
|
|
19770
|
+
id: "media.turnOn",
|
|
19771
|
+
method: "turnOn",
|
|
19772
|
+
label: t("Turn on", "Accendi"),
|
|
19773
|
+
phrase: t("Turn on {device}", "Accendi {device}"),
|
|
19774
|
+
icon: "power"
|
|
19775
|
+
},
|
|
19776
|
+
{
|
|
19777
|
+
id: "media.turnOff",
|
|
19778
|
+
method: "turnOff",
|
|
19779
|
+
label: t("Turn off", "Spegni"),
|
|
19780
|
+
phrase: t("Turn off {device}", "Spegni {device}"),
|
|
19781
|
+
icon: "power"
|
|
19782
|
+
},
|
|
19783
|
+
{
|
|
19784
|
+
id: "media.volumeUp",
|
|
19785
|
+
method: "volumeUp",
|
|
19786
|
+
label: t("Volume up", "Alza il volume"),
|
|
19787
|
+
phrase: t("Raise the volume of {device}", "Alza il volume di {device}"),
|
|
19788
|
+
icon: "volume-2"
|
|
19789
|
+
},
|
|
19790
|
+
{
|
|
19791
|
+
id: "media.volumeDown",
|
|
19792
|
+
method: "volumeDown",
|
|
19793
|
+
label: t("Volume down", "Abbassa il volume"),
|
|
19794
|
+
phrase: t("Lower the volume of {device}", "Abbassa il volume di {device}"),
|
|
19795
|
+
icon: "volume-2"
|
|
19796
|
+
},
|
|
19797
|
+
{
|
|
19798
|
+
id: "media.channelUp",
|
|
19799
|
+
method: "channelUp",
|
|
19800
|
+
label: t("Channel up", "Canale successivo"),
|
|
19801
|
+
phrase: t("Go to the next channel on {device}", "Passa al canale successivo su {device}"),
|
|
19802
|
+
icon: "skip-forward"
|
|
19803
|
+
},
|
|
19804
|
+
{
|
|
19805
|
+
id: "media.channelDown",
|
|
19806
|
+
method: "channelDown",
|
|
19807
|
+
label: t("Channel down", "Canale precedente"),
|
|
19808
|
+
phrase: t("Go to the previous channel on {device}", "Torna al canale precedente su {device}"),
|
|
19809
|
+
icon: "skip-back"
|
|
19810
|
+
},
|
|
19342
19811
|
{
|
|
19343
19812
|
id: "media.play",
|
|
19344
19813
|
method: "play",
|
|
@@ -21225,6 +21694,7 @@ var SYSTEM_CAP_VOCABULARY = {
|
|
|
21225
21694
|
"audio-analyzer": system(PIPELINE),
|
|
21226
21695
|
"audio-codec": system(INTERNAL),
|
|
21227
21696
|
"auth-provider": system(IDENTITY),
|
|
21697
|
+
"board-key-source": system(INTERNAL),
|
|
21228
21698
|
backup: system("system backups — run and restored from the Backups page"),
|
|
21229
21699
|
broker: system("message-broker connections — configured, not actuated"),
|
|
21230
21700
|
"calendar-source": system(INTERNAL),
|
|
@@ -45687,6 +46157,49 @@ object({
|
|
|
45687
46157
|
lastChangedAt: number()
|
|
45688
46158
|
});
|
|
45689
46159
|
DeviceType.Sensor;
|
|
46160
|
+
/**
|
|
46161
|
+
* Why a board key could not be read. Typed so a caller reports the cause and
|
|
46162
|
+
* never treats "could not read" as "has no key" (D49).
|
|
46163
|
+
*/
|
|
46164
|
+
var BoardKeyFailureReasonSchema = _enum([
|
|
46165
|
+
"unknown-source",
|
|
46166
|
+
"not-admin",
|
|
46167
|
+
"no-builder",
|
|
46168
|
+
"no-ingress-url",
|
|
46169
|
+
"unreachable",
|
|
46170
|
+
"unknown-board",
|
|
46171
|
+
"no-encryption-key",
|
|
46172
|
+
"unreadable-config"
|
|
46173
|
+
]);
|
|
46174
|
+
var BoardKeySourceSchema = object({
|
|
46175
|
+
sourceId: string(),
|
|
46176
|
+
name: string()
|
|
46177
|
+
});
|
|
46178
|
+
var BoardKeyBoardSchema = object({
|
|
46179
|
+
/** The builder's file name, e.g. `kitchen.yaml`: the handle `getBoardKey` takes. */
|
|
46180
|
+
configuration: string(),
|
|
46181
|
+
/** The node name the builder gives the board. */
|
|
46182
|
+
name: string(),
|
|
46183
|
+
/** The address the builder knows the board by, when it has one. */
|
|
46184
|
+
address: string().nullable()
|
|
46185
|
+
});
|
|
46186
|
+
var BoardKeyFailureSchema = object({
|
|
46187
|
+
ok: literal(false),
|
|
46188
|
+
reason: BoardKeyFailureReasonSchema,
|
|
46189
|
+
detail: string().optional()
|
|
46190
|
+
});
|
|
46191
|
+
var ListBoardsResultSchema = discriminatedUnion("ok", [object({
|
|
46192
|
+
ok: literal(true),
|
|
46193
|
+
boards: array(BoardKeyBoardSchema).readonly()
|
|
46194
|
+
}), BoardKeyFailureSchema]);
|
|
46195
|
+
var GetBoardKeyResultSchema = discriminatedUnion("ok", [object({
|
|
46196
|
+
ok: literal(true),
|
|
46197
|
+
key: string()
|
|
46198
|
+
}), BoardKeyFailureSchema]);
|
|
46199
|
+
method(_void(), array(BoardKeySourceSchema).readonly(), { auth: "admin" }), method(object({ sourceId: string().min(1) }), ListBoardsResultSchema, { auth: "admin" }), method(object({
|
|
46200
|
+
sourceId: string().min(1),
|
|
46201
|
+
configuration: string().min(1)
|
|
46202
|
+
}), GetBoardKeyResultSchema, { auth: "admin" });
|
|
45690
46203
|
object({
|
|
45691
46204
|
/** Current level as 0..100 inclusive. Firmware-reported. */
|
|
45692
46205
|
level: number().min(0).max(100).meta(readingMeta("percentage")),
|
|
@@ -47823,6 +48336,188 @@ DeviceType.Control, method(object({
|
|
|
47823
48336
|
kind: "mutation",
|
|
47824
48337
|
auth: "protected"
|
|
47825
48338
|
});
|
|
48339
|
+
/**
|
|
48340
|
+
* Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
|
|
48341
|
+
* Apple TV, Spotify, Roku, generic OTT receivers, …) on
|
|
48342
|
+
* `DeviceType.MediaPlayer`. Disjoint from the camera streaming
|
|
48343
|
+
* surface — those use `DeviceType.Camera` + the broker pipeline.
|
|
48344
|
+
*
|
|
48345
|
+
* The cap declares a wide method surface; not every backend supports
|
|
48346
|
+
* every method. Drivers signal sub-capability via the
|
|
48347
|
+
* `DeviceFeature.MediaPlayerXxx` flags (paired one-to-one with the
|
|
48348
|
+
* corresponding method) so the UI gates buttons without a slice
|
|
48349
|
+
* fetch — same pattern as `PanTiltZoom` for cameras.
|
|
48350
|
+
*/
|
|
48351
|
+
var MediaPlayerStateSchema = _enum([
|
|
48352
|
+
"off",
|
|
48353
|
+
"on",
|
|
48354
|
+
"idle",
|
|
48355
|
+
"playing",
|
|
48356
|
+
"paused",
|
|
48357
|
+
"buffering",
|
|
48358
|
+
"standby"
|
|
48359
|
+
]);
|
|
48360
|
+
var MediaPlayerRepeatSchema = _enum([
|
|
48361
|
+
"off",
|
|
48362
|
+
"all",
|
|
48363
|
+
"one"
|
|
48364
|
+
]);
|
|
48365
|
+
var MediaInfoSchema = object({
|
|
48366
|
+
/** Free-form media kind (`music`, `tvshow`, `movie`, `app`, `channel`,
|
|
48367
|
+
* `podcast`, …). */
|
|
48368
|
+
type: string(),
|
|
48369
|
+
/** Human-readable title. */
|
|
48370
|
+
title: string(),
|
|
48371
|
+
/** Optional artist / channel / station label. */
|
|
48372
|
+
artist: string().optional(),
|
|
48373
|
+
/** Optional album / season / show name. */
|
|
48374
|
+
album: string().optional(),
|
|
48375
|
+
/** Optional cover-art / thumbnail URL. */
|
|
48376
|
+
imageUrl: string().optional()
|
|
48377
|
+
});
|
|
48378
|
+
/**
|
|
48379
|
+
* Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
|
|
48380
|
+
* the device said so. `assumed`: the driver has no feedback (infrared) and
|
|
48381
|
+
* tracks what it last SENT; it moves only after a send was accepted, and a
|
|
48382
|
+
* human at the real remote makes it wrong. A UI shows an assumed value as a
|
|
48383
|
+
* guess, never as a reading.
|
|
48384
|
+
*/
|
|
48385
|
+
var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
|
|
48386
|
+
/** The fields of a media-player status a driver can only guess. */
|
|
48387
|
+
var MediaPlayerAssumedFieldSchema = _enum([
|
|
48388
|
+
"state",
|
|
48389
|
+
"volumeLevel",
|
|
48390
|
+
"isMuted",
|
|
48391
|
+
"source"
|
|
48392
|
+
]);
|
|
48393
|
+
var MediaPlayerStatusSchema = object({
|
|
48394
|
+
/** Playback lifecycle state. */
|
|
48395
|
+
state: MediaPlayerStateSchema,
|
|
48396
|
+
/**
|
|
48397
|
+
* Whether the state fields are readings or the driver's own bookkeeping.
|
|
48398
|
+
* A slice written before this field existed parses as `reported`.
|
|
48399
|
+
*/
|
|
48400
|
+
stateSource: MediaPlayerStateSourceSchema.default("reported"),
|
|
48401
|
+
/**
|
|
48402
|
+
* Which fields are the driver's own guess. `stateSource` is `assumed` when
|
|
48403
|
+
* this is not empty; a field not listed here is a reading even then (power
|
|
48404
|
+
* from a running-state source, mute guessed from the last send).
|
|
48405
|
+
*/
|
|
48406
|
+
assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
|
|
48407
|
+
/** Volume as 0..100 inclusive. Null when the device has no volume
|
|
48408
|
+
* surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
|
|
48409
|
+
volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
|
|
48410
|
+
/** Mute toggle distinct from volume=0. Null when no mute surface.
|
|
48411
|
+
* Pair with `DeviceFeature.MediaPlayerMute`. */
|
|
48412
|
+
isMuted: boolean().nullable(),
|
|
48413
|
+
/** Active source / input. Empty when no source surface. */
|
|
48414
|
+
source: string(),
|
|
48415
|
+
/** Selectable sources. Empty when no source surface.
|
|
48416
|
+
* Pair with `DeviceFeature.MediaPlayerSelectSource`. */
|
|
48417
|
+
availableSources: array(string()),
|
|
48418
|
+
/** Currently-playing media info. Null when nothing is playing. */
|
|
48419
|
+
currentMedia: MediaInfoSchema.nullable(),
|
|
48420
|
+
/** Current playback position in ms. Null when not seekable or
|
|
48421
|
+
* nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
|
|
48422
|
+
position: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
|
|
48423
|
+
/** Total duration of the current media in ms. Null when unknown
|
|
48424
|
+
* (live stream) or nothing is playing. */
|
|
48425
|
+
duration: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
|
|
48426
|
+
/** Shuffle toggle. Null when no shuffle surface.
|
|
48427
|
+
* Pair with `DeviceFeature.MediaPlayerShuffle`. */
|
|
48428
|
+
shuffle: boolean().nullable(),
|
|
48429
|
+
/** Repeat mode. Null when no repeat surface.
|
|
48430
|
+
* Pair with `DeviceFeature.MediaPlayerRepeat`. */
|
|
48431
|
+
repeat: MediaPlayerRepeatSchema.nullable(),
|
|
48432
|
+
/** Ms epoch when the slice was last updated. */
|
|
48433
|
+
lastChangedAt: number()
|
|
48434
|
+
});
|
|
48435
|
+
var stepInput = object({
|
|
48436
|
+
deviceId: number().int().nonnegative(),
|
|
48437
|
+
/** How many presses: the caller's count (Alexa `volumeSteps`, `channelCount`). */
|
|
48438
|
+
steps: number().int().min(1).max(50).default(1)
|
|
48439
|
+
});
|
|
48440
|
+
var deviceOnly = object({ deviceId: number().int().nonnegative() });
|
|
48441
|
+
DeviceType.MediaPlayer, method(deviceOnly, _void(), {
|
|
48442
|
+
kind: "mutation",
|
|
48443
|
+
auth: "protected"
|
|
48444
|
+
}), method(deviceOnly, _void(), {
|
|
48445
|
+
kind: "mutation",
|
|
48446
|
+
auth: "protected"
|
|
48447
|
+
}), method(stepInput, _void(), {
|
|
48448
|
+
kind: "mutation",
|
|
48449
|
+
auth: "protected"
|
|
48450
|
+
}), method(stepInput, _void(), {
|
|
48451
|
+
kind: "mutation",
|
|
48452
|
+
auth: "protected"
|
|
48453
|
+
}), method(stepInput, _void(), {
|
|
48454
|
+
kind: "mutation",
|
|
48455
|
+
auth: "protected"
|
|
48456
|
+
}), method(stepInput, _void(), {
|
|
48457
|
+
kind: "mutation",
|
|
48458
|
+
auth: "protected"
|
|
48459
|
+
}), method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
48460
|
+
kind: "mutation",
|
|
48461
|
+
auth: "protected"
|
|
48462
|
+
}), method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
48463
|
+
kind: "mutation",
|
|
48464
|
+
auth: "protected"
|
|
48465
|
+
}), method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
48466
|
+
kind: "mutation",
|
|
48467
|
+
auth: "protected"
|
|
48468
|
+
}), method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
48469
|
+
kind: "mutation",
|
|
48470
|
+
auth: "protected"
|
|
48471
|
+
}), method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
48472
|
+
kind: "mutation",
|
|
48473
|
+
auth: "protected"
|
|
48474
|
+
}), method(object({
|
|
48475
|
+
deviceId: number().int().nonnegative(),
|
|
48476
|
+
position: number().int().nonnegative()
|
|
48477
|
+
}), _void(), {
|
|
48478
|
+
kind: "mutation",
|
|
48479
|
+
auth: "protected"
|
|
48480
|
+
}), method(object({
|
|
48481
|
+
deviceId: number().int().nonnegative(),
|
|
48482
|
+
volumeLevel: number().min(0).max(100)
|
|
48483
|
+
}), _void(), {
|
|
48484
|
+
kind: "mutation",
|
|
48485
|
+
auth: "protected"
|
|
48486
|
+
}), method(object({
|
|
48487
|
+
deviceId: number().int().nonnegative(),
|
|
48488
|
+
muted: boolean()
|
|
48489
|
+
}), _void(), {
|
|
48490
|
+
kind: "mutation",
|
|
48491
|
+
auth: "protected"
|
|
48492
|
+
}), method(object({
|
|
48493
|
+
deviceId: number().int().nonnegative(),
|
|
48494
|
+
shuffle: boolean()
|
|
48495
|
+
}), _void(), {
|
|
48496
|
+
kind: "mutation",
|
|
48497
|
+
auth: "protected"
|
|
48498
|
+
}), method(object({
|
|
48499
|
+
deviceId: number().int().nonnegative(),
|
|
48500
|
+
repeat: MediaPlayerRepeatSchema
|
|
48501
|
+
}), _void(), {
|
|
48502
|
+
kind: "mutation",
|
|
48503
|
+
auth: "protected"
|
|
48504
|
+
}), method(object({
|
|
48505
|
+
deviceId: number().int().nonnegative(),
|
|
48506
|
+
source: string().min(1)
|
|
48507
|
+
}), _void(), {
|
|
48508
|
+
kind: "mutation",
|
|
48509
|
+
auth: "protected"
|
|
48510
|
+
}), method(object({
|
|
48511
|
+
deviceId: number().int().nonnegative(),
|
|
48512
|
+
/** Media identifier / URL. */
|
|
48513
|
+
mediaId: string().min(1),
|
|
48514
|
+
/** Media kind (`music`, `tvshow`, `movie`, `app`, …) — provider
|
|
48515
|
+
* passes it through to the upstream service. */
|
|
48516
|
+
mediaType: string().min(1)
|
|
48517
|
+
}), _void(), {
|
|
48518
|
+
kind: "mutation",
|
|
48519
|
+
auth: "protected"
|
|
48520
|
+
});
|
|
47826
48521
|
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;
|
|
47827
48522
|
var NUMBER = {
|
|
47828
48523
|
kind: "fixed",
|
|
@@ -48270,49 +48965,6 @@ function actuator(input) {
|
|
|
48270
48965
|
};
|
|
48271
48966
|
}
|
|
48272
48967
|
new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
|
|
48273
|
-
var PowerSourceRefusalReasonSchema = _enum([
|
|
48274
|
-
"no-power-source",
|
|
48275
|
-
"switch-missing",
|
|
48276
|
-
"switch-offline",
|
|
48277
|
-
"switch-unknown",
|
|
48278
|
-
"restart-in-progress",
|
|
48279
|
-
"switch-call-failed"
|
|
48280
|
-
]);
|
|
48281
|
-
var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
|
|
48282
|
-
accepted: literal(false),
|
|
48283
|
-
reason: PowerSourceRefusalReasonSchema,
|
|
48284
|
-
/** One line for the operator, naming the switch. */
|
|
48285
|
-
message: string()
|
|
48286
|
-
})]);
|
|
48287
|
-
var PowerSourceInfoSchema = object({
|
|
48288
|
-
/** False when the device has no power source linked; the rest is then empty. */
|
|
48289
|
-
linked: boolean(),
|
|
48290
|
-
switchDeviceId: number().nullable(),
|
|
48291
|
-
switchName: string().nullable(),
|
|
48292
|
-
/** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
|
|
48293
|
-
switchOn: boolean().nullable(),
|
|
48294
|
-
switchOnline: boolean().nullable(),
|
|
48295
|
-
restartInProgress: boolean(),
|
|
48296
|
-
offSeconds: number().int()
|
|
48297
|
-
});
|
|
48298
|
-
object({
|
|
48299
|
-
/** Null while the switch has not reported: unknown is never `false` (D393). */
|
|
48300
|
-
switchOn: boolean().nullable(),
|
|
48301
|
-
offSeconds: number().int(),
|
|
48302
|
-
lastFetchedAt: number()
|
|
48303
|
-
});
|
|
48304
|
-
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;
|
|
48305
|
-
var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
|
|
48306
|
-
systemMethod(deviceInput$2, PowerSourceInfoSchema, { auth: "admin" }), systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
48307
|
-
kind: "mutation",
|
|
48308
|
-
auth: "admin"
|
|
48309
|
-
}), systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
48310
|
-
kind: "mutation",
|
|
48311
|
-
auth: "admin"
|
|
48312
|
-
}), systemMethod(deviceInput$2, PowerSourceActionResultSchema, {
|
|
48313
|
-
kind: "mutation",
|
|
48314
|
-
auth: "admin"
|
|
48315
|
-
});
|
|
48316
48968
|
DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
|
|
48317
48969
|
DeviceType.Camera;
|
|
48318
48970
|
`${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
|
|
@@ -48340,6 +48992,165 @@ var ComposableCapabilitiesSchema = object({ caps: array(object({
|
|
|
48340
48992
|
}).optional()
|
|
48341
48993
|
})) });
|
|
48342
48994
|
/**
|
|
48995
|
+
* A saved Customize assignment (D936). The schema is the type: callers use
|
|
48996
|
+
* `z.infer`, they do not restate the shape.
|
|
48997
|
+
*/
|
|
48998
|
+
var OverrideSourceSchema = object({
|
|
48999
|
+
addonId: string().min(1),
|
|
49000
|
+
stableId: string().min(1),
|
|
49001
|
+
cap: string().min(1),
|
|
49002
|
+
fieldPath: string().min(1)
|
|
49003
|
+
});
|
|
49004
|
+
var IdentityTransformDraftSchema = object({ kind: literal("identity") });
|
|
49005
|
+
var InvertTransformDraftSchema = object({ kind: literal("invert") });
|
|
49006
|
+
var ThresholdTransformDraftSchema = object({
|
|
49007
|
+
kind: literal("threshold"),
|
|
49008
|
+
comparison: _enum(["gte", "lte"]),
|
|
49009
|
+
threshold: number(),
|
|
49010
|
+
startDwellMs: number().nonnegative(),
|
|
49011
|
+
stopDwellMs: number().nonnegative()
|
|
49012
|
+
});
|
|
49013
|
+
var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
|
|
49014
|
+
IdentityTransformDraftSchema,
|
|
49015
|
+
InvertTransformDraftSchema,
|
|
49016
|
+
ThresholdTransformDraftSchema,
|
|
49017
|
+
object({
|
|
49018
|
+
kind: literal("linear"),
|
|
49019
|
+
scale: number(),
|
|
49020
|
+
offset: number(),
|
|
49021
|
+
clampMin: number().optional(),
|
|
49022
|
+
clampMax: number().optional()
|
|
49023
|
+
}),
|
|
49024
|
+
object({
|
|
49025
|
+
kind: literal("enum-map"),
|
|
49026
|
+
mapping: record(string(), string().nullable())
|
|
49027
|
+
}),
|
|
49028
|
+
object({ kind: literal("number") })
|
|
49029
|
+
]);
|
|
49030
|
+
var ScalarOverrideDraftSchema = object({
|
|
49031
|
+
kind: literal("scalar"),
|
|
49032
|
+
cap: string().min(1),
|
|
49033
|
+
targetId: string().min(1),
|
|
49034
|
+
source: OverrideSourceSchema,
|
|
49035
|
+
transform: CustomizeTransformDraftSchema
|
|
49036
|
+
});
|
|
49037
|
+
var KeyedArrayOverrideDraftSchema = object({
|
|
49038
|
+
kind: literal("keyedArray"),
|
|
49039
|
+
cap: string().min(1),
|
|
49040
|
+
targetId: string().min(1),
|
|
49041
|
+
key: string().min(1),
|
|
49042
|
+
label: string().min(1),
|
|
49043
|
+
fieldPath: string().min(1),
|
|
49044
|
+
source: OverrideSourceSchema,
|
|
49045
|
+
transform: CustomizeTransformDraftSchema
|
|
49046
|
+
});
|
|
49047
|
+
var PulseOverrideDraftSchema = object({
|
|
49048
|
+
kind: literal("pulse"),
|
|
49049
|
+
cap: string().min(1),
|
|
49050
|
+
targetId: string().min(1),
|
|
49051
|
+
enabled: boolean(),
|
|
49052
|
+
debounceMs: number().nonnegative(),
|
|
49053
|
+
edges: array(_enum([
|
|
49054
|
+
"rising",
|
|
49055
|
+
"button",
|
|
49056
|
+
"native-event"
|
|
49057
|
+
])).min(1).optional(),
|
|
49058
|
+
sources: array(OverrideSourceSchema).min(1)
|
|
49059
|
+
});
|
|
49060
|
+
var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
|
|
49061
|
+
"in-addition",
|
|
49062
|
+
"confirm",
|
|
49063
|
+
"replace"
|
|
49064
|
+
]).optional() });
|
|
49065
|
+
discriminatedUnion("kind", [
|
|
49066
|
+
ScalarOverrideDraftSchema,
|
|
49067
|
+
KeyedArrayOverrideDraftSchema,
|
|
49068
|
+
PulseOverrideDraftSchema,
|
|
49069
|
+
object({
|
|
49070
|
+
kind: literal("span"),
|
|
49071
|
+
cap: string().min(1),
|
|
49072
|
+
targetId: string().min(1),
|
|
49073
|
+
timeoutMs: number().nonnegative(),
|
|
49074
|
+
cooldownMs: number().nonnegative().optional(),
|
|
49075
|
+
sources: array(SpanSourceDraftSchema)
|
|
49076
|
+
})
|
|
49077
|
+
]);
|
|
49078
|
+
object({
|
|
49079
|
+
targetId: string().min(1),
|
|
49080
|
+
configured: boolean(),
|
|
49081
|
+
source: _enum(["native", "assigned"]),
|
|
49082
|
+
state: _enum([
|
|
49083
|
+
"active",
|
|
49084
|
+
"stale",
|
|
49085
|
+
"released",
|
|
49086
|
+
"unavailable"
|
|
49087
|
+
]),
|
|
49088
|
+
reason: string().nullable()
|
|
49089
|
+
});
|
|
49090
|
+
/**
|
|
49091
|
+
* What a running-threshold proposal says (planning#405). The schema is the
|
|
49092
|
+
* type: the algorithm, the capability method and the UI all read `z.infer`.
|
|
49093
|
+
*
|
|
49094
|
+
* A proposal is read-only advice: it carries the numbers it was derived from
|
|
49095
|
+
* (`evidence`) so the operator can judge it, and an `insufficient` answer names
|
|
49096
|
+
* WHY with a typed reason and the catalog defaults, never an invented number.
|
|
49097
|
+
*/
|
|
49098
|
+
var RunningProposalTierSchema = _enum(["raw", "5m"]);
|
|
49099
|
+
var RunningProposalConfidenceSchema = _enum([
|
|
49100
|
+
"high",
|
|
49101
|
+
"medium",
|
|
49102
|
+
"low"
|
|
49103
|
+
]);
|
|
49104
|
+
var RunningProposalInsufficientReasonSchema = _enum([
|
|
49105
|
+
"no-history",
|
|
49106
|
+
"span-too-short",
|
|
49107
|
+
"truncated",
|
|
49108
|
+
"no-idle-floor",
|
|
49109
|
+
"no-runs",
|
|
49110
|
+
"no-separation"
|
|
49111
|
+
]);
|
|
49112
|
+
var RunningProposalReplaySchema = object({
|
|
49113
|
+
/** Intervals the real dwell expression held true over the window. */
|
|
49114
|
+
intervals: number().int().nonnegative(),
|
|
49115
|
+
/** Intervals outside every run, or a second interval inside one. */
|
|
49116
|
+
extra: number().int().nonnegative(),
|
|
49117
|
+
/** Runs without an interval of their own (none, or merged into a neighbour). */
|
|
49118
|
+
missed: number().int().nonnegative()
|
|
49119
|
+
});
|
|
49120
|
+
var RunningProposalEvidenceSchema = object({
|
|
49121
|
+
tier: RunningProposalTierSchema,
|
|
49122
|
+
spanMs: number().nonnegative(),
|
|
49123
|
+
from: number(),
|
|
49124
|
+
to: number(),
|
|
49125
|
+
points: number().int().nonnegative(),
|
|
49126
|
+
runsFound: number().int().nonnegative(),
|
|
49127
|
+
pausesFound: number().int().nonnegative(),
|
|
49128
|
+
longestPauseMs: number().nonnegative(),
|
|
49129
|
+
idleFloorW: number(),
|
|
49130
|
+
noiseCeilingW: number(),
|
|
49131
|
+
plateauW: number(),
|
|
49132
|
+
replay: RunningProposalReplaySchema
|
|
49133
|
+
});
|
|
49134
|
+
var RunningProposalInsufficientEvidenceSchema = object({
|
|
49135
|
+
spanMs: number().nonnegative(),
|
|
49136
|
+
points: number().int().nonnegative(),
|
|
49137
|
+
runsFound: number().int().nonnegative()
|
|
49138
|
+
});
|
|
49139
|
+
var RunningThresholdProposalSchema = discriminatedUnion("state", [object({
|
|
49140
|
+
state: literal("proposal"),
|
|
49141
|
+
transform: ThresholdTransformDraftSchema,
|
|
49142
|
+
evidence: RunningProposalEvidenceSchema,
|
|
49143
|
+
confidence: RunningProposalConfidenceSchema,
|
|
49144
|
+
/** Why the confidence is not higher; empty when nothing held it back. */
|
|
49145
|
+
notes: array(string())
|
|
49146
|
+
}), object({
|
|
49147
|
+
state: literal("insufficient"),
|
|
49148
|
+
reason: RunningProposalInsufficientReasonSchema,
|
|
49149
|
+
evidence: RunningProposalInsufficientEvidenceSchema,
|
|
49150
|
+
/** The catalog defaults, offered as what stands when nothing better is known. */
|
|
49151
|
+
fallback: ThresholdTransformDraftSchema
|
|
49152
|
+
})]);
|
|
49153
|
+
/**
|
|
48343
49154
|
* The report of a one-shot "move this owner's plaintext secrets into the
|
|
48344
49155
|
* keychain" run (D878). COUNTS only - never a value, never a ref.
|
|
48345
49156
|
*
|
|
@@ -48629,6 +49440,14 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
|
|
|
48629
49440
|
kind: "mutation",
|
|
48630
49441
|
auth: "admin",
|
|
48631
49442
|
access: "view"
|
|
49443
|
+
}), method(object({
|
|
49444
|
+
deviceId: number().int(),
|
|
49445
|
+
cap: string().min(1),
|
|
49446
|
+
fieldPath: string().min(1),
|
|
49447
|
+
deviceType: _enum(DeviceType).optional()
|
|
49448
|
+
}), RunningThresholdProposalSchema, {
|
|
49449
|
+
auth: "admin",
|
|
49450
|
+
access: "view"
|
|
48632
49451
|
}), method(object({
|
|
48633
49452
|
blockId: string().min(1),
|
|
48634
49453
|
trace: AutomationRunTraceSchema.optional(),
|
|
@@ -49437,7 +50256,27 @@ method(object({
|
|
|
49437
50256
|
* exposes only the COMMON surface: link state, supported device
|
|
49438
50257
|
* kinds (so the UI can filter the device picker), and per-device
|
|
49439
50258
|
* expose/unexpose.
|
|
49440
|
-
|
|
50259
|
+
*
|
|
50260
|
+
* INSTANCES. One export addon can serve several independent targets (a
|
|
50261
|
+
* HomeKit bridge, a Home Assistant broker). `listInstances` names them.
|
|
50262
|
+
* `instanceId` on a READ is optional: absent = the whole addon (linked if any
|
|
50263
|
+
* enabled instance is, `exposedDeviceCount` = distinct devices, `setup` = the
|
|
50264
|
+
* default instance's); present = that instance; an id the provider does not
|
|
50265
|
+
* serve is refused with `ExportInstanceUnknownError` (`export-instance-unknown`),
|
|
50266
|
+
* never answered empty. `instanceId` on a WRITE is REQUIRED: a membership
|
|
50267
|
+
* change always names the target it changes. A single-instance provider
|
|
50268
|
+
* accepts an absent id on a read or its own id, and refuses any other.
|
|
50269
|
+
*/
|
|
50270
|
+
/** Which instance of an export addon a call addresses. Absent on a read = the whole addon. */
|
|
50271
|
+
var InstanceIdSchema$1 = string().min(1);
|
|
50272
|
+
var InstancePinSchema = object({ instanceId: InstanceIdSchema$1.optional() });
|
|
50273
|
+
/** One independent export target of an addon. */
|
|
50274
|
+
var ExportInstanceSchema = object({
|
|
50275
|
+
instanceId: InstanceIdSchema$1,
|
|
50276
|
+
name: string(),
|
|
50277
|
+
enabled: boolean(),
|
|
50278
|
+
kind: string().optional()
|
|
50279
|
+
});
|
|
49441
50280
|
var LinkStateSchema = _enum([
|
|
49442
50281
|
"unlinked",
|
|
49443
50282
|
"linked",
|
|
@@ -49488,10 +50327,14 @@ var ExposedDeviceSchema = object({
|
|
|
49488
50327
|
capabilities: array(string()).optional()
|
|
49489
50328
|
});
|
|
49490
50329
|
var ExposeInputSchema = object({
|
|
50330
|
+
instanceId: InstanceIdSchema$1,
|
|
49491
50331
|
deviceId: string(),
|
|
49492
50332
|
capabilities: array(string()).optional()
|
|
49493
50333
|
});
|
|
49494
|
-
var UnexposeInputSchema = object({
|
|
50334
|
+
var UnexposeInputSchema = object({
|
|
50335
|
+
instanceId: InstanceIdSchema$1,
|
|
50336
|
+
deviceId: string()
|
|
50337
|
+
});
|
|
49495
50338
|
/**
|
|
49496
50339
|
* What an exporter may expose besides devices (D968). An action and a
|
|
49497
50340
|
* notification target are LIST ENTRIES, never devices (D954): they enter an
|
|
@@ -49520,15 +50363,21 @@ var ExposedEntitySchema = object({
|
|
|
49520
50363
|
entity: ExportEntityRefSchema,
|
|
49521
50364
|
exposedAs: string().optional()
|
|
49522
50365
|
});
|
|
49523
|
-
var ExposeEntityInputSchema = object({
|
|
49524
|
-
|
|
49525
|
-
|
|
50366
|
+
var ExposeEntityInputSchema = object({
|
|
50367
|
+
instanceId: InstanceIdSchema$1,
|
|
50368
|
+
entity: ExportEntityRefSchema
|
|
50369
|
+
});
|
|
50370
|
+
var UnexposeEntityInputSchema = object({
|
|
50371
|
+
instanceId: InstanceIdSchema$1,
|
|
50372
|
+
entity: ExportEntityRefSchema
|
|
50373
|
+
});
|
|
50374
|
+
method(InstancePinSchema, DeviceExportStatusSchema), method(_void(), array(ExportInstanceSchema)), method(InstancePinSchema, array(DeviceKindSchema)), method(InstancePinSchema, array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), {
|
|
49526
50375
|
kind: "mutation",
|
|
49527
50376
|
auth: "admin"
|
|
49528
50377
|
}), method(UnexposeInputSchema, _void(), {
|
|
49529
50378
|
kind: "mutation",
|
|
49530
50379
|
auth: "admin"
|
|
49531
|
-
}), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(
|
|
50380
|
+
}), optionalMethod(_void(), array(ExportEntityKindSchema)), optionalMethod(InstancePinSchema, array(ExposedEntitySchema)), optionalMethod(ExposeEntityInputSchema, _void(), {
|
|
49532
50381
|
kind: "mutation",
|
|
49533
50382
|
auth: "admin"
|
|
49534
50383
|
}), optionalMethod(UnexposeEntityInputSchema, _void(), {
|
|
@@ -51408,7 +52257,7 @@ object({
|
|
|
51408
52257
|
/** Ms epoch of the last value received from the appliance; 0 = never. */
|
|
51409
52258
|
lastFetchedAt: number()
|
|
51410
52259
|
});
|
|
51411
|
-
var DeviceIdSchema$
|
|
52260
|
+
var DeviceIdSchema$2 = number().int().nonnegative();
|
|
51412
52261
|
/** A value a program option or setting can take. */
|
|
51413
52262
|
var DishwasherValueSchema = union([
|
|
51414
52263
|
boolean(),
|
|
@@ -51472,77 +52321,77 @@ var DishwasherOptionsSchema = object({
|
|
|
51472
52321
|
options: array(DishwasherOptionDescriptorSchema)
|
|
51473
52322
|
});
|
|
51474
52323
|
DeviceType.Dishwasher, method(object({
|
|
51475
|
-
deviceId: DeviceIdSchema$
|
|
52324
|
+
deviceId: DeviceIdSchema$2,
|
|
51476
52325
|
program: string().min(1).optional(),
|
|
51477
52326
|
options: record(string(), DishwasherValueSchema).optional()
|
|
51478
52327
|
}), _void(), {
|
|
51479
52328
|
kind: "mutation",
|
|
51480
52329
|
auth: "protected"
|
|
51481
|
-
}), method(object({ deviceId: DeviceIdSchema$
|
|
52330
|
+
}), method(object({ deviceId: DeviceIdSchema$2 }), _void(), {
|
|
51482
52331
|
kind: "mutation",
|
|
51483
52332
|
auth: "protected"
|
|
51484
|
-
}), method(object({ deviceId: DeviceIdSchema$
|
|
52333
|
+
}), method(object({ deviceId: DeviceIdSchema$2 }), _void(), {
|
|
51485
52334
|
kind: "mutation",
|
|
51486
52335
|
auth: "protected"
|
|
51487
|
-
}), method(object({ deviceId: DeviceIdSchema$
|
|
52336
|
+
}), method(object({ deviceId: DeviceIdSchema$2 }), _void(), {
|
|
51488
52337
|
kind: "mutation",
|
|
51489
52338
|
auth: "protected"
|
|
51490
52339
|
}), method(object({
|
|
51491
|
-
deviceId: DeviceIdSchema$
|
|
52340
|
+
deviceId: DeviceIdSchema$2,
|
|
51492
52341
|
program: string().min(1)
|
|
51493
52342
|
}), _void(), {
|
|
51494
52343
|
kind: "mutation",
|
|
51495
52344
|
auth: "protected"
|
|
51496
52345
|
}), method(object({
|
|
51497
|
-
deviceId: DeviceIdSchema$
|
|
52346
|
+
deviceId: DeviceIdSchema$2,
|
|
51498
52347
|
on: boolean()
|
|
51499
52348
|
}), _void(), {
|
|
51500
52349
|
kind: "mutation",
|
|
51501
52350
|
auth: "protected"
|
|
51502
52351
|
}), method(object({
|
|
51503
|
-
deviceId: DeviceIdSchema$
|
|
52352
|
+
deviceId: DeviceIdSchema$2,
|
|
51504
52353
|
on: boolean()
|
|
51505
52354
|
}), _void(), {
|
|
51506
52355
|
kind: "mutation",
|
|
51507
52356
|
auth: "protected"
|
|
51508
52357
|
}), method(object({
|
|
51509
|
-
deviceId: DeviceIdSchema$
|
|
52358
|
+
deviceId: DeviceIdSchema$2,
|
|
51510
52359
|
on: boolean()
|
|
51511
52360
|
}), _void(), {
|
|
51512
52361
|
kind: "mutation",
|
|
51513
52362
|
auth: "protected"
|
|
51514
52363
|
}), method(object({
|
|
51515
|
-
deviceId: DeviceIdSchema$
|
|
52364
|
+
deviceId: DeviceIdSchema$2,
|
|
51516
52365
|
on: boolean()
|
|
51517
52366
|
}), _void(), {
|
|
51518
52367
|
kind: "mutation",
|
|
51519
52368
|
auth: "protected"
|
|
51520
52369
|
}), method(object({
|
|
51521
|
-
deviceId: DeviceIdSchema$
|
|
52370
|
+
deviceId: DeviceIdSchema$2,
|
|
51522
52371
|
on: boolean()
|
|
51523
52372
|
}), _void(), {
|
|
51524
52373
|
kind: "mutation",
|
|
51525
52374
|
auth: "protected"
|
|
51526
52375
|
}), method(object({
|
|
51527
|
-
deviceId: DeviceIdSchema$
|
|
52376
|
+
deviceId: DeviceIdSchema$2,
|
|
51528
52377
|
on: boolean()
|
|
51529
52378
|
}), _void(), {
|
|
51530
52379
|
kind: "mutation",
|
|
51531
52380
|
auth: "protected"
|
|
51532
52381
|
}), method(object({
|
|
51533
|
-
deviceId: DeviceIdSchema$
|
|
52382
|
+
deviceId: DeviceIdSchema$2,
|
|
51534
52383
|
on: boolean()
|
|
51535
52384
|
}), _void(), {
|
|
51536
52385
|
kind: "mutation",
|
|
51537
52386
|
auth: "protected"
|
|
51538
52387
|
}), method(object({
|
|
51539
|
-
deviceId: DeviceIdSchema$
|
|
52388
|
+
deviceId: DeviceIdSchema$2,
|
|
51540
52389
|
key: string().min(1),
|
|
51541
52390
|
value: DishwasherValueSchema
|
|
51542
52391
|
}), _void(), {
|
|
51543
52392
|
kind: "mutation",
|
|
51544
52393
|
auth: "protected"
|
|
51545
|
-
}), method(object({ deviceId: DeviceIdSchema$
|
|
52394
|
+
}), method(object({ deviceId: DeviceIdSchema$2 }), DishwasherOptionsSchema, { auth: "protected" });
|
|
51546
52395
|
/**
|
|
51547
52396
|
* Palette entry ids (D939). Audio is the one gradient; `audio-*` leaves are
|
|
51548
52397
|
* not entries. `heatmapScrub` is a mark, not an event kind.
|
|
@@ -52729,15 +53578,15 @@ var FanPresetCaptureResultSchema = object({
|
|
|
52729
53578
|
items: array(FanCaptureItemSchema),
|
|
52730
53579
|
omitted: array(FanCaptureOmissionSchema)
|
|
52731
53580
|
});
|
|
52732
|
-
var deviceInput$
|
|
53581
|
+
var deviceInput$2 = object({ deviceId: number().int().nonnegative() });
|
|
52733
53582
|
DeviceType.Fan;
|
|
52734
|
-
method(deviceInput$
|
|
53583
|
+
method(deviceInput$2, array(FanPresetSchema), { auth: "protected" }), method(deviceInput$2.extend({ presets: array(FanPresetSchema).max(12) }), FanPresetsSetResultSchema, {
|
|
52735
53584
|
kind: "mutation",
|
|
52736
53585
|
auth: "admin"
|
|
52737
|
-
}), method(deviceInput$
|
|
53586
|
+
}), method(deviceInput$2.extend({ presetId: string().min(1) }), FanPresetApplyResultSchema, {
|
|
52738
53587
|
kind: "mutation",
|
|
52739
53588
|
auth: "protected"
|
|
52740
|
-
}), method(deviceInput$
|
|
53589
|
+
}), method(deviceInput$2.extend({
|
|
52741
53590
|
fields: array(FanCaptureFieldSchema).optional(),
|
|
52742
53591
|
steps: array(ActionStepSchema).max(20).default([])
|
|
52743
53592
|
}), FanPresetCaptureResultSchema, { auth: "protected" });
|
|
@@ -53261,13 +54110,50 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
|
|
|
53261
54110
|
*/
|
|
53262
54111
|
var ConfigSchemaPassthrough$3 = custom();
|
|
53263
54112
|
var IntegrationInstanceIdSchema = string().min(1);
|
|
54113
|
+
var IntegrationInstanceModeSchema = _enum(["single", "multiple"]);
|
|
54114
|
+
/**
|
|
54115
|
+
* One kind of instance a provider can create (an adapter, an account type). The sign-in and
|
|
54116
|
+
* connect fields follow the `broker` contract (`broker.cap.ts`): `signIn.basePath` is a
|
|
54117
|
+
* hub-relative route set speaking the sign-in contract, `connectPath` / `disconnectPath` open an
|
|
54118
|
+
* interactive account link (popup) and may carry `{{brokerId}}`, which the UI replaces with the
|
|
54119
|
+
* instance id.
|
|
54120
|
+
*/
|
|
54121
|
+
var IntegrationInstanceKindSchema = object({
|
|
54122
|
+
/** Stable id of the kind, sent back as `createInstance.kind`. */
|
|
54123
|
+
id: string().min(1),
|
|
54124
|
+
label: string().min(1),
|
|
54125
|
+
/** The kind signs in through a server-held interactive session. */
|
|
54126
|
+
signIn: object({
|
|
54127
|
+
basePath: string().min(1),
|
|
54128
|
+
methods: array(_enum(["device-code", "proxy"])).optional()
|
|
54129
|
+
}).optional(),
|
|
54130
|
+
/** Relative hub path to start an interactive account link of an instance of this kind. */
|
|
54131
|
+
connectPath: string().min(1).optional(),
|
|
54132
|
+
/** Relative hub path to disconnect that account. */
|
|
54133
|
+
disconnectPath: string().min(1).optional()
|
|
54134
|
+
});
|
|
53264
54135
|
var IntegrationInstanceDescriptionSchema = object({
|
|
53265
54136
|
/** `single`: at most one instance can exist (Loki). `multiple`: an account each. */
|
|
53266
|
-
instanceMode:
|
|
54137
|
+
instanceMode: IntegrationInstanceModeSchema,
|
|
53267
54138
|
/** What the instance is, for the UI (`destination`, `relay`, `account`). */
|
|
53268
54139
|
kind: string(),
|
|
53269
54140
|
/** True when `testInstance` really probes the remote. */
|
|
53270
|
-
canTest: boolean()
|
|
54141
|
+
canTest: boolean(),
|
|
54142
|
+
/**
|
|
54143
|
+
* The kinds an instance can be of. Absent or one entry: nothing to choose; more than one: the
|
|
54144
|
+
* Add flow asks which first and sends it as `createInstance.kind`.
|
|
54145
|
+
*/
|
|
54146
|
+
kinds: array(IntegrationInstanceKindSchema).optional(),
|
|
54147
|
+
/**
|
|
54148
|
+
* The provider accepts `createInstance.id` for an id that once existed (an adopted id, because
|
|
54149
|
+
* devices carry it). The UI then offers "restore under its old id" on Add. Absent means no.
|
|
54150
|
+
*/
|
|
54151
|
+
allowRestoreId: boolean().optional(),
|
|
54152
|
+
/**
|
|
54153
|
+
* `createInstance` without an `id` works: the provider mints it. Absent means the caller must
|
|
54154
|
+
* send an id (the UI then mints an opaque one).
|
|
54155
|
+
*/
|
|
54156
|
+
mintsId: boolean().optional()
|
|
53271
54157
|
});
|
|
53272
54158
|
var IntegrationInstanceStatusSchema = object({
|
|
53273
54159
|
state: _enum([
|
|
@@ -53276,16 +54162,27 @@ var IntegrationInstanceStatusSchema = object({
|
|
|
53276
54162
|
"error",
|
|
53277
54163
|
"unknown"
|
|
53278
54164
|
]),
|
|
53279
|
-
detail: string().optional()
|
|
54165
|
+
detail: string().optional(),
|
|
54166
|
+
/** A short human word for the state ("Signed in", "Token expired"); shown instead of `state`. */
|
|
54167
|
+
label: string().optional()
|
|
53280
54168
|
});
|
|
53281
54169
|
var IntegrationInstanceSchema = object({
|
|
53282
54170
|
id: IntegrationInstanceIdSchema,
|
|
53283
54171
|
name: string(),
|
|
53284
54172
|
/** The addon's own rule (an URL is present, a client id is present...). */
|
|
53285
54173
|
configured: boolean(),
|
|
54174
|
+
/**
|
|
54175
|
+
* The instance's own on/off switch (D996): always present, derived from the record body
|
|
54176
|
+
* (`enabled !== false`, so a row written before the field existed is on).
|
|
54177
|
+
*/
|
|
54178
|
+
enabled: boolean(),
|
|
54179
|
+
/** The kind of this instance (one of `describe().kinds[].id`), when the provider has kinds. */
|
|
54180
|
+
kind: string().optional(),
|
|
53286
54181
|
/** Values for the instance form; a secret is a `SecretFieldView`, never text. */
|
|
53287
54182
|
formValues: record(string(), unknown()),
|
|
53288
|
-
status: IntegrationInstanceStatusSchema.optional()
|
|
54183
|
+
status: IntegrationInstanceStatusSchema.optional(),
|
|
54184
|
+
/** Epoch ms of the last probe of the remote that produced `status`; absent when never checked. */
|
|
54185
|
+
lastCheckedAt: number().optional()
|
|
53289
54186
|
});
|
|
53290
54187
|
var IntegrationInstancePatchSchema = object({
|
|
53291
54188
|
id: IntegrationInstanceIdSchema,
|
|
@@ -53293,25 +54190,57 @@ var IntegrationInstancePatchSchema = object({
|
|
|
53293
54190
|
/** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
|
|
53294
54191
|
values: record(string(), unknown())
|
|
53295
54192
|
});
|
|
53296
|
-
|
|
53297
|
-
|
|
53298
|
-
|
|
53299
|
-
|
|
53300
|
-
|
|
53301
|
-
|
|
53302
|
-
|
|
53303
|
-
|
|
53304
|
-
|
|
53305
|
-
|
|
53306
|
-
|
|
53307
|
-
|
|
53308
|
-
|
|
53309
|
-
|
|
53310
|
-
|
|
53311
|
-
|
|
53312
|
-
|
|
53313
|
-
|
|
53314
|
-
})
|
|
54193
|
+
var integrationInstancesCapability = {
|
|
54194
|
+
name: "integration-instances",
|
|
54195
|
+
scope: "system",
|
|
54196
|
+
mode: "collection",
|
|
54197
|
+
methods: {
|
|
54198
|
+
describe: method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }),
|
|
54199
|
+
/** The form; `id: null` is the create form. */
|
|
54200
|
+
instanceFormSchema: method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3, { auth: "admin" }),
|
|
54201
|
+
listInstances: method(_void(), array(IntegrationInstanceSchema).readonly(), { auth: "admin" }),
|
|
54202
|
+
getInstance: method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable(), { auth: "admin" }),
|
|
54203
|
+
/**
|
|
54204
|
+
* Insert the typed record. With `id` it is stored under that id (a duplicate id is refused);
|
|
54205
|
+
* without one the PROVIDER mints it and answers it. `kind` picks one of `describe().kinds`.
|
|
54206
|
+
*/
|
|
54207
|
+
createInstance: method(object({
|
|
54208
|
+
id: IntegrationInstanceIdSchema.optional(),
|
|
54209
|
+
name: string().min(1),
|
|
54210
|
+
kind: string().min(1).optional()
|
|
54211
|
+
}), object({ id: string() }), {
|
|
54212
|
+
kind: "mutation",
|
|
54213
|
+
auth: "admin"
|
|
54214
|
+
}),
|
|
54215
|
+
updateInstance: method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
|
|
54216
|
+
kind: "mutation",
|
|
54217
|
+
auth: "admin"
|
|
54218
|
+
}),
|
|
54219
|
+
/**
|
|
54220
|
+
* Switch ONE instance on or off. A row control, not a form field: `updateInstance.values` is the
|
|
54221
|
+
* form patch (D62, D996). An off instance keeps its record and secrets and does no work.
|
|
54222
|
+
*/
|
|
54223
|
+
setInstanceEnabled: method(object({
|
|
54224
|
+
id: IntegrationInstanceIdSchema,
|
|
54225
|
+
enabled: boolean()
|
|
54226
|
+
}), object({ ok: literal(true) }), {
|
|
54227
|
+
kind: "mutation",
|
|
54228
|
+
auth: "admin"
|
|
54229
|
+
}),
|
|
54230
|
+
/** Delete the record and its keychain refs together. */
|
|
54231
|
+
deleteInstance: method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
|
|
54232
|
+
kind: "mutation",
|
|
54233
|
+
auth: "admin"
|
|
54234
|
+
}),
|
|
54235
|
+
testInstance: optionalMethod(object({
|
|
54236
|
+
id: IntegrationInstanceIdSchema.nullable(),
|
|
54237
|
+
values: record(string(), unknown())
|
|
54238
|
+
}), TestConnectionResultSchema$1, {
|
|
54239
|
+
kind: "mutation",
|
|
54240
|
+
auth: "admin"
|
|
54241
|
+
})
|
|
54242
|
+
}
|
|
54243
|
+
};
|
|
53315
54244
|
/**
|
|
53316
54245
|
* Two-way audio / talk-back cap. Clients open a WebRTC audio-only
|
|
53317
54246
|
* session (server-initiated offer pattern, identical to the main
|
|
@@ -55110,6 +56039,11 @@ var LogEntrySchema = object({
|
|
|
55110
56039
|
* flight, and not asking for the next slice ends the scan.
|
|
55111
56040
|
*/
|
|
55112
56041
|
var LogReadQuerySchema = object({
|
|
56042
|
+
/**
|
|
56043
|
+
* Which destination INSTANCE answers (a destination addon may hold several records). Absent ⇒
|
|
56044
|
+
* the provider's first readable one, so a caller written before instances existed is unchanged.
|
|
56045
|
+
*/
|
|
56046
|
+
instanceId: string().min(1).max(200).optional(),
|
|
55113
56047
|
/** Correlates `cancelRead`. Caller-chosen, unique per in-flight read. */
|
|
55114
56048
|
requestId: string().min(1).max(64).optional(),
|
|
55115
56049
|
/** The device the line is about (`tags.deviceId`, numeric). */
|
|
@@ -55172,6 +56106,8 @@ var LogReadResultSchema = object({
|
|
|
55172
56106
|
/** True when `cancelRead` ended this slice early. */
|
|
55173
56107
|
cancelled: boolean().optional()
|
|
55174
56108
|
});
|
|
56109
|
+
/** Names one destination instance; absent ⇒ the provider's first readable one. */
|
|
56110
|
+
var LogInstanceSelectorSchema = object({ instanceId: string().min(1).max(200).optional() });
|
|
55175
56111
|
var LogReadCapabilitiesSchema = object({
|
|
55176
56112
|
/** False ⇒ this destination only ships lines (console, forwarder, an unconfigured Loki). */
|
|
55177
56113
|
readable: boolean(),
|
|
@@ -55190,7 +56126,7 @@ var LogReadCapabilitiesSchema = object({
|
|
|
55190
56126
|
/** How far back the store reaches, when the provider knows (days). */
|
|
55191
56127
|
retentionDays: number().nullable()
|
|
55192
56128
|
});
|
|
55193
|
-
method(LogEntrySchema, _void(), { kind: "mutation" }), optionalMethod(
|
|
56129
|
+
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() }), {
|
|
55194
56130
|
kind: "mutation",
|
|
55195
56131
|
auth: "admin"
|
|
55196
56132
|
});
|
|
@@ -55294,164 +56230,6 @@ var LoginMethodContributionSchema = discriminatedUnion("kind", [
|
|
|
55294
56230
|
]);
|
|
55295
56231
|
method(_void(), array(LoginMethodContributionSchema).readonly());
|
|
55296
56232
|
/**
|
|
55297
|
-
* Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
|
|
55298
|
-
* Apple TV, Spotify, Roku, generic OTT receivers, …) on
|
|
55299
|
-
* `DeviceType.MediaPlayer`. Disjoint from the camera streaming
|
|
55300
|
-
* surface — those use `DeviceType.Camera` + the broker pipeline.
|
|
55301
|
-
*
|
|
55302
|
-
* The cap declares a wide method surface; not every backend supports
|
|
55303
|
-
* every method. Drivers signal sub-capability via the
|
|
55304
|
-
* `DeviceFeature.MediaPlayerXxx` flags (paired one-to-one with the
|
|
55305
|
-
* corresponding method) so the UI gates buttons without a slice
|
|
55306
|
-
* fetch — same pattern as `PanTiltZoom` for cameras.
|
|
55307
|
-
*/
|
|
55308
|
-
var MediaPlayerStateSchema = _enum([
|
|
55309
|
-
"off",
|
|
55310
|
-
"on",
|
|
55311
|
-
"idle",
|
|
55312
|
-
"playing",
|
|
55313
|
-
"paused",
|
|
55314
|
-
"buffering",
|
|
55315
|
-
"standby"
|
|
55316
|
-
]);
|
|
55317
|
-
var MediaPlayerRepeatSchema = _enum([
|
|
55318
|
-
"off",
|
|
55319
|
-
"all",
|
|
55320
|
-
"one"
|
|
55321
|
-
]);
|
|
55322
|
-
var MediaInfoSchema = object({
|
|
55323
|
-
/** Free-form media kind (`music`, `tvshow`, `movie`, `app`, `channel`,
|
|
55324
|
-
* `podcast`, …). */
|
|
55325
|
-
type: string(),
|
|
55326
|
-
/** Human-readable title. */
|
|
55327
|
-
title: string(),
|
|
55328
|
-
/** Optional artist / channel / station label. */
|
|
55329
|
-
artist: string().optional(),
|
|
55330
|
-
/** Optional album / season / show name. */
|
|
55331
|
-
album: string().optional(),
|
|
55332
|
-
/** Optional cover-art / thumbnail URL. */
|
|
55333
|
-
imageUrl: string().optional()
|
|
55334
|
-
});
|
|
55335
|
-
/**
|
|
55336
|
-
* Where `state`, `volumeLevel`, `isMuted` and `source` come from. `reported`:
|
|
55337
|
-
* the device said so. `assumed`: the driver has no feedback (infrared) and
|
|
55338
|
-
* tracks what it last SENT; it moves only after a send was accepted, and a
|
|
55339
|
-
* human at the real remote makes it wrong. A UI shows an assumed value as a
|
|
55340
|
-
* guess, never as a reading.
|
|
55341
|
-
*/
|
|
55342
|
-
var MediaPlayerStateSourceSchema = _enum(["reported", "assumed"]);
|
|
55343
|
-
/** The fields of a media-player status a driver can only guess. */
|
|
55344
|
-
var MediaPlayerAssumedFieldSchema = _enum([
|
|
55345
|
-
"state",
|
|
55346
|
-
"volumeLevel",
|
|
55347
|
-
"isMuted",
|
|
55348
|
-
"source"
|
|
55349
|
-
]);
|
|
55350
|
-
object({
|
|
55351
|
-
/** Playback lifecycle state. */
|
|
55352
|
-
state: MediaPlayerStateSchema,
|
|
55353
|
-
/**
|
|
55354
|
-
* Whether the state fields are readings or the driver's own bookkeeping.
|
|
55355
|
-
* A slice written before this field existed parses as `reported`.
|
|
55356
|
-
*/
|
|
55357
|
-
stateSource: MediaPlayerStateSourceSchema.default("reported"),
|
|
55358
|
-
/**
|
|
55359
|
-
* Which fields are the driver's own guess. `stateSource` is `assumed` when
|
|
55360
|
-
* this is not empty; a field not listed here is a reading even then (power
|
|
55361
|
-
* from a running-state source, mute guessed from the last send).
|
|
55362
|
-
*/
|
|
55363
|
-
assumedFields: array(MediaPlayerAssumedFieldSchema).default([]),
|
|
55364
|
-
/** Volume as 0..100 inclusive. Null when the device has no volume
|
|
55365
|
-
* surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
|
|
55366
|
-
volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
|
|
55367
|
-
/** Mute toggle distinct from volume=0. Null when no mute surface.
|
|
55368
|
-
* Pair with `DeviceFeature.MediaPlayerMute`. */
|
|
55369
|
-
isMuted: boolean().nullable(),
|
|
55370
|
-
/** Active source / input. Empty when no source surface. */
|
|
55371
|
-
source: string(),
|
|
55372
|
-
/** Selectable sources. Empty when no source surface.
|
|
55373
|
-
* Pair with `DeviceFeature.MediaPlayerSelectSource`. */
|
|
55374
|
-
availableSources: array(string()),
|
|
55375
|
-
/** Currently-playing media info. Null when nothing is playing. */
|
|
55376
|
-
currentMedia: MediaInfoSchema.nullable(),
|
|
55377
|
-
/** Current playback position in ms. Null when not seekable or
|
|
55378
|
-
* nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
|
|
55379
|
-
position: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
|
|
55380
|
-
/** Total duration of the current media in ms. Null when unknown
|
|
55381
|
-
* (live stream) or nothing is playing. */
|
|
55382
|
-
duration: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
|
|
55383
|
-
/** Shuffle toggle. Null when no shuffle surface.
|
|
55384
|
-
* Pair with `DeviceFeature.MediaPlayerShuffle`. */
|
|
55385
|
-
shuffle: boolean().nullable(),
|
|
55386
|
-
/** Repeat mode. Null when no repeat surface.
|
|
55387
|
-
* Pair with `DeviceFeature.MediaPlayerRepeat`. */
|
|
55388
|
-
repeat: MediaPlayerRepeatSchema.nullable(),
|
|
55389
|
-
/** Ms epoch when the slice was last updated. */
|
|
55390
|
-
lastChangedAt: number()
|
|
55391
|
-
});
|
|
55392
|
-
DeviceType.MediaPlayer, method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
55393
|
-
kind: "mutation",
|
|
55394
|
-
auth: "protected"
|
|
55395
|
-
}), method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
55396
|
-
kind: "mutation",
|
|
55397
|
-
auth: "protected"
|
|
55398
|
-
}), method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
55399
|
-
kind: "mutation",
|
|
55400
|
-
auth: "protected"
|
|
55401
|
-
}), method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
55402
|
-
kind: "mutation",
|
|
55403
|
-
auth: "protected"
|
|
55404
|
-
}), method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
55405
|
-
kind: "mutation",
|
|
55406
|
-
auth: "protected"
|
|
55407
|
-
}), method(object({
|
|
55408
|
-
deviceId: number().int().nonnegative(),
|
|
55409
|
-
position: number().int().nonnegative()
|
|
55410
|
-
}), _void(), {
|
|
55411
|
-
kind: "mutation",
|
|
55412
|
-
auth: "protected"
|
|
55413
|
-
}), method(object({
|
|
55414
|
-
deviceId: number().int().nonnegative(),
|
|
55415
|
-
volumeLevel: number().min(0).max(100)
|
|
55416
|
-
}), _void(), {
|
|
55417
|
-
kind: "mutation",
|
|
55418
|
-
auth: "protected"
|
|
55419
|
-
}), method(object({
|
|
55420
|
-
deviceId: number().int().nonnegative(),
|
|
55421
|
-
muted: boolean()
|
|
55422
|
-
}), _void(), {
|
|
55423
|
-
kind: "mutation",
|
|
55424
|
-
auth: "protected"
|
|
55425
|
-
}), method(object({
|
|
55426
|
-
deviceId: number().int().nonnegative(),
|
|
55427
|
-
shuffle: boolean()
|
|
55428
|
-
}), _void(), {
|
|
55429
|
-
kind: "mutation",
|
|
55430
|
-
auth: "protected"
|
|
55431
|
-
}), method(object({
|
|
55432
|
-
deviceId: number().int().nonnegative(),
|
|
55433
|
-
repeat: MediaPlayerRepeatSchema
|
|
55434
|
-
}), _void(), {
|
|
55435
|
-
kind: "mutation",
|
|
55436
|
-
auth: "protected"
|
|
55437
|
-
}), method(object({
|
|
55438
|
-
deviceId: number().int().nonnegative(),
|
|
55439
|
-
source: string().min(1)
|
|
55440
|
-
}), _void(), {
|
|
55441
|
-
kind: "mutation",
|
|
55442
|
-
auth: "protected"
|
|
55443
|
-
}), method(object({
|
|
55444
|
-
deviceId: number().int().nonnegative(),
|
|
55445
|
-
/** Media identifier / URL. */
|
|
55446
|
-
mediaId: string().min(1),
|
|
55447
|
-
/** Media kind (`music`, `tvshow`, `movie`, `app`, …) — provider
|
|
55448
|
-
* passes it through to the upstream service. */
|
|
55449
|
-
mediaType: string().min(1)
|
|
55450
|
-
}), _void(), {
|
|
55451
|
-
kind: "mutation",
|
|
55452
|
-
auth: "protected"
|
|
55453
|
-
});
|
|
55454
|
-
/**
|
|
55455
56233
|
* mesh-network — collection cap for mesh-VPN providers.
|
|
55456
56234
|
*
|
|
55457
56235
|
* Different shape from `network-access` (public tunnels):
|
|
@@ -63345,7 +64123,14 @@ var NcMediaPolicySchema = object({
|
|
|
63345
64123
|
*/
|
|
63346
64124
|
profile: CamProfileSchema.optional(),
|
|
63347
64125
|
/** See {@link NcLinkedCameraModeSchema}. */
|
|
63348
|
-
linkedCameras: NcLinkedCameraModeSchema.optional()
|
|
64126
|
+
linkedCameras: NcLinkedCameraModeSchema.optional(),
|
|
64127
|
+
/**
|
|
64128
|
+
* Sensor rules only: may this rule wake a SLEEPING battery camera to take
|
|
64129
|
+
* the linked still it attaches (D998)? Absent means yes, an explicit `false`
|
|
64130
|
+
* opts out. Deliberately not a Zod default: a rule that never chose keeps
|
|
64131
|
+
* following the hub's. One bounded wake per event; nothing is attached later.
|
|
64132
|
+
*/
|
|
64133
|
+
wakeSleepingCamera: boolean().optional()
|
|
63349
64134
|
});
|
|
63350
64135
|
/**
|
|
63351
64136
|
* Cooldown GRANULARITY over the subject's class — how much a fired
|
|
@@ -63737,6 +64522,21 @@ var NcTemplateVarKindSchema = _enum([
|
|
|
63737
64522
|
"sound",
|
|
63738
64523
|
"system"
|
|
63739
64524
|
]);
|
|
64525
|
+
/**
|
|
64526
|
+
* The default title/body a rule of one SITUATION is delivered with when the
|
|
64527
|
+
* operator wrote no template — served so an editor can pre-fill it (and the
|
|
64528
|
+
* operator can rebuild it from placeholders) instead of starting from a blank
|
|
64529
|
+
* box that, once saved, silently replaces it. `id` names the situation inside
|
|
64530
|
+
* a rule `kind`: `appliance-cycle` is a `sensor` rule whose device is an
|
|
64531
|
+
* appliance finishing a cycle. Texts are raw templates (`{{var}}` intact), in
|
|
64532
|
+
* the hub's current language and with its overrides applied.
|
|
64533
|
+
*/
|
|
64534
|
+
var NcTemplateDefaultSchema = object({
|
|
64535
|
+
id: _enum(["appliance-cycle"]),
|
|
64536
|
+
kind: NcRuleKindSchema,
|
|
64537
|
+
title: string(),
|
|
64538
|
+
body: string()
|
|
64539
|
+
});
|
|
63740
64540
|
/** Which producer renders a template: an ordinary rule, a timelapse, a digest. */
|
|
63741
64541
|
var NcTemplateFamilySchema = _enum([
|
|
63742
64542
|
"rule",
|
|
@@ -64592,7 +65392,11 @@ method(object({}), object({ rules: array(NcRuleSchema) }), { auth: "admin" }), m
|
|
|
64592
65392
|
}), method(object({}), object({
|
|
64593
65393
|
catalog: array(NcConditionDescriptorSchema),
|
|
64594
65394
|
taxonomy: NcTaxonomySchema.optional()
|
|
64595
|
-
})), method(object({}), object({
|
|
65395
|
+
})), method(object({}), object({
|
|
65396
|
+
vars: array(NcTemplateVarDescriptorSchema),
|
|
65397
|
+
/** Per-situation default texts; absent on a hub that predates them. */
|
|
65398
|
+
defaults: array(NcTemplateDefaultSchema)
|
|
65399
|
+
})), method(NcTemplatePreviewInputSchema, NcTemplatePreviewSchema, {
|
|
64596
65400
|
kind: "mutation",
|
|
64597
65401
|
access: "view"
|
|
64598
65402
|
}), 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 }), {
|
|
@@ -65340,52 +66144,27 @@ object({
|
|
|
65340
66144
|
empty: boolean().nullable(),
|
|
65341
66145
|
/** True while the pump is running. Null while unknown. */
|
|
65342
66146
|
pumpRunning: boolean().nullable(),
|
|
65343
|
-
/** True when the pump works on an on/off duty cycle rather than continuously. */
|
|
65344
|
-
pumpIntervalEnabled: boolean().nullable(),
|
|
65345
|
-
/** How long the pump runs per cycle, in seconds. */
|
|
65346
|
-
pumpOnTime: number().min(0).nullable().meta(readingMeta("duration")),
|
|
65347
|
-
/** How long the pump rests per cycle, in seconds. */
|
|
65348
|
-
pumpOffTime: number().min(0).nullable().meta(readingMeta("duration")),
|
|
65349
66147
|
/** Water drunk in the last drink, in litres. */
|
|
65350
66148
|
lastDrink: number().min(0).nullable().meta(readingMeta("volume")),
|
|
65351
66149
|
/** Water drunk in the current hour, in litres. */
|
|
65352
66150
|
currentHourDrink: number().min(0).nullable().meta(readingMeta("volume")),
|
|
65353
66151
|
/** Water drunk over the last 24 hours (rolling), in litres. */
|
|
65354
66152
|
last24hDrink: number().min(0).nullable().meta(readingMeta("volume")),
|
|
65355
|
-
/** Water drunk since local midnight, in litres. */
|
|
66153
|
+
/** Water drunk since local midnight, in litres. Null when the fountain has no such sensor. */
|
|
65356
66154
|
todayDrink: number().min(0).nullable().meta(readingMeta("volume")),
|
|
65357
66155
|
/** Decoded fault message. Null means healthy; a non-null string is an active fault. */
|
|
65358
66156
|
error: string().nullable(),
|
|
65359
66157
|
/** Ms epoch when the slice was last refreshed. */
|
|
65360
66158
|
lastFetchedAt: number()
|
|
65361
66159
|
});
|
|
65362
|
-
var DeviceIdSchema = number().int().nonnegative();
|
|
65363
|
-
var MinutesSchema = number().int().min(0);
|
|
66160
|
+
var DeviceIdSchema$1 = number().int().nonnegative();
|
|
65364
66161
|
DeviceType.PetFountain, method(object({
|
|
65365
|
-
deviceId: DeviceIdSchema,
|
|
65366
|
-
on: boolean()
|
|
65367
|
-
}), _void(), {
|
|
65368
|
-
kind: "mutation",
|
|
65369
|
-
auth: "protected"
|
|
65370
|
-
}), method(object({
|
|
65371
|
-
deviceId: DeviceIdSchema,
|
|
66162
|
+
deviceId: DeviceIdSchema$1,
|
|
65372
66163
|
on: boolean()
|
|
65373
66164
|
}), _void(), {
|
|
65374
66165
|
kind: "mutation",
|
|
65375
66166
|
auth: "protected"
|
|
65376
|
-
}), method(object({
|
|
65377
|
-
deviceId: DeviceIdSchema,
|
|
65378
|
-
minutes: MinutesSchema
|
|
65379
|
-
}), _void(), {
|
|
65380
|
-
kind: "mutation",
|
|
65381
|
-
auth: "protected"
|
|
65382
|
-
}), method(object({
|
|
65383
|
-
deviceId: DeviceIdSchema,
|
|
65384
|
-
minutes: MinutesSchema
|
|
65385
|
-
}), _void(), {
|
|
65386
|
-
kind: "mutation",
|
|
65387
|
-
auth: "protected"
|
|
65388
|
-
}), method(object({ deviceId: DeviceIdSchema }), _void(), {
|
|
66167
|
+
}), method(object({ deviceId: DeviceIdSchema$1 }), _void(), {
|
|
65389
66168
|
kind: "mutation",
|
|
65390
66169
|
auth: "protected"
|
|
65391
66170
|
});
|
|
@@ -66919,6 +67698,49 @@ object({
|
|
|
66919
67698
|
DeviceType.PetFeeder,
|
|
66920
67699
|
DeviceType.Heater
|
|
66921
67700
|
].filter((type) => type !== DeviceType.Sensor);
|
|
67701
|
+
var PowerSourceRefusalReasonSchema = _enum([
|
|
67702
|
+
"no-power-source",
|
|
67703
|
+
"switch-missing",
|
|
67704
|
+
"switch-offline",
|
|
67705
|
+
"switch-unknown",
|
|
67706
|
+
"restart-in-progress",
|
|
67707
|
+
"switch-call-failed"
|
|
67708
|
+
]);
|
|
67709
|
+
var PowerSourceActionResultSchema = discriminatedUnion("accepted", [object({ accepted: literal(true) }), object({
|
|
67710
|
+
accepted: literal(false),
|
|
67711
|
+
reason: PowerSourceRefusalReasonSchema,
|
|
67712
|
+
/** One line for the operator, naming the switch. */
|
|
67713
|
+
message: string()
|
|
67714
|
+
})]);
|
|
67715
|
+
var PowerSourceInfoSchema = object({
|
|
67716
|
+
/** False when the device has no power source linked; the rest is then empty. */
|
|
67717
|
+
linked: boolean(),
|
|
67718
|
+
switchDeviceId: number().nullable(),
|
|
67719
|
+
switchName: string().nullable(),
|
|
67720
|
+
/** The switch's on/off state, or null when it is unknown. Never false for unknown (D393). */
|
|
67721
|
+
switchOn: boolean().nullable(),
|
|
67722
|
+
switchOnline: boolean().nullable(),
|
|
67723
|
+
restartInProgress: boolean(),
|
|
67724
|
+
offSeconds: number().int()
|
|
67725
|
+
});
|
|
67726
|
+
object({
|
|
67727
|
+
/** Null while the switch has not reported: unknown is never `false` (D393). */
|
|
67728
|
+
switchOn: boolean().nullable(),
|
|
67729
|
+
offSeconds: number().int(),
|
|
67730
|
+
lastFetchedAt: number()
|
|
67731
|
+
});
|
|
67732
|
+
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;
|
|
67733
|
+
var deviceInput$1 = object({ deviceId: number().int().nonnegative() });
|
|
67734
|
+
systemMethod(deviceInput$1, PowerSourceInfoSchema, { auth: "admin" }), systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
67735
|
+
kind: "mutation",
|
|
67736
|
+
auth: "admin"
|
|
67737
|
+
}), systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
67738
|
+
kind: "mutation",
|
|
67739
|
+
auth: "admin"
|
|
67740
|
+
}), systemMethod(deviceInput$1, PowerSourceActionResultSchema, {
|
|
67741
|
+
kind: "mutation",
|
|
67742
|
+
auth: "admin"
|
|
67743
|
+
});
|
|
66922
67744
|
/**
|
|
66923
67745
|
* Presence cap. Models HA `person.*` and `device_tracker.*` entities
|
|
66924
67746
|
* on `DeviceType.Presence`. Read-only — no setters: presence is
|
|
@@ -67345,6 +68167,37 @@ DeviceType.Camera, method(object({ deviceId: number() }), PtzAutotrackStatusSche
|
|
|
67345
68167
|
deviceId: number(),
|
|
67346
68168
|
status: PtzAutotrackStatusSchema
|
|
67347
68169
|
});
|
|
68170
|
+
object({
|
|
68171
|
+
/** True when the pump works on the on/off cycle, false when it runs continuously. Null while unknown. */
|
|
68172
|
+
enabled: boolean().nullable(),
|
|
68173
|
+
/** How long the pump runs per cycle, in seconds. Null while unknown. */
|
|
68174
|
+
onTime: number().min(0).nullable().meta(readingMeta("duration")),
|
|
68175
|
+
/** How long the pump rests per cycle, in seconds. Null while unknown. */
|
|
68176
|
+
offTime: number().min(0).nullable().meta(readingMeta("duration")),
|
|
68177
|
+
/** Ms epoch when the slice was last refreshed. */
|
|
68178
|
+
lastFetchedAt: number()
|
|
68179
|
+
});
|
|
68180
|
+
var DeviceIdSchema = number().int().nonnegative();
|
|
68181
|
+
var MinutesSchema = number().int().min(0);
|
|
68182
|
+
DeviceType.PetFountain, method(object({
|
|
68183
|
+
deviceId: DeviceIdSchema,
|
|
68184
|
+
on: boolean()
|
|
68185
|
+
}), _void(), {
|
|
68186
|
+
kind: "mutation",
|
|
68187
|
+
auth: "protected"
|
|
68188
|
+
}), method(object({
|
|
68189
|
+
deviceId: DeviceIdSchema,
|
|
68190
|
+
minutes: MinutesSchema
|
|
68191
|
+
}), _void(), {
|
|
68192
|
+
kind: "mutation",
|
|
68193
|
+
auth: "protected"
|
|
68194
|
+
}), method(object({
|
|
68195
|
+
deviceId: DeviceIdSchema,
|
|
68196
|
+
minutes: MinutesSchema
|
|
68197
|
+
}), _void(), {
|
|
68198
|
+
kind: "mutation",
|
|
68199
|
+
auth: "protected"
|
|
68200
|
+
});
|
|
67348
68201
|
DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, DeviceType.Gateway, method(object({ deviceId: number().int().nonnegative() }), object({ success: literal(true) }), {
|
|
67349
68202
|
kind: "mutation",
|
|
67350
68203
|
auth: "admin"
|
|
@@ -70058,7 +70911,13 @@ var RemoteMediaCommandSchema = _enum([
|
|
|
70058
70911
|
"next",
|
|
70059
70912
|
"previous",
|
|
70060
70913
|
"setMute",
|
|
70061
|
-
"selectSource"
|
|
70914
|
+
"selectSource",
|
|
70915
|
+
"turnOn",
|
|
70916
|
+
"turnOff",
|
|
70917
|
+
"volumeUp",
|
|
70918
|
+
"volumeDown",
|
|
70919
|
+
"channelUp",
|
|
70920
|
+
"channelDown"
|
|
70062
70921
|
]);
|
|
70063
70922
|
/** A send result, or `not-routed`: the role is not bound, so the native cap answers. */
|
|
70064
70923
|
var RemoteMediaCommandResultSchema = discriminatedUnion("status", [...RemoteSendResultSchema.options, object({ status: literal("not-routed") })]);
|
|
@@ -70079,7 +70938,7 @@ method(deviceInput, RemoteConfigSchema, { auth: "admin" }), method(deviceInput.e
|
|
|
70079
70938
|
}), RemoteSendResultSchema, {
|
|
70080
70939
|
kind: "mutation",
|
|
70081
70940
|
auth: "protected"
|
|
70082
|
-
}), method(deviceInput.extend({
|
|
70941
|
+
}), method(deviceInput, MediaPlayerStatusSchema.nullable(), { auth: "protected" }), method(deviceInput.extend({
|
|
70083
70942
|
command: RemoteMediaCommandSchema,
|
|
70084
70943
|
muted: boolean().optional(),
|
|
70085
70944
|
source: string().optional(),
|
|
@@ -71107,10 +71966,12 @@ DeviceType.Camera, method(object({
|
|
|
71107
71966
|
* even when the cache is well within the device's
|
|
71108
71967
|
* `snapshotMaxAgeS` window.
|
|
71109
71968
|
*
|
|
71110
|
-
* **`force` is
|
|
71969
|
+
* **`force` is a demand with a named principal, not a freshness
|
|
71970
|
+
* preference** — an operator button, the scheduled refresh (D847), or a
|
|
71971
|
+
* notification rule that left the camera's wake allowed (D998). On a
|
|
71111
71972
|
* battery camera it is the one thing that walks past the wrapper's
|
|
71112
|
-
* sleep gate and wakes the camera, so a
|
|
71113
|
-
*
|
|
71973
|
+
* sleep gate and wakes the camera, so a poller, a thumbnail or a
|
|
71974
|
+
* prestream must NEVER set it. Every such caller
|
|
71114
71975
|
* gets the cached frame, which on a sleeping battery camera is the
|
|
71115
71976
|
* correct answer: stale but honest beats woken.
|
|
71116
71977
|
*/
|
|
@@ -71918,7 +72779,11 @@ object({
|
|
|
71918
72779
|
});
|
|
71919
72780
|
Object.values(DeviceType), method(_void(), array(StreamSignalSchema).readonly());
|
|
71920
72781
|
object({
|
|
71921
|
-
|
|
72782
|
+
/**
|
|
72783
|
+
* `null` = the device has never reported its state (a measurement that
|
|
72784
|
+
* has not happened is unknown, never `false`).
|
|
72785
|
+
*/
|
|
72786
|
+
on: boolean().nullable().meta({ label: "On" }),
|
|
71922
72787
|
/** Ms epoch of the last state change. Useful for UI "X minutes ago". */
|
|
71923
72788
|
lastChangedAt: number()
|
|
71924
72789
|
});
|
|
@@ -71930,7 +72795,7 @@ DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceTyp
|
|
|
71930
72795
|
auth: "protected"
|
|
71931
72796
|
}), object({
|
|
71932
72797
|
deviceId: number(),
|
|
71933
|
-
on: boolean(),
|
|
72798
|
+
on: boolean().nullable(),
|
|
71934
72799
|
lastChangedAt: number()
|
|
71935
72800
|
});
|
|
71936
72801
|
/** C0 controls, DEL and the C1 range: none of them belongs in a name. */
|
|
@@ -74942,6 +75807,26 @@ function buildDescriptor(capName, kind) {
|
|
|
74942
75807
|
* contributes one entry per bound cap present in this map.
|
|
74943
75808
|
*/
|
|
74944
75809
|
var EVENT_KIND_BY_CAP = Object.freeze(Object.fromEntries(Object.entries(CAP_TO_KIND).map(([capName, kind]) => [capName, buildDescriptor(capName, kind)])));
|
|
75810
|
+
/**
|
|
75811
|
+
* What a remote key is CALLED and what it LOOKS like, for every role (D963),
|
|
75812
|
+
* shared by the admin UI and the viewer. A role is data on the wire; each
|
|
75813
|
+
* client resolves its name from `label` (English and Italian side by side) and
|
|
75814
|
+
* its glyph from `icon`, a lucide id from the shared icon catalogue — the
|
|
75815
|
+
* server sends an id, a client maps it to its own icon set.
|
|
75816
|
+
*
|
|
75817
|
+
* `legend` is text printed on the key INSTEAD of a glyph (a digit has no
|
|
75818
|
+
* pictogram): a client that can draw text shows it, one that cannot falls back
|
|
75819
|
+
* to `icon`.
|
|
75820
|
+
*/
|
|
75821
|
+
function digit(n) {
|
|
75822
|
+
return {
|
|
75823
|
+
label: t(n, n),
|
|
75824
|
+
icon: "hash",
|
|
75825
|
+
legend: n
|
|
75826
|
+
};
|
|
75827
|
+
}
|
|
75828
|
+
digit("0"), digit("1"), digit("2"), digit("3"), digit("4"), digit("5"), digit("6"), digit("7"), digit("8"), digit("9");
|
|
75829
|
+
DeviceFeature.MediaPlayerPower, DeviceFeature.MediaPlayerVolumeStep, DeviceFeature.MediaPlayerChannelStep, DeviceFeature.MediaPlayerMute, DeviceFeature.MediaPlayerStop, DeviceFeature.MediaPlayerNext, DeviceFeature.MediaPlayerPrevious;
|
|
74945
75830
|
var btn = (idSuffix, row, col, init) => ({
|
|
74946
75831
|
idSuffix,
|
|
74947
75832
|
row,
|
|
@@ -75068,7 +75953,35 @@ discriminatedUnion("kind", [object({
|
|
|
75068
75953
|
kind: literal("pronto"),
|
|
75069
75954
|
code: string()
|
|
75070
75955
|
})]);
|
|
75071
|
-
|
|
75956
|
+
/**
|
|
75957
|
+
* Propose a running threshold from a device's own power history (planning#405).
|
|
75958
|
+
*
|
|
75959
|
+
* Pure and deterministic: the same series gives the same answer. The series is
|
|
75960
|
+
* a HELD-VALUE step series — a point's value stands until the next point — which
|
|
75961
|
+
* is how device-history stores a gauge, so "time at a level" is exact, not a
|
|
75962
|
+
* count of samples.
|
|
75963
|
+
*
|
|
75964
|
+
* 1. idle floor F = time-weighted median; noise ceiling N = highest value seen
|
|
75965
|
+
* near F. A device that is not near one level most of the time has no floor.
|
|
75966
|
+
* 2. coarse activity = above max(2N, N + 5 W); consecutive stretches are episodes.
|
|
75967
|
+
* 3. gaps between episodes split in two by the largest log-scale jump (>= 3x):
|
|
75968
|
+
* the short ones are pauses INSIDE a run (a soak), the long ones separate runs.
|
|
75969
|
+
* 4. T = geometric mean of N and the plateau's 10th percentile, snapped to a
|
|
75970
|
+
* grid a person would type; stop dwell covers the longest pause; start dwell
|
|
75971
|
+
* covers the longest standby blip above T.
|
|
75972
|
+
* 5. the numbers are proved by REPLAY through the real `dwell` evaluation: each
|
|
75973
|
+
* run must hold exactly one true interval and nothing may flap outside.
|
|
75974
|
+
*/
|
|
75975
|
+
var SECOND_MS = 1e3;
|
|
75976
|
+
var MINUTE_MS$1 = 60 * SECOND_MS;
|
|
75977
|
+
24 * (60 * MINUTE_MS$1);
|
|
75978
|
+
10 * MINUTE_MS$1;
|
|
75979
|
+
30 * SECOND_MS;
|
|
75980
|
+
60 * SECOND_MS;
|
|
75981
|
+
20 * MINUTE_MS$1;
|
|
75982
|
+
10 * SECOND_MS;
|
|
75983
|
+
10 * SECOND_MS;
|
|
75984
|
+
DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
|
|
75072
75985
|
SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
|
|
75073
75986
|
String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
|
|
75074
75987
|
DeviceRole.BinarySensor, DeviceRole.MotionSensor;
|
|
@@ -76049,6 +76962,27 @@ Object.freeze({
|
|
|
76049
76962
|
access: "view",
|
|
76050
76963
|
role: "viewer"
|
|
76051
76964
|
},
|
|
76965
|
+
"boardKeySource.getBoardKey": {
|
|
76966
|
+
capName: "board-key-source",
|
|
76967
|
+
capScope: "system",
|
|
76968
|
+
addonId: null,
|
|
76969
|
+
access: "view",
|
|
76970
|
+
role: "admin"
|
|
76971
|
+
},
|
|
76972
|
+
"boardKeySource.listBoards": {
|
|
76973
|
+
capName: "board-key-source",
|
|
76974
|
+
capScope: "system",
|
|
76975
|
+
addonId: null,
|
|
76976
|
+
access: "view",
|
|
76977
|
+
role: "admin"
|
|
76978
|
+
},
|
|
76979
|
+
"boardKeySource.listSources": {
|
|
76980
|
+
capName: "board-key-source",
|
|
76981
|
+
capScope: "system",
|
|
76982
|
+
addonId: null,
|
|
76983
|
+
access: "view",
|
|
76984
|
+
role: "admin"
|
|
76985
|
+
},
|
|
76052
76986
|
"brightness.getStatus": {
|
|
76053
76987
|
capName: "brightness",
|
|
76054
76988
|
capScope: "device",
|
|
@@ -76588,6 +77522,13 @@ Object.freeze({
|
|
|
76588
77522
|
access: "create",
|
|
76589
77523
|
role: "admin"
|
|
76590
77524
|
},
|
|
77525
|
+
"coreBlocks.proposeRunningThreshold": {
|
|
77526
|
+
capName: "core-blocks",
|
|
77527
|
+
capScope: "system",
|
|
77528
|
+
addonId: null,
|
|
77529
|
+
access: "view",
|
|
77530
|
+
role: "admin"
|
|
77531
|
+
},
|
|
76591
77532
|
"coreBlocks.reportAutomationRun": {
|
|
76592
77533
|
capName: "core-blocks",
|
|
76593
77534
|
capScope: "system",
|
|
@@ -77099,6 +78040,13 @@ Object.freeze({
|
|
|
77099
78040
|
access: "view",
|
|
77100
78041
|
role: "admin"
|
|
77101
78042
|
},
|
|
78043
|
+
"deviceExport.listInstances": {
|
|
78044
|
+
capName: "device-export",
|
|
78045
|
+
capScope: "system",
|
|
78046
|
+
addonId: null,
|
|
78047
|
+
access: "view",
|
|
78048
|
+
role: "admin"
|
|
78049
|
+
},
|
|
77102
78050
|
"deviceExport.listSupportedDeviceKinds": {
|
|
77103
78051
|
capName: "device-export",
|
|
77104
78052
|
capScope: "system",
|
|
@@ -78730,6 +79678,13 @@ Object.freeze({
|
|
|
78730
79678
|
access: "view",
|
|
78731
79679
|
role: "admin"
|
|
78732
79680
|
},
|
|
79681
|
+
"integrationInstances.setInstanceEnabled": {
|
|
79682
|
+
capName: "integration-instances",
|
|
79683
|
+
capScope: "system",
|
|
79684
|
+
addonId: null,
|
|
79685
|
+
access: "create",
|
|
79686
|
+
role: "admin"
|
|
79687
|
+
},
|
|
78733
79688
|
"integrationInstances.testInstance": {
|
|
78734
79689
|
capName: "integration-instances",
|
|
78735
79690
|
capScope: "system",
|
|
@@ -79528,6 +80483,20 @@ Object.freeze({
|
|
|
79528
80483
|
access: "view",
|
|
79529
80484
|
role: "admin"
|
|
79530
80485
|
},
|
|
80486
|
+
"mediaPlayer.channelDown": {
|
|
80487
|
+
capName: "media-player",
|
|
80488
|
+
capScope: "device",
|
|
80489
|
+
addonId: null,
|
|
80490
|
+
access: "create",
|
|
80491
|
+
role: "viewer"
|
|
80492
|
+
},
|
|
80493
|
+
"mediaPlayer.channelUp": {
|
|
80494
|
+
capName: "media-player",
|
|
80495
|
+
capScope: "device",
|
|
80496
|
+
addonId: null,
|
|
80497
|
+
access: "create",
|
|
80498
|
+
role: "viewer"
|
|
80499
|
+
},
|
|
79531
80500
|
"mediaPlayer.getStatus": {
|
|
79532
80501
|
capName: "media-player",
|
|
79533
80502
|
capScope: "device",
|
|
@@ -79619,6 +80588,34 @@ Object.freeze({
|
|
|
79619
80588
|
access: "create",
|
|
79620
80589
|
role: "viewer"
|
|
79621
80590
|
},
|
|
80591
|
+
"mediaPlayer.turnOff": {
|
|
80592
|
+
capName: "media-player",
|
|
80593
|
+
capScope: "device",
|
|
80594
|
+
addonId: null,
|
|
80595
|
+
access: "create",
|
|
80596
|
+
role: "viewer"
|
|
80597
|
+
},
|
|
80598
|
+
"mediaPlayer.turnOn": {
|
|
80599
|
+
capName: "media-player",
|
|
80600
|
+
capScope: "device",
|
|
80601
|
+
addonId: null,
|
|
80602
|
+
access: "create",
|
|
80603
|
+
role: "viewer"
|
|
80604
|
+
},
|
|
80605
|
+
"mediaPlayer.volumeDown": {
|
|
80606
|
+
capName: "media-player",
|
|
80607
|
+
capScope: "device",
|
|
80608
|
+
addonId: null,
|
|
80609
|
+
access: "create",
|
|
80610
|
+
role: "viewer"
|
|
80611
|
+
},
|
|
80612
|
+
"mediaPlayer.volumeUp": {
|
|
80613
|
+
capName: "media-player",
|
|
80614
|
+
capScope: "device",
|
|
80615
|
+
addonId: null,
|
|
80616
|
+
access: "create",
|
|
80617
|
+
role: "viewer"
|
|
80618
|
+
},
|
|
79622
80619
|
"meshNetwork.getStatus": {
|
|
79623
80620
|
capName: "mesh-network",
|
|
79624
80621
|
capScope: "system",
|
|
@@ -80865,27 +81862,6 @@ Object.freeze({
|
|
|
80865
81862
|
access: "create",
|
|
80866
81863
|
role: "viewer"
|
|
80867
81864
|
},
|
|
80868
|
-
"petFountain.setPumpInterval": {
|
|
80869
|
-
capName: "pet-fountain",
|
|
80870
|
-
capScope: "device",
|
|
80871
|
-
addonId: null,
|
|
80872
|
-
access: "create",
|
|
80873
|
-
role: "viewer"
|
|
80874
|
-
},
|
|
80875
|
-
"petFountain.setPumpOffMinutes": {
|
|
80876
|
-
capName: "pet-fountain",
|
|
80877
|
-
capScope: "device",
|
|
80878
|
-
addonId: null,
|
|
80879
|
-
access: "create",
|
|
80880
|
-
role: "viewer"
|
|
80881
|
-
},
|
|
80882
|
-
"petFountain.setPumpOnMinutes": {
|
|
80883
|
-
capName: "pet-fountain",
|
|
80884
|
-
capScope: "device",
|
|
80885
|
-
addonId: null,
|
|
80886
|
-
access: "create",
|
|
80887
|
-
role: "viewer"
|
|
80888
|
-
},
|
|
80889
81865
|
"pipelineAnalytics.addRetrainFrameFromRecording": {
|
|
80890
81866
|
capName: "pipeline-analytics",
|
|
80891
81867
|
capScope: "device",
|
|
@@ -82664,6 +83640,34 @@ Object.freeze({
|
|
|
82664
83640
|
access: "create",
|
|
82665
83641
|
role: "admin"
|
|
82666
83642
|
},
|
|
83643
|
+
"pumpCycle.getStatus": {
|
|
83644
|
+
capName: "pump-cycle",
|
|
83645
|
+
capScope: "device",
|
|
83646
|
+
addonId: null,
|
|
83647
|
+
access: "view",
|
|
83648
|
+
role: "viewer"
|
|
83649
|
+
},
|
|
83650
|
+
"pumpCycle.setEnabled": {
|
|
83651
|
+
capName: "pump-cycle",
|
|
83652
|
+
capScope: "device",
|
|
83653
|
+
addonId: null,
|
|
83654
|
+
access: "create",
|
|
83655
|
+
role: "viewer"
|
|
83656
|
+
},
|
|
83657
|
+
"pumpCycle.setOffMinutes": {
|
|
83658
|
+
capName: "pump-cycle",
|
|
83659
|
+
capScope: "device",
|
|
83660
|
+
addonId: null,
|
|
83661
|
+
access: "create",
|
|
83662
|
+
role: "viewer"
|
|
83663
|
+
},
|
|
83664
|
+
"pumpCycle.setOnMinutes": {
|
|
83665
|
+
capName: "pump-cycle",
|
|
83666
|
+
capScope: "device",
|
|
83667
|
+
addonId: null,
|
|
83668
|
+
access: "create",
|
|
83669
|
+
role: "viewer"
|
|
83670
|
+
},
|
|
82667
83671
|
"reboot.reboot": {
|
|
82668
83672
|
capName: "reboot",
|
|
82669
83673
|
capScope: "device",
|
|
@@ -83168,6 +84172,13 @@ Object.freeze({
|
|
|
83168
84172
|
access: "create",
|
|
83169
84173
|
role: "admin"
|
|
83170
84174
|
},
|
|
84175
|
+
"remote.getAssumedStatus": {
|
|
84176
|
+
capName: "remote",
|
|
84177
|
+
capScope: "system",
|
|
84178
|
+
addonId: null,
|
|
84179
|
+
access: "view",
|
|
84180
|
+
role: "admin"
|
|
84181
|
+
},
|
|
83171
84182
|
"remote.getConfig": {
|
|
83172
84183
|
capName: "remote",
|
|
83173
84184
|
capScope: "system",
|
|
@@ -85983,6 +86994,11 @@ Object.freeze({
|
|
|
85983
86994
|
form: "single",
|
|
85984
86995
|
optional: false
|
|
85985
86996
|
}],
|
|
86997
|
+
"coreBlocks.proposeRunningThreshold": [{
|
|
86998
|
+
name: "deviceId",
|
|
86999
|
+
form: "single",
|
|
87000
|
+
optional: false
|
|
87001
|
+
}],
|
|
85986
87002
|
"cover.close": [{
|
|
85987
87003
|
name: "deviceId",
|
|
85988
87004
|
form: "single",
|
|
@@ -86834,6 +87850,16 @@ Object.freeze({
|
|
|
86834
87850
|
form: "single",
|
|
86835
87851
|
optional: true
|
|
86836
87852
|
}],
|
|
87853
|
+
"mediaPlayer.channelDown": [{
|
|
87854
|
+
name: "deviceId",
|
|
87855
|
+
form: "single",
|
|
87856
|
+
optional: false
|
|
87857
|
+
}],
|
|
87858
|
+
"mediaPlayer.channelUp": [{
|
|
87859
|
+
name: "deviceId",
|
|
87860
|
+
form: "single",
|
|
87861
|
+
optional: false
|
|
87862
|
+
}],
|
|
86837
87863
|
"mediaPlayer.getStatus": [{
|
|
86838
87864
|
name: "deviceId",
|
|
86839
87865
|
form: "single",
|
|
@@ -86899,6 +87925,26 @@ Object.freeze({
|
|
|
86899
87925
|
form: "single",
|
|
86900
87926
|
optional: false
|
|
86901
87927
|
}],
|
|
87928
|
+
"mediaPlayer.turnOff": [{
|
|
87929
|
+
name: "deviceId",
|
|
87930
|
+
form: "single",
|
|
87931
|
+
optional: false
|
|
87932
|
+
}],
|
|
87933
|
+
"mediaPlayer.turnOn": [{
|
|
87934
|
+
name: "deviceId",
|
|
87935
|
+
form: "single",
|
|
87936
|
+
optional: false
|
|
87937
|
+
}],
|
|
87938
|
+
"mediaPlayer.volumeDown": [{
|
|
87939
|
+
name: "deviceId",
|
|
87940
|
+
form: "single",
|
|
87941
|
+
optional: false
|
|
87942
|
+
}],
|
|
87943
|
+
"mediaPlayer.volumeUp": [{
|
|
87944
|
+
name: "deviceId",
|
|
87945
|
+
form: "single",
|
|
87946
|
+
optional: false
|
|
87947
|
+
}],
|
|
86902
87948
|
"meter.getStatus": [{
|
|
86903
87949
|
name: "deviceId",
|
|
86904
87950
|
form: "single",
|
|
@@ -87208,21 +88254,6 @@ Object.freeze({
|
|
|
87208
88254
|
form: "single",
|
|
87209
88255
|
optional: false
|
|
87210
88256
|
}],
|
|
87211
|
-
"petFountain.setPumpInterval": [{
|
|
87212
|
-
name: "deviceId",
|
|
87213
|
-
form: "single",
|
|
87214
|
-
optional: false
|
|
87215
|
-
}],
|
|
87216
|
-
"petFountain.setPumpOffMinutes": [{
|
|
87217
|
-
name: "deviceId",
|
|
87218
|
-
form: "single",
|
|
87219
|
-
optional: false
|
|
87220
|
-
}],
|
|
87221
|
-
"petFountain.setPumpOnMinutes": [{
|
|
87222
|
-
name: "deviceId",
|
|
87223
|
-
form: "single",
|
|
87224
|
-
optional: false
|
|
87225
|
-
}],
|
|
87226
88257
|
"pipelineAnalytics.addRetrainFrameFromRecording": [{
|
|
87227
88258
|
name: "deviceId",
|
|
87228
88259
|
form: "single",
|
|
@@ -87858,6 +88889,26 @@ Object.freeze({
|
|
|
87858
88889
|
form: "single",
|
|
87859
88890
|
optional: false
|
|
87860
88891
|
}],
|
|
88892
|
+
"pumpCycle.getStatus": [{
|
|
88893
|
+
name: "deviceId",
|
|
88894
|
+
form: "single",
|
|
88895
|
+
optional: false
|
|
88896
|
+
}],
|
|
88897
|
+
"pumpCycle.setEnabled": [{
|
|
88898
|
+
name: "deviceId",
|
|
88899
|
+
form: "single",
|
|
88900
|
+
optional: false
|
|
88901
|
+
}],
|
|
88902
|
+
"pumpCycle.setOffMinutes": [{
|
|
88903
|
+
name: "deviceId",
|
|
88904
|
+
form: "single",
|
|
88905
|
+
optional: false
|
|
88906
|
+
}],
|
|
88907
|
+
"pumpCycle.setOnMinutes": [{
|
|
88908
|
+
name: "deviceId",
|
|
88909
|
+
form: "single",
|
|
88910
|
+
optional: false
|
|
88911
|
+
}],
|
|
87861
88912
|
"reboot.reboot": [{
|
|
87862
88913
|
name: "deviceId",
|
|
87863
88914
|
form: "single",
|
|
@@ -88059,6 +89110,11 @@ Object.freeze({
|
|
|
88059
89110
|
form: "single",
|
|
88060
89111
|
optional: true
|
|
88061
89112
|
}],
|
|
89113
|
+
"remote.getAssumedStatus": [{
|
|
89114
|
+
name: "deviceId",
|
|
89115
|
+
form: "single",
|
|
89116
|
+
optional: false
|
|
89117
|
+
}],
|
|
88062
89118
|
"remote.getConfig": [{
|
|
88063
89119
|
name: "deviceId",
|
|
88064
89120
|
form: "single",
|
|
@@ -88606,99 +89662,6 @@ Object.freeze({
|
|
|
88606
89662
|
"network-access": "ingress",
|
|
88607
89663
|
"smtp-provider": "email"
|
|
88608
89664
|
});
|
|
88609
|
-
/**
|
|
88610
|
-
* A saved Customize assignment (D936). The schema is the type: callers use
|
|
88611
|
-
* `z.infer`, they do not restate the shape.
|
|
88612
|
-
*/
|
|
88613
|
-
var OverrideSourceSchema = object({
|
|
88614
|
-
addonId: string().min(1),
|
|
88615
|
-
stableId: string().min(1),
|
|
88616
|
-
cap: string().min(1),
|
|
88617
|
-
fieldPath: string().min(1)
|
|
88618
|
-
});
|
|
88619
|
-
var CustomizeTransformDraftSchema = discriminatedUnion("kind", [
|
|
88620
|
-
object({ kind: literal("identity") }),
|
|
88621
|
-
object({ kind: literal("invert") }),
|
|
88622
|
-
object({
|
|
88623
|
-
kind: literal("threshold"),
|
|
88624
|
-
comparison: _enum(["gte", "lte"]),
|
|
88625
|
-
threshold: number(),
|
|
88626
|
-
startDwellMs: number().nonnegative(),
|
|
88627
|
-
stopDwellMs: number().nonnegative()
|
|
88628
|
-
}),
|
|
88629
|
-
object({
|
|
88630
|
-
kind: literal("linear"),
|
|
88631
|
-
scale: number(),
|
|
88632
|
-
offset: number(),
|
|
88633
|
-
clampMin: number().optional(),
|
|
88634
|
-
clampMax: number().optional()
|
|
88635
|
-
}),
|
|
88636
|
-
object({
|
|
88637
|
-
kind: literal("enum-map"),
|
|
88638
|
-
mapping: record(string(), string().nullable())
|
|
88639
|
-
}),
|
|
88640
|
-
object({ kind: literal("number") })
|
|
88641
|
-
]);
|
|
88642
|
-
var ScalarOverrideDraftSchema = object({
|
|
88643
|
-
kind: literal("scalar"),
|
|
88644
|
-
cap: string().min(1),
|
|
88645
|
-
targetId: string().min(1),
|
|
88646
|
-
source: OverrideSourceSchema,
|
|
88647
|
-
transform: CustomizeTransformDraftSchema
|
|
88648
|
-
});
|
|
88649
|
-
var KeyedArrayOverrideDraftSchema = object({
|
|
88650
|
-
kind: literal("keyedArray"),
|
|
88651
|
-
cap: string().min(1),
|
|
88652
|
-
targetId: string().min(1),
|
|
88653
|
-
key: string().min(1),
|
|
88654
|
-
label: string().min(1),
|
|
88655
|
-
fieldPath: string().min(1),
|
|
88656
|
-
source: OverrideSourceSchema,
|
|
88657
|
-
transform: CustomizeTransformDraftSchema
|
|
88658
|
-
});
|
|
88659
|
-
var PulseOverrideDraftSchema = object({
|
|
88660
|
-
kind: literal("pulse"),
|
|
88661
|
-
cap: string().min(1),
|
|
88662
|
-
targetId: string().min(1),
|
|
88663
|
-
enabled: boolean(),
|
|
88664
|
-
debounceMs: number().nonnegative(),
|
|
88665
|
-
edges: array(_enum([
|
|
88666
|
-
"rising",
|
|
88667
|
-
"button",
|
|
88668
|
-
"native-event"
|
|
88669
|
-
])).min(1).optional(),
|
|
88670
|
-
sources: array(OverrideSourceSchema).min(1)
|
|
88671
|
-
});
|
|
88672
|
-
var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
|
|
88673
|
-
"in-addition",
|
|
88674
|
-
"confirm",
|
|
88675
|
-
"replace"
|
|
88676
|
-
]).optional() });
|
|
88677
|
-
discriminatedUnion("kind", [
|
|
88678
|
-
ScalarOverrideDraftSchema,
|
|
88679
|
-
KeyedArrayOverrideDraftSchema,
|
|
88680
|
-
PulseOverrideDraftSchema,
|
|
88681
|
-
object({
|
|
88682
|
-
kind: literal("span"),
|
|
88683
|
-
cap: string().min(1),
|
|
88684
|
-
targetId: string().min(1),
|
|
88685
|
-
timeoutMs: number().nonnegative(),
|
|
88686
|
-
cooldownMs: number().nonnegative().optional(),
|
|
88687
|
-
sources: array(SpanSourceDraftSchema)
|
|
88688
|
-
})
|
|
88689
|
-
]);
|
|
88690
|
-
object({
|
|
88691
|
-
targetId: string().min(1),
|
|
88692
|
-
configured: boolean(),
|
|
88693
|
-
source: _enum(["native", "assigned"]),
|
|
88694
|
-
state: _enum([
|
|
88695
|
-
"active",
|
|
88696
|
-
"stale",
|
|
88697
|
-
"released",
|
|
88698
|
-
"unavailable"
|
|
88699
|
-
]),
|
|
88700
|
-
reason: string().nullable()
|
|
88701
|
-
});
|
|
88702
89665
|
var G711_SCALE_CORRECTION_DB = {
|
|
88703
89666
|
PCMU: 20 * Math.log10(4),
|
|
88704
89667
|
PCMA: 20 * Math.log10(8)
|
|
@@ -89314,6 +90277,7 @@ var STATE_SUMMARY_VARS = [
|
|
|
89314
90277
|
[...RULE_TEXT],
|
|
89315
90278
|
([...RULE_TEXT], [...DEVICE_EVENTS]),
|
|
89316
90279
|
([...RULE_TEXT], [...COMBINED_ALARM]),
|
|
90280
|
+
([...RULE_TEXT], [...COMBINED_ALARM]),
|
|
89317
90281
|
([...RULE_TEXT], [...DEVICE_EVENTS]),
|
|
89318
90282
|
([...RULE_TEXT], [...BATTERY, ...CONSUMABLE]),
|
|
89319
90283
|
([...RULE_TEXT], [...CONSUMABLE]),
|
|
@@ -90075,13 +91039,20 @@ var GoogleBrokerEntrySchema = object({
|
|
|
90075
91039
|
calendarFilter: string().default(""),
|
|
90076
91040
|
/** Default From for Gmail send — usually the connected account email. */
|
|
90077
91041
|
fromAddress: string().default(""),
|
|
90078
|
-
fromName: string().default("")
|
|
91042
|
+
fromName: string().default(""),
|
|
91043
|
+
/**
|
|
91044
|
+
* The stamped calendar ids (`<brokerId>:<googleCalendarId>`) this broker imports. `undefined`
|
|
91045
|
+
* means "not moved here yet": the reader falls back to the shared `calendarIds` setting of
|
|
91046
|
+
* the addon until the migration copied it. Empty = import none.
|
|
91047
|
+
*/
|
|
91048
|
+
calendarIds: array(string()).optional()
|
|
90079
91049
|
});
|
|
90080
91050
|
var GoogleServicesConfigSchema = object({
|
|
90081
91051
|
brokers: array(GoogleBrokerEntrySchema).default([]),
|
|
90082
91052
|
/**
|
|
90083
|
-
*
|
|
90084
|
-
*
|
|
91053
|
+
* @deprecated The shared set (`brokerId:googleCalendarId`). Each broker owns its own
|
|
91054
|
+
* `calendarIds` now; this key is read only while a broker has none, and the migration
|
|
91055
|
+
* `google-services/2026-11-03-drop-shared-calendar-ids` deletes it.
|
|
90085
91056
|
*/
|
|
90086
91057
|
calendarIds: array(string()).default([]),
|
|
90087
91058
|
/** @deprecated Pre-broker singleton — cleared after migrateLegacyToBrokers. */
|
|
@@ -90159,14 +91130,15 @@ function settingsToBrokerFields(settings) {
|
|
|
90159
91130
|
clientSecret: parsed.clientSecret,
|
|
90160
91131
|
calendarFilter: calendarIdsToFilter(parsed),
|
|
90161
91132
|
fromAddress: parsed.fromAddress ?? "",
|
|
90162
|
-
fromName: parsed.fromName ?? ""
|
|
91133
|
+
fromName: parsed.fromName ?? "",
|
|
91134
|
+
...parsed.calendarIds !== void 0 ? { calendarIds: [...parsed.calendarIds] } : {}
|
|
90163
91135
|
};
|
|
90164
91136
|
}
|
|
90165
91137
|
function brokerEntryToSettings(entry) {
|
|
90166
91138
|
return {
|
|
90167
91139
|
clientId: entry.clientId,
|
|
90168
91140
|
clientSecret: entry.clientSecret,
|
|
90169
|
-
calendarIds: [...parseCalendarFilter(entry.calendarFilter)],
|
|
91141
|
+
calendarIds: [...entry.calendarIds ?? parseCalendarFilter(entry.calendarFilter)],
|
|
90170
91142
|
fromAddress: entry.fromAddress,
|
|
90171
91143
|
fromName: entry.fromName
|
|
90172
91144
|
};
|
|
@@ -90217,6 +91189,13 @@ function migrateLegacyToBrokers(rawConfig, legacyOauth) {
|
|
|
90217
91189
|
didMigrate: true
|
|
90218
91190
|
};
|
|
90219
91191
|
}
|
|
91192
|
+
/**
|
|
91193
|
+
* The stamped calendar ids one broker imports: its own set, or — while the migration has not
|
|
91194
|
+
* reached it — the shared setting older versions wrote, so the move never changes what is imported.
|
|
91195
|
+
*/
|
|
91196
|
+
function calendarIdsOfEntry(entry, shared) {
|
|
91197
|
+
return new Set(entry?.calendarIds ?? shared);
|
|
91198
|
+
}
|
|
90220
91199
|
function parseCalendarFilter(raw) {
|
|
90221
91200
|
const trimmed = raw.trim();
|
|
90222
91201
|
if (!trimmed) return /* @__PURE__ */ new Set();
|
|
@@ -90303,6 +91282,7 @@ var GoogleBrokerRegistry = class {
|
|
|
90303
91282
|
const entry = GoogleBrokerEntrySchema.parse({
|
|
90304
91283
|
id,
|
|
90305
91284
|
name,
|
|
91285
|
+
calendarIds: [],
|
|
90306
91286
|
...fields
|
|
90307
91287
|
});
|
|
90308
91288
|
this.entries = [...this.entries, entry];
|
|
@@ -90316,6 +91296,7 @@ var GoogleBrokerRegistry = class {
|
|
|
90316
91296
|
const next = GoogleBrokerEntrySchema.parse({
|
|
90317
91297
|
id: prev.id,
|
|
90318
91298
|
name: prev.name,
|
|
91299
|
+
...prev.calendarIds !== void 0 ? { calendarIds: prev.calendarIds } : {},
|
|
90319
91300
|
...fields
|
|
90320
91301
|
});
|
|
90321
91302
|
this.entries = this.entries.map((e, i) => i === idx ? next : e);
|
|
@@ -90521,6 +91502,24 @@ function buildGoogleBrokerSettingsSchema(publicOrigin) {
|
|
|
90521
91502
|
showToggle: true
|
|
90522
91503
|
}]
|
|
90523
91504
|
},
|
|
91505
|
+
{
|
|
91506
|
+
id: "google-calendars-import",
|
|
91507
|
+
title: "Calendars to import",
|
|
91508
|
+
titleKey: "google-services:googleBrokerSettings.s_google-calendars-import.title",
|
|
91509
|
+
description: "Nothing selected = no calendars imported from this account. Options appear once the account is signed in.",
|
|
91510
|
+
descriptionKey: "google-services:googleBrokerSettings.s_google-calendars-import.description",
|
|
91511
|
+
columns: 1,
|
|
91512
|
+
fields: [{
|
|
91513
|
+
type: "multiselect",
|
|
91514
|
+
key: "calendarIds",
|
|
91515
|
+
label: "Calendars",
|
|
91516
|
+
labelKey: "google-services:googleBrokerSettings.s_google-calendars-import.calendarIds.label",
|
|
91517
|
+
options: [],
|
|
91518
|
+
optionsFrom: "_calendarCatalog",
|
|
91519
|
+
description: "The calendars of this Google account that CamStack imports.",
|
|
91520
|
+
descriptionKey: "google-services:googleBrokerSettings.s_google-calendars-import.calendarIds.description"
|
|
91521
|
+
}]
|
|
91522
|
+
},
|
|
90524
91523
|
{
|
|
90525
91524
|
id: "gmail-send",
|
|
90526
91525
|
title: "Gmail send",
|
|
@@ -90544,7 +91543,7 @@ function buildGoogleBrokerSettingsSchema(publicOrigin) {
|
|
|
90544
91543
|
}
|
|
90545
91544
|
] };
|
|
90546
91545
|
}
|
|
90547
|
-
/** Connectivity Calendars / Email pages — calendar import picker
|
|
91546
|
+
/** Connectivity Calendars / Email pages — the calendar import picker is on each broker. */
|
|
90548
91547
|
function buildGoogleFeatureSettingsSchema(cap, publicOrigin) {
|
|
90549
91548
|
const redirect = escapeHtml(redirectUriDisplay(publicOrigin));
|
|
90550
91549
|
if (cap === "calendar-source") return { sections: [{
|
|
@@ -90560,27 +91559,10 @@ function buildGoogleFeatureSettingsSchema(cap, publicOrigin) {
|
|
|
90560
91559
|
format: "html",
|
|
90561
91560
|
content: [
|
|
90562
91561
|
"<p>Link a Google account under Connectivity → <strong>Brokers</strong> ",
|
|
90563
|
-
"(Client ID / secret + Sign in).
|
|
91562
|
+
"(Client ID / secret + Sign in). Pick the calendars to import on each broker. ",
|
|
90564
91563
|
`OAuth redirect URI: <code>${redirect}</code>.</p>`
|
|
90565
91564
|
].join("")
|
|
90566
91565
|
}]
|
|
90567
|
-
}, {
|
|
90568
|
-
id: "google-calendars-import",
|
|
90569
|
-
title: "Calendars to import",
|
|
90570
|
-
titleKey: "google-services:googleFeatureSettings.s_google-calendars-import.title",
|
|
90571
|
-
description: "Nothing selected = no calendars imported. Options appear after at least one Google broker is signed in.",
|
|
90572
|
-
descriptionKey: "google-services:googleFeatureSettings.s_google-calendars-import.description",
|
|
90573
|
-
columns: 1,
|
|
90574
|
-
fields: [{
|
|
90575
|
-
type: "multiselect",
|
|
90576
|
-
key: "calendarIds",
|
|
90577
|
-
label: "Calendars",
|
|
90578
|
-
labelKey: "google-services:googleFeatureSettings.s_google-calendars-import.calendarIds.label",
|
|
90579
|
-
options: [],
|
|
90580
|
-
optionsFrom: "_calendarCatalog",
|
|
90581
|
-
description: "Ids are stamped as brokerId:googleCalendarId across every Google broker.",
|
|
90582
|
-
descriptionKey: "google-services:googleFeatureSettings.s_google-calendars-import.calendarIds.description"
|
|
90583
|
-
}]
|
|
90584
91566
|
}] };
|
|
90585
91567
|
return { sections: [{
|
|
90586
91568
|
id: "setup-intro",
|
|
@@ -90670,11 +91652,11 @@ function createGoogleCalendarApi(getAccessToken, http = { fetch }) {
|
|
|
90670
91652
|
function createGoogleCalendarSource(deps) {
|
|
90671
91653
|
return {
|
|
90672
91654
|
listCalendars: async () => {
|
|
90673
|
-
const selected = deps.selectedCalendarIds();
|
|
90674
|
-
if (selected.size === 0) return [];
|
|
90675
91655
|
const out = [];
|
|
90676
91656
|
for (const account of deps.accounts()) {
|
|
90677
91657
|
if (!account.connected) continue;
|
|
91658
|
+
const selected = deps.selectedCalendarIds(account.brokerId);
|
|
91659
|
+
if (selected.size === 0) continue;
|
|
90678
91660
|
try {
|
|
90679
91661
|
const list = await account.api.listCalendars();
|
|
90680
91662
|
for (const c of list) {
|
|
@@ -90697,20 +91679,18 @@ function createGoogleCalendarSource(deps) {
|
|
|
90697
91679
|
return out;
|
|
90698
91680
|
},
|
|
90699
91681
|
listEvents: async (input) => {
|
|
90700
|
-
const selected = deps.selectedCalendarIds();
|
|
90701
|
-
if (selected.size === 0) return [];
|
|
90702
91682
|
const timeMin = new Date(input.startMs).toISOString();
|
|
90703
91683
|
const timeMax = new Date(input.endMs).toISOString();
|
|
90704
91684
|
const accounts = deps.accounts().filter((a) => a.connected);
|
|
90705
91685
|
const byBroker = new Map(accounts.map((a) => [a.brokerId, a]));
|
|
90706
|
-
const requestIds = input.calendarIds !== void 0 && input.calendarIds.length > 0 ? input.calendarIds : [...
|
|
91686
|
+
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)))];
|
|
90707
91687
|
/** brokerId → google calendar ids to fetch */
|
|
90708
91688
|
const wanted = /* @__PURE__ */ new Map();
|
|
90709
91689
|
for (const stamped of requestIds) {
|
|
90710
|
-
if (!selected.has(stamped)) continue;
|
|
90711
91690
|
const parsed = parseStampedCalendarId(stamped);
|
|
90712
91691
|
if (parsed === null) continue;
|
|
90713
91692
|
if (!byBroker.has(parsed.brokerId)) continue;
|
|
91693
|
+
if (!deps.selectedCalendarIds(parsed.brokerId).has(stamped)) continue;
|
|
90714
91694
|
const list = wanted.get(parsed.brokerId) ?? [];
|
|
90715
91695
|
list.push(parsed.googleCalendarId);
|
|
90716
91696
|
wanted.set(parsed.brokerId, list);
|
|
@@ -91059,11 +92039,233 @@ var googleBrokersEntryKind = defineEntryKind("brokers", {
|
|
|
91059
92039
|
clientSecret: GoogleBrokerEntrySchema.shape.clientSecret,
|
|
91060
92040
|
calendarFilter: GoogleBrokerEntrySchema.shape.calendarFilter,
|
|
91061
92041
|
fromAddress: GoogleBrokerEntrySchema.shape.fromAddress,
|
|
91062
|
-
fromName: GoogleBrokerEntrySchema.shape.fromName
|
|
92042
|
+
fromName: GoogleBrokerEntrySchema.shape.fromName,
|
|
92043
|
+
calendarIds: GoogleBrokerEntrySchema.shape.calendarIds
|
|
91063
92044
|
}),
|
|
91064
92045
|
writeTrigger: "an operator adds or edits a Google OAuth broker"
|
|
91065
92046
|
});
|
|
91066
92047
|
//#endregion
|
|
92048
|
+
//#region src/google-instances.ts
|
|
92049
|
+
/**
|
|
92050
|
+
* The Google accounts as `integration-instances` (planning#408 P3): one instance per broker
|
|
92051
|
+
* record, over the SAME `brokers` records and ids the `broker` cap serves (`google_NNN`), so
|
|
92052
|
+
* nothing moves. The OAuth session lives beside the record (`oauthAccounts`), so an instance's
|
|
92053
|
+
* status is the registry's answer and a change re-seeds the registry.
|
|
92054
|
+
*/
|
|
92055
|
+
function asTestResult(r) {
|
|
92056
|
+
return r.ok ? {
|
|
92057
|
+
success: true,
|
|
92058
|
+
status: "validated",
|
|
92059
|
+
...r.latencyMs !== void 0 ? { latencyMs: r.latencyMs } : {}
|
|
92060
|
+
} : {
|
|
92061
|
+
success: false,
|
|
92062
|
+
status: "rejected",
|
|
92063
|
+
error: r.error ?? "Google rejected the check"
|
|
92064
|
+
};
|
|
92065
|
+
}
|
|
92066
|
+
function buildGoogleInstances(records, deps) {
|
|
92067
|
+
return new RecordIntegrationInstances(records, googleBrokersEntryKind, {
|
|
92068
|
+
description: {
|
|
92069
|
+
instanceMode: "multiple",
|
|
92070
|
+
kind: "account",
|
|
92071
|
+
canTest: true,
|
|
92072
|
+
kinds: [{
|
|
92073
|
+
id: GOOGLE_BROKER_KIND,
|
|
92074
|
+
label: GOOGLE_BROKER_KIND_LABEL,
|
|
92075
|
+
connectPath: deps.connectPath,
|
|
92076
|
+
disconnectPath: deps.disconnectPath
|
|
92077
|
+
}],
|
|
92078
|
+
allowRestoreId: true
|
|
92079
|
+
},
|
|
92080
|
+
formSchema: () => {
|
|
92081
|
+
const full = buildGoogleBrokerSettingsSchema(deps.publicOrigin());
|
|
92082
|
+
return {
|
|
92083
|
+
...full,
|
|
92084
|
+
sections: full.sections.filter((s) => s.id !== "google-calendars-import")
|
|
92085
|
+
};
|
|
92086
|
+
},
|
|
92087
|
+
initialBody: (name) => ({
|
|
92088
|
+
name,
|
|
92089
|
+
clientId: "",
|
|
92090
|
+
clientSecret: "",
|
|
92091
|
+
calendarFilter: "",
|
|
92092
|
+
fromAddress: "",
|
|
92093
|
+
fromName: "",
|
|
92094
|
+
calendarIds: []
|
|
92095
|
+
}),
|
|
92096
|
+
mintId: deps.mintId,
|
|
92097
|
+
kindOf: () => GOOGLE_BROKER_KIND,
|
|
92098
|
+
health: (_body, id) => {
|
|
92099
|
+
const session = deps.session(id);
|
|
92100
|
+
if (session === "connected") return { status: {
|
|
92101
|
+
state: "ok",
|
|
92102
|
+
label: "Signed in"
|
|
92103
|
+
} };
|
|
92104
|
+
if (session === "reauth") return { status: {
|
|
92105
|
+
state: "error",
|
|
92106
|
+
label: "Sign in again",
|
|
92107
|
+
detail: "Google session expired"
|
|
92108
|
+
} };
|
|
92109
|
+
return { status: {
|
|
92110
|
+
state: "unknown",
|
|
92111
|
+
label: "Not signed in"
|
|
92112
|
+
} };
|
|
92113
|
+
},
|
|
92114
|
+
configured: (body) => body.clientId.length > 0 && (body.clientSecret ?? "").length > 0,
|
|
92115
|
+
onChanged: deps.onChanged,
|
|
92116
|
+
test: async ({ id, values }) => {
|
|
92117
|
+
const clientId = values["clientId"];
|
|
92118
|
+
const secret = values["clientSecret"];
|
|
92119
|
+
if (typeof clientId === "string" && clientId.length > 0 && typeof secret === "string" && secret.length > 0 && secret !== "__camstack_redacted__") return asTestResult(await deps.testClient(clientId, secret));
|
|
92120
|
+
if (id === null) return asTestResult({
|
|
92121
|
+
ok: false,
|
|
92122
|
+
error: "Enter the Client ID and Client secret first"
|
|
92123
|
+
});
|
|
92124
|
+
return asTestResult(await deps.testSaved(id));
|
|
92125
|
+
}
|
|
92126
|
+
});
|
|
92127
|
+
}
|
|
92128
|
+
//#endregion
|
|
92129
|
+
//#region src/migrations/calendar-ids-to-brokers.ts
|
|
92130
|
+
/**
|
|
92131
|
+
* The calendars a Google account imports are a fact of THAT account (D985): each broker record
|
|
92132
|
+
* carries its own `calendarIds`, and the addon-level `calendarIds` setting (one shared set of
|
|
92133
|
+
* `<brokerId>:<googleCalendarId>` ids) goes away.
|
|
92134
|
+
*
|
|
92135
|
+
* Two migrations, in this order:
|
|
92136
|
+
*
|
|
92137
|
+
* 1. `calendar-ids-to-brokers` (procedure) copies the shared set to EVERY broker record that has
|
|
92138
|
+
* no `calendarIds` yet. A record that already has one is never overwritten; a broker deleted
|
|
92139
|
+
* meanwhile is skipped, never recreated; a stored value that is not a list of strings is an
|
|
92140
|
+
* ambiguity and throws (nothing is guessed).
|
|
92141
|
+
* 2. `drop-shared-calendar-ids` (plan) deletes the shared setting, but only after a read-back:
|
|
92142
|
+
* every live broker must show its own `calendarIds`. When one of them holds a set that differs
|
|
92143
|
+
* from the shared one (an operator choice made before the copy), the shared value is the
|
|
92144
|
+
* loser and stays PARKED where it is, never deleted.
|
|
92145
|
+
*
|
|
92146
|
+
* Both are idempotent: with the copy landed the first reports `already-done`, and with the row
|
|
92147
|
+
* gone the second plans nothing.
|
|
92148
|
+
*/
|
|
92149
|
+
var GOOGLE_ADDON_ID = "google-services";
|
|
92150
|
+
/** The canonical settings table: one row per key, id `<addonId>.<key>`. */
|
|
92151
|
+
var ADDON_SETTINGS_TABLE = "addon-settings";
|
|
92152
|
+
/** The legacy shared set. */
|
|
92153
|
+
var SHARED_CALENDAR_IDS_KEY = "calendarIds";
|
|
92154
|
+
async function readSharedSet(ports) {
|
|
92155
|
+
const answer = await ports.readStored(SHARED_CALENDAR_IDS_KEY);
|
|
92156
|
+
if (answer.kind === "unreadable") throw new Error(`google-services: the shared calendarIds cannot be read (${answer.reason})`);
|
|
92157
|
+
if (answer.kind === "absent") return null;
|
|
92158
|
+
const value = answer.value;
|
|
92159
|
+
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");
|
|
92160
|
+
return value;
|
|
92161
|
+
}
|
|
92162
|
+
async function copyCalendarIdsToBrokers(ports) {
|
|
92163
|
+
const shared = await readSharedSet(ports);
|
|
92164
|
+
if (shared === null) return {
|
|
92165
|
+
outcome: "already-done",
|
|
92166
|
+
summary: { reason: "no-shared-set" }
|
|
92167
|
+
};
|
|
92168
|
+
let copied = 0;
|
|
92169
|
+
let kept = 0;
|
|
92170
|
+
let gone = 0;
|
|
92171
|
+
for (const broker of await ports.listBrokers()) {
|
|
92172
|
+
if (broker.calendarIds !== void 0) {
|
|
92173
|
+
kept += 1;
|
|
92174
|
+
continue;
|
|
92175
|
+
}
|
|
92176
|
+
if (await ports.setBrokerCalendarIds(broker.id, shared)) copied += 1;
|
|
92177
|
+
else gone += 1;
|
|
92178
|
+
}
|
|
92179
|
+
return {
|
|
92180
|
+
outcome: copied === 0 ? "already-done" : "done",
|
|
92181
|
+
summary: {
|
|
92182
|
+
copied,
|
|
92183
|
+
kept,
|
|
92184
|
+
gone
|
|
92185
|
+
}
|
|
92186
|
+
};
|
|
92187
|
+
}
|
|
92188
|
+
var sameSet = (a, b) => {
|
|
92189
|
+
const left = new Set(a);
|
|
92190
|
+
const right = new Set(b);
|
|
92191
|
+
return left.size === right.size && [...left].every((id) => right.has(id));
|
|
92192
|
+
};
|
|
92193
|
+
async function planDropSharedCalendarIds(ports) {
|
|
92194
|
+
const shared = await readSharedSet(ports);
|
|
92195
|
+
if (shared === null) return { edits: [] };
|
|
92196
|
+
const brokers = await ports.listBrokers();
|
|
92197
|
+
if (brokers.length === 0) return { edits: [] };
|
|
92198
|
+
const missing = brokers.filter((broker) => broker.calendarIds === void 0);
|
|
92199
|
+
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`);
|
|
92200
|
+
if (!brokers.every((broker) => sameSet(broker.calendarIds ?? [], shared))) return { edits: [] };
|
|
92201
|
+
const rowId = `${GOOGLE_ADDON_ID}.${SHARED_CALENDAR_IDS_KEY}`;
|
|
92202
|
+
return { edits: [{
|
|
92203
|
+
op: "deleteKeys",
|
|
92204
|
+
scope: ADDON_SETTINGS_TABLE,
|
|
92205
|
+
keys: [rowId],
|
|
92206
|
+
precondition: preconditionOf({ [rowId]: {
|
|
92207
|
+
addonId: GOOGLE_ADDON_ID,
|
|
92208
|
+
key: SHARED_CALENDAR_IDS_KEY,
|
|
92209
|
+
value: shared
|
|
92210
|
+
} })
|
|
92211
|
+
}] };
|
|
92212
|
+
}
|
|
92213
|
+
//#endregion
|
|
92214
|
+
//#region src/migrations/index.ts
|
|
92215
|
+
/** The `google-services` catalogue (D987): offered to the engine when the addon initialises. */
|
|
92216
|
+
var ADDON_MIGRATIONS = [{
|
|
92217
|
+
meta: {
|
|
92218
|
+
id: "google-services/2026-11-03-calendar-ids-to-brokers",
|
|
92219
|
+
owner: "google-services",
|
|
92220
|
+
level: "addon-state",
|
|
92221
|
+
seq: 1,
|
|
92222
|
+
dependsOn: [],
|
|
92223
|
+
store: "settings",
|
|
92224
|
+
scope: "cluster",
|
|
92225
|
+
onFailure: "tolerate",
|
|
92226
|
+
bodyKind: "procedure",
|
|
92227
|
+
description: "Copy the shared calendar set to every Google broker record that has none of its own",
|
|
92228
|
+
fixtureMark: "the client secret marker of a broker record the migration does not know"
|
|
92229
|
+
},
|
|
92230
|
+
procedure: copyCalendarIdsToBrokers
|
|
92231
|
+
}, {
|
|
92232
|
+
meta: {
|
|
92233
|
+
id: "google-services/2026-11-03-drop-shared-calendar-ids",
|
|
92234
|
+
owner: "google-services",
|
|
92235
|
+
level: "addon-state",
|
|
92236
|
+
seq: 2,
|
|
92237
|
+
dependsOn: ["google-services/2026-11-03-calendar-ids-to-brokers"],
|
|
92238
|
+
store: "settings",
|
|
92239
|
+
scope: "cluster",
|
|
92240
|
+
onFailure: "tolerate",
|
|
92241
|
+
bodyKind: "plan",
|
|
92242
|
+
description: "Delete the shared calendar set once every broker shows its own",
|
|
92243
|
+
fixtureMark: "the shared set parked when a broker holds a different one"
|
|
92244
|
+
},
|
|
92245
|
+
plan: planDropSharedCalendarIds
|
|
92246
|
+
}];
|
|
92247
|
+
//#endregion
|
|
92248
|
+
//#region src/migrations/ports.ts
|
|
92249
|
+
/** The migration's ports over the `brokers` record kind and the addon's raw settings read. */
|
|
92250
|
+
function calendarIdsPortsOver(kind, readStored) {
|
|
92251
|
+
return {
|
|
92252
|
+
readStored,
|
|
92253
|
+
listBrokers: async () => {
|
|
92254
|
+
return (await kind.query({ limit: 1e3 })).records.map((record) => ({
|
|
92255
|
+
id: record.id,
|
|
92256
|
+
calendarIds: record.data.calendarIds
|
|
92257
|
+
}));
|
|
92258
|
+
},
|
|
92259
|
+
setBrokerCalendarIds: async (id, ids) => {
|
|
92260
|
+
const row = await kind.get(id);
|
|
92261
|
+
if (row === null) return false;
|
|
92262
|
+
if (row.data.calendarIds !== void 0) return true;
|
|
92263
|
+
await kind.patch(id, { calendarIds: [...ids] });
|
|
92264
|
+
return true;
|
|
92265
|
+
}
|
|
92266
|
+
};
|
|
92267
|
+
}
|
|
92268
|
+
//#endregion
|
|
91067
92269
|
//#region src/google-broker-store.ts
|
|
91068
92270
|
function rowToEntry(id, data) {
|
|
91069
92271
|
const parsed = GoogleBrokerEntrySchema.safeParse({
|
|
@@ -91101,14 +92303,15 @@ var GoogleBrokerStore = class GoogleBrokerStore {
|
|
|
91101
92303
|
const nextIds = new Set(entries.map((e) => e.id));
|
|
91102
92304
|
for (const row of page.records) if (!nextIds.has(row.id)) await this.kind.delete(row.id);
|
|
91103
92305
|
for (const entry of entries) {
|
|
91104
|
-
const { id, name, clientId, clientSecret, calendarFilter, fromAddress, fromName } = entry;
|
|
92306
|
+
const { id, name, clientId, clientSecret, calendarFilter, fromAddress, fromName, calendarIds } = entry;
|
|
91105
92307
|
const body = {
|
|
91106
92308
|
name,
|
|
91107
92309
|
clientId,
|
|
91108
92310
|
clientSecret,
|
|
91109
92311
|
calendarFilter,
|
|
91110
92312
|
fromAddress,
|
|
91111
|
-
fromName
|
|
92313
|
+
fromName,
|
|
92314
|
+
...calendarIds !== void 0 ? { calendarIds } : {}
|
|
91112
92315
|
};
|
|
91113
92316
|
const parsed = googleBrokersEntryKind.schema.safeParse(body);
|
|
91114
92317
|
if (!parsed.success) throw new Error(`invalid Google broker '${id}': ${parsed.error.message}`);
|
|
@@ -91153,6 +92356,7 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
91153
92356
|
oauthAccountsHandle = null;
|
|
91154
92357
|
registry = new GoogleBrokerRegistry();
|
|
91155
92358
|
brokerStore = null;
|
|
92359
|
+
instances = null;
|
|
91156
92360
|
activeBrokers = [];
|
|
91157
92361
|
/** CSRF state → { brokerId, issuedAt } for the in-flight OAuth consents. */
|
|
91158
92362
|
pendingStates = /* @__PURE__ */ new Map();
|
|
@@ -91200,11 +92404,13 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
91200
92404
|
if (records === void 0) throw new Error("google-services: ctx.records is required (hub-only addon)");
|
|
91201
92405
|
await records.declare({ brokers: googleBrokersEntryKind });
|
|
91202
92406
|
this.brokerStore = GoogleBrokerStore.of(records);
|
|
92407
|
+
this.instances = this.buildInstances(records);
|
|
91203
92408
|
this.activeBrokers = await this.loadPersistedBrokers();
|
|
91204
92409
|
if ((await this.brokerStore.listResolved()).length === 0 && this.config.brokers.length > 0) {
|
|
91205
92410
|
await this.brokerStore.replaceAll(this.config.brokers);
|
|
91206
92411
|
this.activeBrokers = await this.loadPersistedBrokers();
|
|
91207
92412
|
}
|
|
92413
|
+
await this.migrateCalendarIds(records);
|
|
91208
92414
|
this.registry.seedFrom(this.activeBrokers, oauthAccounts.accounts);
|
|
91209
92415
|
await this.refreshPublicOrigin();
|
|
91210
92416
|
const calendarProvider = createGoogleCalendarSource({
|
|
@@ -91214,7 +92420,7 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
91214
92420
|
connected: this.registry.isConnected(entry.id),
|
|
91215
92421
|
api: createGoogleCalendarApi(() => this.ensureAccessToken(entry.id))
|
|
91216
92422
|
})),
|
|
91217
|
-
selectedCalendarIds: () =>
|
|
92423
|
+
selectedCalendarIds: (brokerId) => this.calendarIdsOf(brokerId)
|
|
91218
92424
|
});
|
|
91219
92425
|
const smtpProvider = createGmailSmtpProvider({ accounts: () => this.registry.listEntries().map((entry) => {
|
|
91220
92426
|
const oauth = this.registry.getOauth(entry.id);
|
|
@@ -91261,6 +92467,10 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
91261
92467
|
capability: brokerCapability,
|
|
91262
92468
|
provider: this.buildBrokerProvider()
|
|
91263
92469
|
},
|
|
92470
|
+
{
|
|
92471
|
+
capability: integrationInstancesCapability,
|
|
92472
|
+
provider: this.requireInstances().toProvider()
|
|
92473
|
+
},
|
|
91264
92474
|
{
|
|
91265
92475
|
capability: calendarSourceCapability,
|
|
91266
92476
|
provider: calendarProvider
|
|
@@ -91280,6 +92490,35 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
91280
92490
|
this.activeBrokers = await this.loadPersistedBrokers();
|
|
91281
92491
|
this.registry.seedFrom(this.activeBrokers, accounts);
|
|
91282
92492
|
}
|
|
92493
|
+
requireInstances() {
|
|
92494
|
+
if (this.instances === null) throw new Error("google-services: instances used before initialize");
|
|
92495
|
+
return this.instances;
|
|
92496
|
+
}
|
|
92497
|
+
/** The accounts as `integration-instances`, over the same `brokers` records (planning#408 P3). */
|
|
92498
|
+
buildInstances(records) {
|
|
92499
|
+
const brokerProvider = this.buildBrokerProvider();
|
|
92500
|
+
return buildGoogleInstances(records, {
|
|
92501
|
+
connectPath: `/addon/${ADDON_ID}${START_PATH}?brokerId={{brokerId}}`,
|
|
92502
|
+
disconnectPath: `/addon/${ADDON_ID}${DISCONNECT_PATH}?brokerId={{brokerId}}`,
|
|
92503
|
+
publicOrigin: () => this.publicOrigin,
|
|
92504
|
+
mintId: () => this.registry.allocateId(),
|
|
92505
|
+
session: (id) => this.registry.isConnected(id) ? "connected" : this.registry.needsReauth(id) ? "reauth" : "none",
|
|
92506
|
+
onChanged: async () => {
|
|
92507
|
+
const accounts = this.registry.listOauthAccounts();
|
|
92508
|
+
this.activeBrokers = await this.loadPersistedBrokers();
|
|
92509
|
+
this.registry.seedFrom(this.activeBrokers, accounts);
|
|
92510
|
+
await this.persistOauthAccounts();
|
|
92511
|
+
},
|
|
92512
|
+
testSaved: async (id) => brokerProvider.testConnection({ id }),
|
|
92513
|
+
testClient: async (clientId, clientSecret) => brokerProvider.testSettings({
|
|
92514
|
+
kind: GOOGLE_BROKER_KIND,
|
|
92515
|
+
settings: {
|
|
92516
|
+
clientId,
|
|
92517
|
+
clientSecret
|
|
92518
|
+
}
|
|
92519
|
+
})
|
|
92520
|
+
});
|
|
92521
|
+
}
|
|
91283
92522
|
buildBrokerProvider() {
|
|
91284
92523
|
const ownerAddonId = this.ctx.id;
|
|
91285
92524
|
const owns = (id) => this.registry.getEntry(id) !== null;
|
|
@@ -91385,7 +92624,10 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
91385
92624
|
getSettings: async ({ id }) => {
|
|
91386
92625
|
const entry = this.registry.getEntry(id);
|
|
91387
92626
|
if (entry === null) return null;
|
|
91388
|
-
return
|
|
92627
|
+
return {
|
|
92628
|
+
...brokerEntryToSettings(entry),
|
|
92629
|
+
_calendarCatalog: await this.loadStampedCalendarCatalog(id)
|
|
92630
|
+
};
|
|
91389
92631
|
},
|
|
91390
92632
|
setSettings: async ({ id, settings }) => {
|
|
91391
92633
|
if (!owns(id)) return;
|
|
@@ -91398,6 +92640,7 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
91398
92640
|
const oauth = this.registry.getOauth(id);
|
|
91399
92641
|
return {
|
|
91400
92642
|
...brokerEntryToSettings(entry),
|
|
92643
|
+
_calendarCatalog: await this.loadStampedCalendarCatalog(id),
|
|
91401
92644
|
accountEmail: oauth?.accountEmail ?? "",
|
|
91402
92645
|
connected: this.registry.isConnected(id)
|
|
91403
92646
|
};
|
|
@@ -91603,6 +92846,13 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
91603
92846
|
const cutoff = Date.now() - 10 * 6e4;
|
|
91604
92847
|
for (const [k, v] of this.pendingStates) if (v.issuedAt < cutoff) this.pendingStates.delete(k);
|
|
91605
92848
|
}
|
|
92849
|
+
/**
|
|
92850
|
+
* The shared calendar set becomes each broker's own (D985, D987). Brokers are reloaded once the
|
|
92851
|
+
* copy landed, so the registry is seeded with the sets the migration wrote.
|
|
92852
|
+
*/
|
|
92853
|
+
async migrateCalendarIds(records) {
|
|
92854
|
+
if ((await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, calendarIdsPortsOver(records.kind(googleBrokersEntryKind), (key) => this.readStoredGlobalKey(key))))).applied.length > 0) this.activeBrokers = await this.loadPersistedBrokers();
|
|
92855
|
+
}
|
|
91606
92856
|
async loadPersistedBrokers() {
|
|
91607
92857
|
const store = this.brokerStore;
|
|
91608
92858
|
if (store !== null) {
|
|
@@ -91626,9 +92876,10 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
91626
92876
|
await this.oauthAccountsHandle.set({ accounts: [...this.registry.listOauthAccounts()] });
|
|
91627
92877
|
}
|
|
91628
92878
|
/** Stamped catalog for Connectivity → Calendars (`optionsFrom`). */
|
|
91629
|
-
async loadStampedCalendarCatalog() {
|
|
92879
|
+
async loadStampedCalendarCatalog(onlyBrokerId) {
|
|
91630
92880
|
const out = [];
|
|
91631
92881
|
for (const entry of this.registry.listEntries()) {
|
|
92882
|
+
if (onlyBrokerId !== void 0 && entry.id !== onlyBrokerId) continue;
|
|
91632
92883
|
if (!this.registry.isConnected(entry.id)) continue;
|
|
91633
92884
|
try {
|
|
91634
92885
|
const list = await createGoogleCalendarApi(() => this.ensureAccessToken(entry.id)).listCalendars();
|
|
@@ -91768,13 +93019,15 @@ var GoogleServicesAddon = class extends BaseAddon {
|
|
|
91768
93019
|
}
|
|
91769
93020
|
async getGlobalSettings(overlay, cap, nodeId) {
|
|
91770
93021
|
await this.refreshPublicOrigin();
|
|
91771
|
-
|
|
91772
|
-
|
|
91773
|
-
|
|
91774
|
-
|
|
91775
|
-
|
|
91776
|
-
|
|
91777
|
-
|
|
93022
|
+
return super.getGlobalSettings(overlay, cap, nodeId);
|
|
93023
|
+
}
|
|
93024
|
+
/**
|
|
93025
|
+
* The stamped calendar ids ONE broker imports. A broker the migration has not reached yet
|
|
93026
|
+
* (no `calendarIds` of its own) reads the shared setting older versions wrote, so the move
|
|
93027
|
+
* never changes what is imported mid-way.
|
|
93028
|
+
*/
|
|
93029
|
+
calendarIdsOf(brokerId) {
|
|
93030
|
+
return calendarIdsOfEntry(this.registry.getEntry(brokerId), this.config.calendarIds ?? []);
|
|
91778
93031
|
}
|
|
91779
93032
|
};
|
|
91780
93033
|
function asSettingsCap(cap) {
|