@camstack/addon-provider-homeassistant 1.2.155 → 1.2.156

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.js CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_dist = require("./dist-CERTsNmo.js");
2
+ const require_dist = require("./dist-CUS3XDdU.js");
3
3
  let node_crypto = require("node:crypto");
4
4
  let node_zlib = require("node:zlib");
5
5
  //#region src/ha-admin-link.ts
@@ -7047,6 +7047,11 @@ function createHaNotificationOutputProvider(deps) {
7047
7047
  }
7048
7048
  return out;
7049
7049
  },
7050
+ probeTargetConfig: async ({ kind }) => ({
7051
+ ok: false,
7052
+ resolvedUrl: "",
7053
+ error: `Kind '${kind}' does not support credential probing`
7054
+ }),
7050
7055
  send: async ({ targetId, notification }) => {
7051
7056
  const target = await store.getById(targetId);
7052
7057
  if (!target) return {
package/dist/addon.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { A as connectivityCapability, At as string, B as floodCapability, C as buildAddonRouteProvider, Ct as _enum, D as carbonMonoxideCapability, Dt as number, E as calendarSourceCapability, Et as literal, F as deviceAdoptionCapability, G as lawnMowerControlCapability, H as humidifierCapability, J as motionCapability, K as lockControlCapability, L as enumSensorCapability, M as controlCapability, Mt as unknown, N as coverCapability, Nt as url, O as climateControlCapability, Ot as object, P as deviceAdminLinkCapability, Pt as EventCategory, Q as numericSensorCapability, R as eventEmitterCapability, S as brokerCapability, T as buttonCapability, Tt as discriminatedUnion, U as humiditySensorCapability, V as gasCapability, W as imageCapability, X as notificationOutputCapability, Y as normalizeUnit, Z as notifierCapability, _ as automationControlCapability, _t as DeviceRole, at as scriptRunnerCapability, b as binaryCapability, ct as tamperCapability, d as TargetSchema, dt as vacuumControlCapability, et as parseAdminBase, f as accessoriesCapability, ft as valveCapability, g as ambientLightSensorCapability, gt as DeviceFeature, h as alarmPanelCapability, ht as weatherCapability, it as pressureSensorCapability, j as contactCapability, jt as union, k as colorCapability, kt as record, lt as temperatureSensorCapability, m as airQualitySensorCapability, mt as waterHeaterCapability, n as BaseDevice, nt as prepareNotification, ot as smokeCapability, p as addonRoutesCapability, pt as vibrationCapability, q as mediaPlayerCapability, r as BaseDeviceProvider, rt as presenceCapability, s as EnumSensorDateTimeFormatSchema, st as switchCapability, tt as powerMeterCapability, ut as updateCapability, v as batteryCapability, vt as DeviceType, w as buildDeviceAdminUrl, wt as array, x as brightnessCapability, xt as createEvent, y as bestLocationMatch, yt as errMsg, z as fanControlCapability } from "./dist-DqKiElqb.mjs";
1
+ import { A as connectivityCapability, At as string, B as floodCapability, C as buildAddonRouteProvider, Ct as _enum, D as carbonMonoxideCapability, Dt as number, E as calendarSourceCapability, Et as literal, F as deviceAdoptionCapability, G as lawnMowerControlCapability, H as humidifierCapability, J as motionCapability, K as lockControlCapability, L as enumSensorCapability, M as controlCapability, Mt as unknown, N as coverCapability, Nt as url, O as climateControlCapability, Ot as object, P as deviceAdminLinkCapability, Pt as EventCategory, Q as numericSensorCapability, R as eventEmitterCapability, S as brokerCapability, T as buttonCapability, Tt as discriminatedUnion, U as humiditySensorCapability, V as gasCapability, W as imageCapability, X as notificationOutputCapability, Y as normalizeUnit, Z as notifierCapability, _ as automationControlCapability, _t as DeviceRole, at as scriptRunnerCapability, b as binaryCapability, ct as tamperCapability, d as TargetSchema, dt as vacuumControlCapability, et as parseAdminBase, f as accessoriesCapability, ft as valveCapability, g as ambientLightSensorCapability, gt as DeviceFeature, h as alarmPanelCapability, ht as weatherCapability, it as pressureSensorCapability, j as contactCapability, jt as union, k as colorCapability, kt as record, lt as temperatureSensorCapability, m as airQualitySensorCapability, mt as waterHeaterCapability, n as BaseDevice, nt as prepareNotification, ot as smokeCapability, p as addonRoutesCapability, pt as vibrationCapability, q as mediaPlayerCapability, r as BaseDeviceProvider, rt as presenceCapability, s as EnumSensorDateTimeFormatSchema, st as switchCapability, tt as powerMeterCapability, ut as updateCapability, v as batteryCapability, vt as DeviceType, w as buildDeviceAdminUrl, wt as array, x as brightnessCapability, xt as createEvent, y as bestLocationMatch, yt as errMsg, z as fanControlCapability } from "./dist-CbGUsakM.mjs";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { brotliCompressSync, deflateSync, gzipSync } from "node:zlib";
4
4
  //#region src/ha-admin-link.ts
@@ -7046,6 +7046,11 @@ function createHaNotificationOutputProvider(deps) {
7046
7046
  }
7047
7047
  return out;
7048
7048
  },
7049
+ probeTargetConfig: async ({ kind }) => ({
7050
+ ok: false,
7051
+ resolvedUrl: "",
7052
+ error: `Kind '${kind}' does not support credential probing`
7053
+ }),
7049
7054
  send: async ({ targetId, notification }) => {
7050
7055
  const target = await store.getById(targetId);
7051
7056
  if (!target) return {
@@ -5379,7 +5379,7 @@ var ZodIssueCode = {
5379
5379
  var ZodFirstPartyTypeKind;
5380
5380
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5381
5381
  //#endregion
5382
- //#region ../types/dist/sleep-CDCONfkt.mjs
5382
+ //#region ../types/dist/sleep-DXKLNFvA.mjs
5383
5383
  /**
5384
5384
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5385
5385
  * window to float samples (D455).
@@ -5687,6 +5687,43 @@ function hydrateField(field, values) {
5687
5687
  }
5688
5688
  const rawValue = storedValue !== void 0 ? storedValue : defaultValue !== void 0 ? defaultValue : null;
5689
5689
  const value = field.type === "textarea" && field.isJson && rawValue !== null && typeof rawValue === "object" ? JSON.stringify(rawValue, null, 2) : rawValue;
5690
+ if (field.type === "tags" && typeof field.suggestionsFrom === "string") {
5691
+ const from = values[field.suggestionsFrom];
5692
+ const extra = Array.isArray(from) ? from.filter((s) => typeof s === "string" && s.length > 0) : [];
5693
+ const merged = [...new Set([...field.suggestions ?? [], ...extra])];
5694
+ return {
5695
+ ...field,
5696
+ suggestions: merged,
5697
+ value
5698
+ };
5699
+ }
5700
+ if ((field.type === "select" || field.type === "multiselect") && typeof field.optionsFrom === "string") {
5701
+ const from = values[field.optionsFrom];
5702
+ const byValue = /* @__PURE__ */ new Map();
5703
+ for (const opt of field.options) byValue.set(opt.value, opt);
5704
+ if (Array.isArray(from)) for (const entry of from) {
5705
+ if (typeof entry === "string" && entry.length > 0) {
5706
+ if (!byValue.has(entry)) byValue.set(entry, {
5707
+ value: entry,
5708
+ label: entry
5709
+ });
5710
+ continue;
5711
+ }
5712
+ if (entry === null || typeof entry !== "object") continue;
5713
+ const v = Reflect.get(entry, "value");
5714
+ const lbl = Reflect.get(entry, "label");
5715
+ if (typeof v !== "string" || v.length === 0) continue;
5716
+ if (!byValue.has(v)) byValue.set(v, {
5717
+ value: v,
5718
+ label: typeof lbl === "string" && lbl.length > 0 ? lbl : v
5719
+ });
5720
+ }
5721
+ return {
5722
+ ...field,
5723
+ options: [...byValue.values()],
5724
+ value
5725
+ };
5726
+ }
5690
5727
  return {
5691
5728
  ...field,
5692
5729
  value
@@ -18329,6 +18366,17 @@ var SendResultSchema = object({
18329
18366
  });
18330
18367
  /** Same shape as SendResult — kept as a distinct name for the test panel. */
18331
18368
  var TestResultSchema = SendResultSchema;
18369
+ /**
18370
+ * Pre-save credential probe against a partial (unsaved) target config.
18371
+ * `resolvedUrl` never includes secrets (token stripped from path).
18372
+ */
18373
+ var ProbeTargetConfigResultSchema = object({
18374
+ ok: boolean(),
18375
+ resolvedUrl: string(),
18376
+ httpStatus: number().int().optional(),
18377
+ stateInstance: string().optional(),
18378
+ error: string().optional()
18379
+ });
18332
18380
  var notificationOutputCapability = {
18333
18381
  name: "notification-output",
18334
18382
  scope: "system",
@@ -18336,10 +18384,32 @@ var notificationOutputCapability = {
18336
18384
  methods: {
18337
18385
  listTargetKinds: method(object({}), array(TargetKindSchema)),
18338
18386
  listTargets: method(object({}), array(TargetSchema), { auth: "admin" }),
18387
+ /**
18388
+ * One target WITH secrets — admin edit / duplicate hydration only.
18389
+ * `listTargets` stays redacted; the editor loads this so password fields
18390
+ * are filled (masked in the input) and a save can round-trip the token
18391
+ * when a destination changes (Duplicate with a new chatId, etc.).
18392
+ */
18393
+ getTarget: method(object({ targetId: string() }), TargetSchema, { auth: "admin" }),
18339
18394
  discoverTargets: method(object({
18340
18395
  kind: string(),
18341
- config: record(string(), unknown()).optional()
18396
+ config: record(string(), unknown()).optional(),
18397
+ /**
18398
+ * When editing / duplicating, the blank password field never round-trips
18399
+ * the stored token. Pass the source target id so the provider can restore
18400
+ * secrets server-side before dialling (same merge as upsert).
18401
+ */
18402
+ targetId: string().optional()
18342
18403
  }), array(DiscoveredTargetSchema), { auth: "admin" }),
18404
+ probeTargetConfig: method(object({
18405
+ kind: string(),
18406
+ config: record(string(), unknown()),
18407
+ /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
18408
+ targetId: string().optional()
18409
+ }), ProbeTargetConfigResultSchema, {
18410
+ kind: "mutation",
18411
+ auth: "admin"
18412
+ }),
18343
18413
  send: method(object({
18344
18414
  targetId: string(),
18345
18415
  notification: NotificationSchema
@@ -18354,7 +18424,16 @@ var notificationOutputCapability = {
18354
18424
  kind: "mutation",
18355
18425
  auth: "admin"
18356
18426
  }),
18357
- upsertTarget: method(object({ target: TargetSchema }), TargetSchema, {
18427
+ upsertTarget: method(object({
18428
+ target: TargetSchema,
18429
+ /**
18430
+ * Duplicate flow: create a NEW target id while copying secrets from an
18431
+ * existing row. The UI never holds the stored token; blank / redacted
18432
+ * password fields are restored from this source (D634 still applies if
18433
+ * destinations diverge without a re-typed secret).
18434
+ */
18435
+ cloneSecretsFrom: string().optional()
18436
+ }), TargetSchema, {
18358
18437
  kind: "mutation",
18359
18438
  auth: "admin"
18360
18439
  }),
@@ -44767,6 +44846,12 @@ Object.freeze({
44767
44846
  addonId: null,
44768
44847
  access: "view"
44769
44848
  },
44849
+ "notificationOutput.getTarget": {
44850
+ capName: "notification-output",
44851
+ capScope: "system",
44852
+ addonId: null,
44853
+ access: "view"
44854
+ },
44770
44855
  "notificationOutput.listTargetKinds": {
44771
44856
  capName: "notification-output",
44772
44857
  capScope: "system",
@@ -44779,6 +44864,12 @@ Object.freeze({
44779
44864
  addonId: null,
44780
44865
  access: "view"
44781
44866
  },
44867
+ "notificationOutput.probeTargetConfig": {
44868
+ capName: "notification-output",
44869
+ capScope: "system",
44870
+ addonId: null,
44871
+ access: "create"
44872
+ },
44782
44873
  "notificationOutput.send": {
44783
44874
  capName: "notification-output",
44784
44875
  capScope: "system",
@@ -5378,7 +5378,7 @@ var ZodIssueCode = {
5378
5378
  var ZodFirstPartyTypeKind;
5379
5379
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5380
5380
  //#endregion
5381
- //#region ../types/dist/sleep-CDCONfkt.mjs
5381
+ //#region ../types/dist/sleep-DXKLNFvA.mjs
5382
5382
  /**
5383
5383
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5384
5384
  * window to float samples (D455).
@@ -5686,6 +5686,43 @@ function hydrateField(field, values) {
5686
5686
  }
5687
5687
  const rawValue = storedValue !== void 0 ? storedValue : defaultValue !== void 0 ? defaultValue : null;
5688
5688
  const value = field.type === "textarea" && field.isJson && rawValue !== null && typeof rawValue === "object" ? JSON.stringify(rawValue, null, 2) : rawValue;
5689
+ if (field.type === "tags" && typeof field.suggestionsFrom === "string") {
5690
+ const from = values[field.suggestionsFrom];
5691
+ const extra = Array.isArray(from) ? from.filter((s) => typeof s === "string" && s.length > 0) : [];
5692
+ const merged = [...new Set([...field.suggestions ?? [], ...extra])];
5693
+ return {
5694
+ ...field,
5695
+ suggestions: merged,
5696
+ value
5697
+ };
5698
+ }
5699
+ if ((field.type === "select" || field.type === "multiselect") && typeof field.optionsFrom === "string") {
5700
+ const from = values[field.optionsFrom];
5701
+ const byValue = /* @__PURE__ */ new Map();
5702
+ for (const opt of field.options) byValue.set(opt.value, opt);
5703
+ if (Array.isArray(from)) for (const entry of from) {
5704
+ if (typeof entry === "string" && entry.length > 0) {
5705
+ if (!byValue.has(entry)) byValue.set(entry, {
5706
+ value: entry,
5707
+ label: entry
5708
+ });
5709
+ continue;
5710
+ }
5711
+ if (entry === null || typeof entry !== "object") continue;
5712
+ const v = Reflect.get(entry, "value");
5713
+ const lbl = Reflect.get(entry, "label");
5714
+ if (typeof v !== "string" || v.length === 0) continue;
5715
+ if (!byValue.has(v)) byValue.set(v, {
5716
+ value: v,
5717
+ label: typeof lbl === "string" && lbl.length > 0 ? lbl : v
5718
+ });
5719
+ }
5720
+ return {
5721
+ ...field,
5722
+ options: [...byValue.values()],
5723
+ value
5724
+ };
5725
+ }
5689
5726
  return {
5690
5727
  ...field,
5691
5728
  value
@@ -18328,6 +18365,17 @@ var SendResultSchema = object({
18328
18365
  });
18329
18366
  /** Same shape as SendResult — kept as a distinct name for the test panel. */
18330
18367
  var TestResultSchema = SendResultSchema;
18368
+ /**
18369
+ * Pre-save credential probe against a partial (unsaved) target config.
18370
+ * `resolvedUrl` never includes secrets (token stripped from path).
18371
+ */
18372
+ var ProbeTargetConfigResultSchema = object({
18373
+ ok: boolean(),
18374
+ resolvedUrl: string(),
18375
+ httpStatus: number().int().optional(),
18376
+ stateInstance: string().optional(),
18377
+ error: string().optional()
18378
+ });
18331
18379
  var notificationOutputCapability = {
18332
18380
  name: "notification-output",
18333
18381
  scope: "system",
@@ -18335,10 +18383,32 @@ var notificationOutputCapability = {
18335
18383
  methods: {
18336
18384
  listTargetKinds: method(object({}), array(TargetKindSchema)),
18337
18385
  listTargets: method(object({}), array(TargetSchema), { auth: "admin" }),
18386
+ /**
18387
+ * One target WITH secrets — admin edit / duplicate hydration only.
18388
+ * `listTargets` stays redacted; the editor loads this so password fields
18389
+ * are filled (masked in the input) and a save can round-trip the token
18390
+ * when a destination changes (Duplicate with a new chatId, etc.).
18391
+ */
18392
+ getTarget: method(object({ targetId: string() }), TargetSchema, { auth: "admin" }),
18338
18393
  discoverTargets: method(object({
18339
18394
  kind: string(),
18340
- config: record(string(), unknown()).optional()
18395
+ config: record(string(), unknown()).optional(),
18396
+ /**
18397
+ * When editing / duplicating, the blank password field never round-trips
18398
+ * the stored token. Pass the source target id so the provider can restore
18399
+ * secrets server-side before dialling (same merge as upsert).
18400
+ */
18401
+ targetId: string().optional()
18341
18402
  }), array(DiscoveredTargetSchema), { auth: "admin" }),
18403
+ probeTargetConfig: method(object({
18404
+ kind: string(),
18405
+ config: record(string(), unknown()),
18406
+ /** See `discoverTargets.targetId` — restore stored secrets for a blank password. */
18407
+ targetId: string().optional()
18408
+ }), ProbeTargetConfigResultSchema, {
18409
+ kind: "mutation",
18410
+ auth: "admin"
18411
+ }),
18342
18412
  send: method(object({
18343
18413
  targetId: string(),
18344
18414
  notification: NotificationSchema
@@ -18353,7 +18423,16 @@ var notificationOutputCapability = {
18353
18423
  kind: "mutation",
18354
18424
  auth: "admin"
18355
18425
  }),
18356
- upsertTarget: method(object({ target: TargetSchema }), TargetSchema, {
18426
+ upsertTarget: method(object({
18427
+ target: TargetSchema,
18428
+ /**
18429
+ * Duplicate flow: create a NEW target id while copying secrets from an
18430
+ * existing row. The UI never holds the stored token; blank / redacted
18431
+ * password fields are restored from this source (D634 still applies if
18432
+ * destinations diverge without a re-typed secret).
18433
+ */
18434
+ cloneSecretsFrom: string().optional()
18435
+ }), TargetSchema, {
18357
18436
  kind: "mutation",
18358
18437
  auth: "admin"
18359
18438
  }),
@@ -44766,6 +44845,12 @@ Object.freeze({
44766
44845
  addonId: null,
44767
44846
  access: "view"
44768
44847
  },
44848
+ "notificationOutput.getTarget": {
44849
+ capName: "notification-output",
44850
+ capScope: "system",
44851
+ addonId: null,
44852
+ access: "view"
44853
+ },
44769
44854
  "notificationOutput.listTargetKinds": {
44770
44855
  capName: "notification-output",
44771
44856
  capScope: "system",
@@ -44778,6 +44863,12 @@ Object.freeze({
44778
44863
  addonId: null,
44779
44864
  access: "view"
44780
44865
  },
44866
+ "notificationOutput.probeTargetConfig": {
44867
+ capName: "notification-output",
44868
+ capScope: "system",
44869
+ addonId: null,
44870
+ access: "create"
44871
+ },
44781
44872
  "notificationOutput.send": {
44782
44873
  capName: "notification-output",
44783
44874
  capScope: "system",
@@ -3,7 +3,7 @@ Object.defineProperties(exports, {
3
3
  [Symbol.toStringTag]: { value: "Module" }
4
4
  });
5
5
  //#endregion
6
- const require_dist = require("../dist-CERTsNmo.js");
6
+ const require_dist = require("../dist-CUS3XDdU.js");
7
7
  let node_crypto = require("node:crypto");
8
8
  //#region src/ha-export/topics.ts
9
9
  /**
@@ -2582,13 +2582,18 @@ mireds: {
2582
2582
  },
2583
2583
  /**
2584
2584
  * The generic user-settable input. `value` is `number | string` and
2585
- * `kind` says which of the four shapes it is — a `number`, a `select`
2586
- * or a text box, all three of which would need `control.setValue` in
2587
- * the route table before they could be anything but a reading.
2585
+ * `kind` on the slice says which of the four shapes it is. The row
2586
+ * here is the PROJECTOR + route authority (`writable` gates
2587
+ * `control.setValue`); the announced platform (`select` / `number` /
2588
+ * read-only `sensor`) is minted at reconcile from the slice by
2589
+ * {@link controlEntitySpecs} — a static `select` here cannot carry
2590
+ * the device's live `options`, and a static `number` cannot carry
2591
+ * its live min/max/step.
2588
2592
  */
2589
2593
  control: {
2590
2594
  platform: "sensor",
2591
2595
  field: "value",
2596
+ writable: true,
2592
2597
  extras: { kind: {
2593
2598
  platform: "sensor",
2594
2599
  field: "kind",
@@ -2755,6 +2760,71 @@ function capEntitySpecs(capName) {
2755
2760
  });
2756
2761
  return specs;
2757
2762
  }
2763
+ function readControlOptions(slice) {
2764
+ const raw = slice?.["options"];
2765
+ if (!Array.isArray(raw)) return [];
2766
+ return raw.filter((entry) => typeof entry === "string" && entry.length > 0);
2767
+ }
2768
+ function readFiniteNumber(slice, field) {
2769
+ const raw = slice?.[field];
2770
+ return typeof raw === "number" && Number.isFinite(raw) ? raw : void 0;
2771
+ }
2772
+ /**
2773
+ * The announced shape of a `control` device, from its live slice.
2774
+ *
2775
+ * `CAP_ENTITY_MAP.control` cannot name a static `select`/`number`: the
2776
+ * options and the bounds live on the slice and change (VPN station list,
2777
+ * HA `input_number` limits). A select with no options is an entity HA
2778
+ * builds with nothing to choose from — so an empty list falls back to a
2779
+ * read-only sensor rather than a dead control.
2780
+ */
2781
+ function controlEntitySpecs(slice) {
2782
+ const kind = typeof slice?.["kind"] === "string" ? slice["kind"] : null;
2783
+ const primary = derivedEntityId("control");
2784
+ const kindExtra = {
2785
+ entity: derivedExtraEntityId("control", "kind"),
2786
+ platform: "sensor",
2787
+ label: "Input kind",
2788
+ entityCategory: "diagnostic",
2789
+ enabledByDefault: false
2790
+ };
2791
+ if (kind === "select") {
2792
+ const options = readControlOptions(slice);
2793
+ if (options.length > 0) return [{
2794
+ entity: primary,
2795
+ platform: "select",
2796
+ label: humanise("control"),
2797
+ writable: true,
2798
+ options,
2799
+ enabledByDefault: true
2800
+ }, kindExtra];
2801
+ }
2802
+ if (kind === "numeric") {
2803
+ const min = readFiniteNumber(slice, "min") ?? PERCENT_RANGE.min;
2804
+ const max = readFiniteNumber(slice, "max") ?? PERCENT_RANGE.max;
2805
+ const step = readFiniteNumber(slice, "step") ?? PERCENT_RANGE.step;
2806
+ const unit = typeof slice?.["unit"] === "string" ? slice["unit"] : void 0;
2807
+ if (min < max && step > 0) return [{
2808
+ entity: primary,
2809
+ platform: "number",
2810
+ label: humanise("control"),
2811
+ writable: true,
2812
+ range: {
2813
+ min,
2814
+ max,
2815
+ step
2816
+ },
2817
+ ...unit !== void 0 ? { unit } : {},
2818
+ enabledByDefault: true
2819
+ }, kindExtra];
2820
+ }
2821
+ return [{
2822
+ entity: primary,
2823
+ platform: "sensor",
2824
+ label: humanise("control"),
2825
+ enabledByDefault: true
2826
+ }, kindExtra];
2827
+ }
2758
2828
  var CAPS_NOT_EXPORTED = [
2759
2829
  {
2760
2830
  cap: "recording-onboard",
@@ -2984,6 +3054,15 @@ function buildDerivedPlan(device, options = UNNEGOTIATED_PLAN_OPTIONS) {
2984
3054
  * two halves of this file would build the same entity id twice.
2985
3055
  */
2986
3056
  if (CAMERA_DERIVED_CAPS.includes(cap)) continue;
3057
+ /**
3058
+ * `control` is slice-shaped: select options and number bounds are not
3059
+ * on the static row. Mint the announced platform here; the CAP_ENTITY_MAP
3060
+ * row stays the projector + writable-route authority.
3061
+ */
3062
+ if (cap === "control") {
3063
+ specs.push(...controlEntitySpecs(device.capSlices?.["control"]).map((spec) => degradeUnsupported(spec, options)));
3064
+ continue;
3065
+ }
2987
3066
  const mapping = CAP_ENTITY_MAP[cap];
2988
3067
  const platform = nativePlatformFor(cap);
2989
3068
  const plan = mapping === void 0 || platform === null || !options.platforms.has(platform) ? null : buildNativeComponent({
@@ -3207,6 +3286,11 @@ var CAP_COMMAND_ROUTES = {
3207
3286
  deviceId,
3208
3287
  capName: "button"
3209
3288
  }) },
3289
+ control: { primary: (deviceId, value) => value.trim().length === 0 ? null : {
3290
+ kind: "cap-control",
3291
+ deviceId,
3292
+ value
3293
+ } },
3210
3294
  cover: {
3211
3295
  extras: {
3212
3296
  position: (deviceId, value) => {
@@ -3895,6 +3979,28 @@ async function applyCapCommand(device, command) {
3895
3979
  };
3896
3980
  await device.button.press({});
3897
3981
  return { ok: true };
3982
+ case "cap-control": return call(device.control, "control", async (control) => {
3983
+ const status = await control.getStatus({});
3984
+ if (status === null || typeof status !== "object") throw new Error("the device reports no control status, so setValue has no kind");
3985
+ const kind = Reflect.get(status, "kind");
3986
+ if (kind === "numeric") {
3987
+ const numeric = Number(command.value);
3988
+ if (!Number.isFinite(numeric)) throw new Error(`control value is not a number: ${command.value}`);
3989
+ await control.setValue({ control: {
3990
+ kind: "numeric",
3991
+ value: numeric
3992
+ } });
3993
+ return;
3994
+ }
3995
+ if (kind === "select" || kind === "text" || kind === "datetime") {
3996
+ await control.setValue({ control: {
3997
+ kind,
3998
+ value: command.value
3999
+ } });
4000
+ return;
4001
+ }
4002
+ throw new Error(`unsupported control kind: ${String(kind)}`);
4003
+ });
3898
4004
  case "cover-position": return call(device.cover, "cover", (cover) => cover.setPosition({ position: command.position }));
3899
4005
  case "cover-tilt": return call(device.cover, "cover", (cover) => cover.setTiltPosition({ tiltPosition: command.tiltPosition }));
3900
4006
  case "cover-verb": return call(device.cover, "cover", (cover) => {
@@ -1,4 +1,4 @@
1
- import { $ as oauthIntegrationCapability, At as string, C as buildAddonRouteProvider, I as deviceExportCapability, Pt as EventCategory, St as nodePin, a as COCO_TO_MACRO, bt as BaseAddon, c as FanDirectionSchema, i as CAMERA_SWITCH_ORDER, l as HvacModeSchema, o as CameraSwitchIdSchema, p as addonRoutesCapability, t as AlarmArmModeSchema, u as MediaPlayerRepeatSchema } from "../dist-DqKiElqb.mjs";
1
+ import { $ as oauthIntegrationCapability, At as string, C as buildAddonRouteProvider, I as deviceExportCapability, Pt as EventCategory, St as nodePin, a as COCO_TO_MACRO, bt as BaseAddon, c as FanDirectionSchema, i as CAMERA_SWITCH_ORDER, l as HvacModeSchema, o as CameraSwitchIdSchema, p as addonRoutesCapability, t as AlarmArmModeSchema, u as MediaPlayerRepeatSchema } from "../dist-CbGUsakM.mjs";
2
2
  import { createHmac, timingSafeEqual } from "node:crypto";
3
3
  //#region src/ha-export/topics.ts
4
4
  /**
@@ -2577,13 +2577,18 @@ mireds: {
2577
2577
  },
2578
2578
  /**
2579
2579
  * The generic user-settable input. `value` is `number | string` and
2580
- * `kind` says which of the four shapes it is — a `number`, a `select`
2581
- * or a text box, all three of which would need `control.setValue` in
2582
- * the route table before they could be anything but a reading.
2580
+ * `kind` on the slice says which of the four shapes it is. The row
2581
+ * here is the PROJECTOR + route authority (`writable` gates
2582
+ * `control.setValue`); the announced platform (`select` / `number` /
2583
+ * read-only `sensor`) is minted at reconcile from the slice by
2584
+ * {@link controlEntitySpecs} — a static `select` here cannot carry
2585
+ * the device's live `options`, and a static `number` cannot carry
2586
+ * its live min/max/step.
2583
2587
  */
2584
2588
  control: {
2585
2589
  platform: "sensor",
2586
2590
  field: "value",
2591
+ writable: true,
2587
2592
  extras: { kind: {
2588
2593
  platform: "sensor",
2589
2594
  field: "kind",
@@ -2750,6 +2755,71 @@ function capEntitySpecs(capName) {
2750
2755
  });
2751
2756
  return specs;
2752
2757
  }
2758
+ function readControlOptions(slice) {
2759
+ const raw = slice?.["options"];
2760
+ if (!Array.isArray(raw)) return [];
2761
+ return raw.filter((entry) => typeof entry === "string" && entry.length > 0);
2762
+ }
2763
+ function readFiniteNumber(slice, field) {
2764
+ const raw = slice?.[field];
2765
+ return typeof raw === "number" && Number.isFinite(raw) ? raw : void 0;
2766
+ }
2767
+ /**
2768
+ * The announced shape of a `control` device, from its live slice.
2769
+ *
2770
+ * `CAP_ENTITY_MAP.control` cannot name a static `select`/`number`: the
2771
+ * options and the bounds live on the slice and change (VPN station list,
2772
+ * HA `input_number` limits). A select with no options is an entity HA
2773
+ * builds with nothing to choose from — so an empty list falls back to a
2774
+ * read-only sensor rather than a dead control.
2775
+ */
2776
+ function controlEntitySpecs(slice) {
2777
+ const kind = typeof slice?.["kind"] === "string" ? slice["kind"] : null;
2778
+ const primary = derivedEntityId("control");
2779
+ const kindExtra = {
2780
+ entity: derivedExtraEntityId("control", "kind"),
2781
+ platform: "sensor",
2782
+ label: "Input kind",
2783
+ entityCategory: "diagnostic",
2784
+ enabledByDefault: false
2785
+ };
2786
+ if (kind === "select") {
2787
+ const options = readControlOptions(slice);
2788
+ if (options.length > 0) return [{
2789
+ entity: primary,
2790
+ platform: "select",
2791
+ label: humanise("control"),
2792
+ writable: true,
2793
+ options,
2794
+ enabledByDefault: true
2795
+ }, kindExtra];
2796
+ }
2797
+ if (kind === "numeric") {
2798
+ const min = readFiniteNumber(slice, "min") ?? PERCENT_RANGE.min;
2799
+ const max = readFiniteNumber(slice, "max") ?? PERCENT_RANGE.max;
2800
+ const step = readFiniteNumber(slice, "step") ?? PERCENT_RANGE.step;
2801
+ const unit = typeof slice?.["unit"] === "string" ? slice["unit"] : void 0;
2802
+ if (min < max && step > 0) return [{
2803
+ entity: primary,
2804
+ platform: "number",
2805
+ label: humanise("control"),
2806
+ writable: true,
2807
+ range: {
2808
+ min,
2809
+ max,
2810
+ step
2811
+ },
2812
+ ...unit !== void 0 ? { unit } : {},
2813
+ enabledByDefault: true
2814
+ }, kindExtra];
2815
+ }
2816
+ return [{
2817
+ entity: primary,
2818
+ platform: "sensor",
2819
+ label: humanise("control"),
2820
+ enabledByDefault: true
2821
+ }, kindExtra];
2822
+ }
2753
2823
  var CAPS_NOT_EXPORTED = [
2754
2824
  {
2755
2825
  cap: "recording-onboard",
@@ -2979,6 +3049,15 @@ function buildDerivedPlan(device, options = UNNEGOTIATED_PLAN_OPTIONS) {
2979
3049
  * two halves of this file would build the same entity id twice.
2980
3050
  */
2981
3051
  if (CAMERA_DERIVED_CAPS.includes(cap)) continue;
3052
+ /**
3053
+ * `control` is slice-shaped: select options and number bounds are not
3054
+ * on the static row. Mint the announced platform here; the CAP_ENTITY_MAP
3055
+ * row stays the projector + writable-route authority.
3056
+ */
3057
+ if (cap === "control") {
3058
+ specs.push(...controlEntitySpecs(device.capSlices?.["control"]).map((spec) => degradeUnsupported(spec, options)));
3059
+ continue;
3060
+ }
2982
3061
  const mapping = CAP_ENTITY_MAP[cap];
2983
3062
  const platform = nativePlatformFor(cap);
2984
3063
  const plan = mapping === void 0 || platform === null || !options.platforms.has(platform) ? null : buildNativeComponent({
@@ -3202,6 +3281,11 @@ var CAP_COMMAND_ROUTES = {
3202
3281
  deviceId,
3203
3282
  capName: "button"
3204
3283
  }) },
3284
+ control: { primary: (deviceId, value) => value.trim().length === 0 ? null : {
3285
+ kind: "cap-control",
3286
+ deviceId,
3287
+ value
3288
+ } },
3205
3289
  cover: {
3206
3290
  extras: {
3207
3291
  position: (deviceId, value) => {
@@ -3890,6 +3974,28 @@ async function applyCapCommand(device, command) {
3890
3974
  };
3891
3975
  await device.button.press({});
3892
3976
  return { ok: true };
3977
+ case "cap-control": return call(device.control, "control", async (control) => {
3978
+ const status = await control.getStatus({});
3979
+ if (status === null || typeof status !== "object") throw new Error("the device reports no control status, so setValue has no kind");
3980
+ const kind = Reflect.get(status, "kind");
3981
+ if (kind === "numeric") {
3982
+ const numeric = Number(command.value);
3983
+ if (!Number.isFinite(numeric)) throw new Error(`control value is not a number: ${command.value}`);
3984
+ await control.setValue({ control: {
3985
+ kind: "numeric",
3986
+ value: numeric
3987
+ } });
3988
+ return;
3989
+ }
3990
+ if (kind === "select" || kind === "text" || kind === "datetime") {
3991
+ await control.setValue({ control: {
3992
+ kind,
3993
+ value: command.value
3994
+ } });
3995
+ return;
3996
+ }
3997
+ throw new Error(`unsupported control kind: ${String(kind)}`);
3998
+ });
3893
3999
  case "cover-position": return call(device.cover, "cover", (cover) => cover.setPosition({ position: command.position }));
3894
4000
  case "cover-tilt": return call(device.cover, "cover", (cover) => cover.setTiltPosition({ tiltPosition: command.tiltPosition }));
3895
4001
  case "cover-verb": return call(device.cover, "cover", (cover) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-homeassistant",
3
- "version": "1.2.155",
3
+ "version": "1.2.156",
4
4
  "description": "Home Assistant device provider addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",