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