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