@camstack/addon-provider-homeassistant 1.2.648 → 1.2.650

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.
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  enumerable: true
25
25
  }) : target, mod));
26
26
  //#endregion
27
- const require_ha_broker_source = require("../ha-broker-source-Ba20mhmb.js");
27
+ const require_ha_broker_source = require("../ha-broker-source-DUj9pUWr.js");
28
28
  let node_crypto = require("node:crypto");
29
29
  require("node:zlib");
30
30
  let node_fs = require("node:fs");
@@ -975,19 +975,22 @@ var NATIVE_CAP_PLATFORMS = {
975
975
  */
976
976
  cover: {
977
977
  platform: "cover",
978
+ availableWhenOnline: true,
978
979
  primary: { from: null },
979
980
  controls: {
980
981
  position: {
981
982
  from: "position",
982
983
  min: 0,
983
984
  max: 100,
984
- step: 1
985
+ step: 1,
986
+ requiresFeature: "cover-positionable"
985
987
  },
986
988
  tilt: {
987
989
  from: "tilt",
988
990
  min: 0,
989
991
  max: 100,
990
- step: 1
992
+ step: 1,
993
+ requiresFeature: "cover-tilt"
991
994
  },
992
995
  open: {
993
996
  from: "open",
@@ -999,7 +1002,8 @@ var NATIVE_CAP_PLATFORMS = {
999
1002
  },
1000
1003
  stop: {
1001
1004
  from: "stop",
1002
- commandOnly: true
1005
+ commandOnly: true,
1006
+ requiresFeature: "cover-stoppable"
1003
1007
  }
1004
1008
  }
1005
1009
  },
@@ -1388,9 +1392,11 @@ function buildNativeComponent(input) {
1388
1392
  if (native === void 0) return null;
1389
1393
  const primary = resolveBinding(input, native.primary);
1390
1394
  if (primary === null) return null;
1395
+ const features = input.features ?? [];
1391
1396
  const absorbed = new Set([derivedEntityId(input.capName), ...primary.entities]);
1392
1397
  const controls = {};
1393
1398
  for (const [name, binding] of Object.entries(native.controls ?? {})) {
1399
+ if (binding.requiresFeature !== void 0 && !features.includes(binding.requiresFeature)) continue;
1394
1400
  const resolved = resolveBinding(input, binding);
1395
1401
  if (resolved === null) continue;
1396
1402
  controls[name] = resolved.control;
@@ -1409,6 +1415,11 @@ function buildNativeComponent(input) {
1409
1415
  ...primary.control,
1410
1416
  ...native.deviceClass !== void 0 ? { device_class: native.deviceClass } : {},
1411
1417
  ...native.icon !== void 0 ? { icon: native.icon } : {},
1418
+ ...native.availableWhenOnline === true ? {
1419
+ availability_topic: stateTopic(input.deviceKey, "online"),
1420
+ payload_available: "true",
1421
+ payload_not_available: "false"
1422
+ } : {},
1412
1423
  ...Object.keys(controls).length > 0 ? { controls } : {},
1413
1424
  ...native.constants,
1414
1425
  ...flags
@@ -2343,7 +2354,8 @@ var CAP_ENTITY_MAP = {
2343
2354
  unit: "%",
2344
2355
  icon: "mdi:arrow-up-down",
2345
2356
  writable: true,
2346
- range: PERCENT_RANGE
2357
+ range: PERCENT_RANGE,
2358
+ requiresFeature: "cover-positionable"
2347
2359
  },
2348
2360
  tilt: {
2349
2361
  platform: "number",
@@ -2353,7 +2365,8 @@ var CAP_ENTITY_MAP = {
2353
2365
  icon: "mdi:angle-acute",
2354
2366
  writable: true,
2355
2367
  range: PERCENT_RANGE,
2356
- enabledByDefault: false
2368
+ enabledByDefault: false,
2369
+ requiresFeature: "cover-tilt"
2357
2370
  }
2358
2371
  },
2359
2372
  commands: {
@@ -2368,7 +2381,8 @@ var CAP_ENTITY_MAP = {
2368
2381
  stop: {
2369
2382
  label: "Stop",
2370
2383
  icon: "mdi:stop",
2371
- enabledByDefault: false
2384
+ enabledByDefault: false,
2385
+ requiresFeature: "cover-stoppable"
2372
2386
  }
2373
2387
  }
2374
2388
  },
@@ -3375,7 +3389,8 @@ function capEntities(capName) {
3375
3389
  for (const [suffix, extra] of Object.entries(mapping.extras ?? {})) refs.push({
3376
3390
  entity: derivedExtraEntityId(capName, suffix),
3377
3391
  label: extra.label,
3378
- descriptor: extra
3392
+ descriptor: extra,
3393
+ ...extra.requiresFeature !== void 0 ? { requiresFeature: extra.requiresFeature } : {}
3379
3394
  });
3380
3395
  return refs;
3381
3396
  }
@@ -3394,8 +3409,9 @@ function deriveCapValue(derivation, raw) {
3394
3409
  return derivation.kind === "equals" ? raw === derivation.value : raw !== derivation.value;
3395
3410
  }
3396
3411
  /** A capability's entities as the catalog builders consume them. */
3397
- function capEntitySpecs(capName) {
3398
- const specs = capEntities(capName).map((ref) => ({
3412
+ function capEntitySpecs(capName, features = []) {
3413
+ const declared = (required) => required === void 0 || features.includes(required);
3414
+ const specs = capEntities(capName).filter((ref) => declared(ref.requiresFeature)).map((ref) => ({
3399
3415
  entity: ref.entity,
3400
3416
  platform: ref.descriptor.platform,
3401
3417
  label: ref.label,
@@ -3408,15 +3424,18 @@ function capEntitySpecs(capName) {
3408
3424
  ...ref.descriptor.entityCategory !== void 0 ? { entityCategory: ref.descriptor.entityCategory } : {},
3409
3425
  ...ref.descriptor.writable === true ? { writable: true } : {}
3410
3426
  }));
3411
- for (const ref of capCommandEntities(capName)) specs.push({
3412
- entity: ref.entity,
3413
- platform: "button",
3414
- label: ref.label,
3415
- enabledByDefault: ref.command.enabledByDefault ?? true,
3416
- writable: true,
3417
- ...ref.command.icon !== void 0 ? { icon: ref.command.icon } : {},
3418
- ...ref.command.entityCategory !== void 0 ? { entityCategory: ref.command.entityCategory } : {}
3419
- });
3427
+ for (const ref of capCommandEntities(capName)) {
3428
+ if (!declared(ref.command.requiresFeature)) continue;
3429
+ specs.push({
3430
+ entity: ref.entity,
3431
+ platform: "button",
3432
+ label: ref.label,
3433
+ enabledByDefault: ref.command.enabledByDefault ?? true,
3434
+ writable: true,
3435
+ ...ref.command.icon !== void 0 ? { icon: ref.command.icon } : {},
3436
+ ...ref.command.entityCategory !== void 0 ? { entityCategory: ref.command.entityCategory } : {}
3437
+ });
3438
+ }
3420
3439
  return specs;
3421
3440
  }
3422
3441
  function readControlOptions(slice) {
@@ -3794,11 +3813,12 @@ function buildDerivedPlan(device, options = UNNEGOTIATED_PLAN_OPTIONS) {
3794
3813
  deviceId: device.id,
3795
3814
  capName: cap,
3796
3815
  label: humanise(cap),
3816
+ features: device.features,
3797
3817
  slice: device.capSlices?.[cap],
3798
3818
  mapping
3799
3819
  });
3800
3820
  if (plan === null) {
3801
- specs.push(...capEntitySpecs(cap).map((spec) => degradeUnsupported(spec, options)));
3821
+ specs.push(...capEntitySpecs(cap, device.features).map((spec) => degradeUnsupported(spec, options)));
3802
3822
  continue;
3803
3823
  }
3804
3824
  native[toComponentKey(plan.component.platform, derivedEntityId(cap))] = plan.component;
@@ -3809,7 +3829,7 @@ function buildDerivedPlan(device, options = UNNEGOTIATED_PLAN_OPTIONS) {
3809
3829
  * to make the device look tidy would be a silent loss of a value an
3810
3830
  * operator already automates on.
3811
3831
  */
3812
- specs.push(...capEntitySpecs(cap).filter((spec) => !plan.absorbed.has(spec.entity)));
3832
+ specs.push(...capEntitySpecs(cap, device.features).filter((spec) => !plan.absorbed.has(spec.entity)));
3813
3833
  }
3814
3834
  const assembled = assemble(device, specs);
3815
3835
  return {
@@ -4671,6 +4691,15 @@ function isCameraCommand(command) {
4671
4691
  function isCapCommand(command) {
4672
4692
  return !isCameraCommand(command);
4673
4693
  }
4694
+ var APPLIED = { ok: true };
4695
+ /** The 422 body of a command that was not applied: the reason travels with it. */
4696
+ function commandNotAppliedBody(refusal) {
4697
+ return {
4698
+ error: "command not applied",
4699
+ reason: refusal.reason,
4700
+ ...refusal.message !== void 0 ? { message: refusal.message } : {}
4701
+ };
4702
+ }
4674
4703
  /**
4675
4704
  * Make the call the command names.
4676
4705
  *
@@ -4950,18 +4979,29 @@ async function call(slice, capName, make) {
4950
4979
  *
4951
4980
  * Idempotent: a store without the key yields an empty plan, which writes nothing.
4952
4981
  */
4953
- /** The settings scope of this addon (`<addonId>:addon-settings`). */
4954
- var HA_EXPORT_SETTINGS_SCOPE = "homeassistant-export:addon-settings";
4982
+ /** The addon id, the prefix of its rows in the canonical settings table. */
4983
+ var HA_EXPORT_ADDON_ID = "homeassistant-export";
4984
+ /**
4985
+ * Where the addon's global settings live: the canonical `addon-settings` table, one row per
4986
+ * key, id `<addonId>.<key>`, data `{ addonId, key, value }` (never a per-addon scoped table,
4987
+ * which exists only when something lazily declared it).
4988
+ */
4989
+ var ADDON_SETTINGS_TABLE = "addon-settings";
4955
4990
  var RETIRED_URL_KEYS = ["publicBaseUrl"];
4956
4991
  async function planDropPublicBaseUrl(ports) {
4957
4992
  const stored = await ports.readStored();
4958
4993
  const present = RETIRED_URL_KEYS.filter((key) => key in stored);
4959
4994
  if (present.length === 0) return { edits: [] };
4995
+ const rowId = (key) => `${HA_EXPORT_ADDON_ID}.${key}`;
4960
4996
  return { edits: [{
4961
4997
  op: "deleteKeys",
4962
- scope: HA_EXPORT_SETTINGS_SCOPE,
4963
- keys: [...present],
4964
- precondition: require_ha_broker_source.preconditionOf(Object.fromEntries(present.map((key) => [key, stored[key]])))
4998
+ scope: ADDON_SETTINGS_TABLE,
4999
+ keys: present.map(rowId),
5000
+ precondition: require_ha_broker_source.preconditionOf(Object.fromEntries(present.map((key) => [rowId(key), {
5001
+ addonId: HA_EXPORT_ADDON_ID,
5002
+ key,
5003
+ value: stored[key]
5004
+ }])))
4965
5005
  }] };
4966
5006
  }
4967
5007
  //#endregion
@@ -9157,9 +9197,23 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
9157
9197
  reply.send({ error: `unroutable command (${resolution.reason})` });
9158
9198
  return;
9159
9199
  }
9160
- const applied = await this.dispatch(resolution.command);
9161
- reply.status(applied ? 200 : 422);
9162
- reply.send(applied ? {} : { error: "command not applied" });
9200
+ const outcome = await this.dispatch(resolution.command);
9201
+ if (outcome.ok) {
9202
+ reply.status(200);
9203
+ reply.send({});
9204
+ return;
9205
+ }
9206
+ this.ctx.logger.warn("ha-export: command answered 422", {
9207
+ tags: { deviceId: exported.deviceId },
9208
+ meta: {
9209
+ topic,
9210
+ entity: parsed.entity,
9211
+ reason: outcome.reason,
9212
+ message: outcome.message
9213
+ }
9214
+ });
9215
+ reply.status(422);
9216
+ reply.send(commandNotAppliedBody(outcome));
9163
9217
  }
9164
9218
  async dispatch(command) {
9165
9219
  try {
@@ -9173,7 +9227,7 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
9173
9227
  */
9174
9228
  if (isCapCommand(command)) {
9175
9229
  const outcome = await applyCapCommand(await this.ctx.fetchDevice(command.deviceId), command);
9176
- if (outcome.ok) return true;
9230
+ if (outcome.ok) return APPLIED;
9177
9231
  if (outcome.reason === "no-provider") return this.noProvider(command.deviceId, outcome.capName);
9178
9232
  this.ctx.logger.warn("ha-export: the device refused the command", {
9179
9233
  tags: { deviceId: command.deviceId },
@@ -9183,7 +9237,11 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
9183
9237
  ...outcome.error !== void 0 ? { error: outcome.error } : {}
9184
9238
  }
9185
9239
  });
9186
- return false;
9240
+ return {
9241
+ ok: false,
9242
+ reason: outcome.reason,
9243
+ ...outcome.error !== void 0 ? { message: outcome.error } : {}
9244
+ };
9187
9245
  }
9188
9246
  switch (command.kind) {
9189
9247
  case "camera-function": {
@@ -9193,7 +9251,11 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
9193
9251
  tags: { deviceId: command.deviceId },
9194
9252
  meta: { switchId: command.switchId }
9195
9253
  });
9196
- return false;
9254
+ return {
9255
+ ok: false,
9256
+ reason: "unknown-function",
9257
+ message: "the switch id is not a camera function"
9258
+ };
9197
9259
  }
9198
9260
  /**
9199
9261
  * A FRESH read for this one device — the pass-level caches are up to
@@ -9203,7 +9265,11 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
9203
9265
  const device = await this.ctx.api.deviceManager.getDevice.query({ deviceId: command.deviceId });
9204
9266
  if (device === null) {
9205
9267
  this.ctx.logger.warn("ha-export: dropping a camera-function command, the device is not in the registry", { tags: { deviceId: command.deviceId } });
9206
- return false;
9268
+ return {
9269
+ ok: false,
9270
+ reason: "device-not-found",
9271
+ message: "the device is not in the registry"
9272
+ };
9207
9273
  }
9208
9274
  const privacySlice = await this.ctx.api.deviceState.getCapSlice.query({
9209
9275
  deviceId: command.deviceId,
@@ -9239,22 +9305,26 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
9239
9305
  tags: { deviceId: command.deviceId },
9240
9306
  meta: { switchId: parsed.data }
9241
9307
  });
9242
- return false;
9308
+ return {
9309
+ ok: false,
9310
+ reason: "function-unavailable",
9311
+ message: "the camera function is not available"
9312
+ };
9243
9313
  }
9244
9314
  await applyCameraFunctionWrite(this.ctx.api, command.deviceId, write, sourceNodeId);
9245
- return true;
9315
+ return APPLIED;
9246
9316
  }
9247
9317
  case "reboot": {
9248
9318
  const device = await this.ctx.fetchDevice(command.deviceId);
9249
9319
  if (device.reboot === void 0) return this.noProvider(command.deviceId, "reboot");
9250
9320
  await device.reboot.reboot({});
9251
- return true;
9321
+ return APPLIED;
9252
9322
  }
9253
9323
  case "ptz-move": {
9254
9324
  const device = await this.ctx.fetchDevice(command.deviceId);
9255
9325
  if (device.ptz === void 0) return this.noProvider(command.deviceId, "ptz");
9256
9326
  await device.ptz.move(PTZ_MOVE_BY_DIRECTION[command.direction] ?? {});
9257
- return true;
9327
+ return APPLIED;
9258
9328
  }
9259
9329
  case "ptz-preset": {
9260
9330
  const device = await this.ctx.fetchDevice(command.deviceId);
@@ -9265,10 +9335,14 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
9265
9335
  tags: { deviceId: command.deviceId },
9266
9336
  meta: { preset: command.preset }
9267
9337
  });
9268
- return false;
9338
+ return {
9339
+ ok: false,
9340
+ reason: "no-such-preset",
9341
+ message: "no preset by that name"
9342
+ };
9269
9343
  }
9270
9344
  await device.ptz.goToPreset({ presetId: preset.id });
9271
- return true;
9345
+ return APPLIED;
9272
9346
  }
9273
9347
  }
9274
9348
  } catch (err) {
@@ -9284,7 +9358,11 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
9284
9358
  error: errMsg(err)
9285
9359
  }
9286
9360
  });
9287
- return false;
9361
+ return {
9362
+ ok: false,
9363
+ reason: "error",
9364
+ message: errMsg(err)
9365
+ };
9288
9366
  }
9289
9367
  }
9290
9368
  /** A cap the device does not provide. Refused, and never silently. */
@@ -9293,7 +9371,11 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
9293
9371
  tags: { deviceId },
9294
9372
  meta: { capName }
9295
9373
  });
9296
- return false;
9374
+ return {
9375
+ ok: false,
9376
+ reason: "no-provider",
9377
+ message: `the device has no provider for ${capName}`
9378
+ };
9297
9379
  }
9298
9380
  /**
9299
9381
  * A public, signed, expiring route Home Assistant can fetch bytes from.