@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.
@@ -1,4 +1,4 @@
1
- import { B as consumableKindOf, Bt as BaseAddon, Dt as sendToExportedTarget, Ht as baseUnitOf, Jt as isReadingQuantity, K as deviceExportCapability, Wt as defineAddonMigrations, Yt as nodePin, Zt as array, _ as PLUG_APPLIANCE_TYPES, _t as pruneDeletedEntities, c as COCO_TO_MACRO, d as ConsumableItemSchema, f as ConsumableStatusSchema, fn as EventCategory, g as MediaPlayerRepeatSchema, h as HvacModeSchema, i as AlarmArmModeSchema, in as string, j as buildAddonRouteProvider, l as CameraSwitchIdSchema, m as FanDirectionSchema, nn as object, on as unknown, pt as preconditionOf, r as listHomeAssistantBrokers, s as CAMERA_SWITCH_ORDER, u as ConsumableFormSchema, ut as oauthIntegrationCapability, x as addonRoutesCapability, xt as runExportedAction, z as consumableFormOf } from "../ha-broker-source-iLA_DsW8.mjs";
1
+ import { B as consumableKindOf, Bt as BaseAddon, Dt as sendToExportedTarget, Ht as baseUnitOf, Jt as isReadingQuantity, K as deviceExportCapability, Wt as defineAddonMigrations, Yt as nodePin, Zt as array, _ as PLUG_APPLIANCE_TYPES, _t as pruneDeletedEntities, c as COCO_TO_MACRO, d as ConsumableItemSchema, f as ConsumableStatusSchema, fn as EventCategory, g as MediaPlayerRepeatSchema, h as HvacModeSchema, i as AlarmArmModeSchema, in as string, j as buildAddonRouteProvider, l as CameraSwitchIdSchema, m as FanDirectionSchema, nn as object, on as unknown, pt as preconditionOf, r as listHomeAssistantBrokers, s as CAMERA_SWITCH_ORDER, u as ConsumableFormSchema, ut as oauthIntegrationCapability, x as addonRoutesCapability, xt as runExportedAction, z as consumableFormOf } from "../ha-broker-source-B2v2dnPe.mjs";
2
2
  import { createHmac, timingSafeEqual } from "node:crypto";
3
3
  import "node:zlib";
4
4
  import "node:fs";
@@ -940,19 +940,22 @@ var NATIVE_CAP_PLATFORMS = {
940
940
  */
941
941
  cover: {
942
942
  platform: "cover",
943
+ availableWhenOnline: true,
943
944
  primary: { from: null },
944
945
  controls: {
945
946
  position: {
946
947
  from: "position",
947
948
  min: 0,
948
949
  max: 100,
949
- step: 1
950
+ step: 1,
951
+ requiresFeature: "cover-positionable"
950
952
  },
951
953
  tilt: {
952
954
  from: "tilt",
953
955
  min: 0,
954
956
  max: 100,
955
- step: 1
957
+ step: 1,
958
+ requiresFeature: "cover-tilt"
956
959
  },
957
960
  open: {
958
961
  from: "open",
@@ -964,7 +967,8 @@ var NATIVE_CAP_PLATFORMS = {
964
967
  },
965
968
  stop: {
966
969
  from: "stop",
967
- commandOnly: true
970
+ commandOnly: true,
971
+ requiresFeature: "cover-stoppable"
968
972
  }
969
973
  }
970
974
  },
@@ -1353,9 +1357,11 @@ function buildNativeComponent(input) {
1353
1357
  if (native === void 0) return null;
1354
1358
  const primary = resolveBinding(input, native.primary);
1355
1359
  if (primary === null) return null;
1360
+ const features = input.features ?? [];
1356
1361
  const absorbed = new Set([derivedEntityId(input.capName), ...primary.entities]);
1357
1362
  const controls = {};
1358
1363
  for (const [name, binding] of Object.entries(native.controls ?? {})) {
1364
+ if (binding.requiresFeature !== void 0 && !features.includes(binding.requiresFeature)) continue;
1359
1365
  const resolved = resolveBinding(input, binding);
1360
1366
  if (resolved === null) continue;
1361
1367
  controls[name] = resolved.control;
@@ -1374,6 +1380,11 @@ function buildNativeComponent(input) {
1374
1380
  ...primary.control,
1375
1381
  ...native.deviceClass !== void 0 ? { device_class: native.deviceClass } : {},
1376
1382
  ...native.icon !== void 0 ? { icon: native.icon } : {},
1383
+ ...native.availableWhenOnline === true ? {
1384
+ availability_topic: stateTopic(input.deviceKey, "online"),
1385
+ payload_available: "true",
1386
+ payload_not_available: "false"
1387
+ } : {},
1377
1388
  ...Object.keys(controls).length > 0 ? { controls } : {},
1378
1389
  ...native.constants,
1379
1390
  ...flags
@@ -2308,7 +2319,8 @@ var CAP_ENTITY_MAP = {
2308
2319
  unit: "%",
2309
2320
  icon: "mdi:arrow-up-down",
2310
2321
  writable: true,
2311
- range: PERCENT_RANGE
2322
+ range: PERCENT_RANGE,
2323
+ requiresFeature: "cover-positionable"
2312
2324
  },
2313
2325
  tilt: {
2314
2326
  platform: "number",
@@ -2318,7 +2330,8 @@ var CAP_ENTITY_MAP = {
2318
2330
  icon: "mdi:angle-acute",
2319
2331
  writable: true,
2320
2332
  range: PERCENT_RANGE,
2321
- enabledByDefault: false
2333
+ enabledByDefault: false,
2334
+ requiresFeature: "cover-tilt"
2322
2335
  }
2323
2336
  },
2324
2337
  commands: {
@@ -2333,7 +2346,8 @@ var CAP_ENTITY_MAP = {
2333
2346
  stop: {
2334
2347
  label: "Stop",
2335
2348
  icon: "mdi:stop",
2336
- enabledByDefault: false
2349
+ enabledByDefault: false,
2350
+ requiresFeature: "cover-stoppable"
2337
2351
  }
2338
2352
  }
2339
2353
  },
@@ -3340,7 +3354,8 @@ function capEntities(capName) {
3340
3354
  for (const [suffix, extra] of Object.entries(mapping.extras ?? {})) refs.push({
3341
3355
  entity: derivedExtraEntityId(capName, suffix),
3342
3356
  label: extra.label,
3343
- descriptor: extra
3357
+ descriptor: extra,
3358
+ ...extra.requiresFeature !== void 0 ? { requiresFeature: extra.requiresFeature } : {}
3344
3359
  });
3345
3360
  return refs;
3346
3361
  }
@@ -3359,8 +3374,9 @@ function deriveCapValue(derivation, raw) {
3359
3374
  return derivation.kind === "equals" ? raw === derivation.value : raw !== derivation.value;
3360
3375
  }
3361
3376
  /** A capability's entities as the catalog builders consume them. */
3362
- function capEntitySpecs(capName) {
3363
- const specs = capEntities(capName).map((ref) => ({
3377
+ function capEntitySpecs(capName, features = []) {
3378
+ const declared = (required) => required === void 0 || features.includes(required);
3379
+ const specs = capEntities(capName).filter((ref) => declared(ref.requiresFeature)).map((ref) => ({
3364
3380
  entity: ref.entity,
3365
3381
  platform: ref.descriptor.platform,
3366
3382
  label: ref.label,
@@ -3373,15 +3389,18 @@ function capEntitySpecs(capName) {
3373
3389
  ...ref.descriptor.entityCategory !== void 0 ? { entityCategory: ref.descriptor.entityCategory } : {},
3374
3390
  ...ref.descriptor.writable === true ? { writable: true } : {}
3375
3391
  }));
3376
- for (const ref of capCommandEntities(capName)) specs.push({
3377
- entity: ref.entity,
3378
- platform: "button",
3379
- label: ref.label,
3380
- enabledByDefault: ref.command.enabledByDefault ?? true,
3381
- writable: true,
3382
- ...ref.command.icon !== void 0 ? { icon: ref.command.icon } : {},
3383
- ...ref.command.entityCategory !== void 0 ? { entityCategory: ref.command.entityCategory } : {}
3384
- });
3392
+ for (const ref of capCommandEntities(capName)) {
3393
+ if (!declared(ref.command.requiresFeature)) continue;
3394
+ specs.push({
3395
+ entity: ref.entity,
3396
+ platform: "button",
3397
+ label: ref.label,
3398
+ enabledByDefault: ref.command.enabledByDefault ?? true,
3399
+ writable: true,
3400
+ ...ref.command.icon !== void 0 ? { icon: ref.command.icon } : {},
3401
+ ...ref.command.entityCategory !== void 0 ? { entityCategory: ref.command.entityCategory } : {}
3402
+ });
3403
+ }
3385
3404
  return specs;
3386
3405
  }
3387
3406
  function readControlOptions(slice) {
@@ -3759,11 +3778,12 @@ function buildDerivedPlan(device, options = UNNEGOTIATED_PLAN_OPTIONS) {
3759
3778
  deviceId: device.id,
3760
3779
  capName: cap,
3761
3780
  label: humanise(cap),
3781
+ features: device.features,
3762
3782
  slice: device.capSlices?.[cap],
3763
3783
  mapping
3764
3784
  });
3765
3785
  if (plan === null) {
3766
- specs.push(...capEntitySpecs(cap).map((spec) => degradeUnsupported(spec, options)));
3786
+ specs.push(...capEntitySpecs(cap, device.features).map((spec) => degradeUnsupported(spec, options)));
3767
3787
  continue;
3768
3788
  }
3769
3789
  native[toComponentKey(plan.component.platform, derivedEntityId(cap))] = plan.component;
@@ -3774,7 +3794,7 @@ function buildDerivedPlan(device, options = UNNEGOTIATED_PLAN_OPTIONS) {
3774
3794
  * to make the device look tidy would be a silent loss of a value an
3775
3795
  * operator already automates on.
3776
3796
  */
3777
- specs.push(...capEntitySpecs(cap).filter((spec) => !plan.absorbed.has(spec.entity)));
3797
+ specs.push(...capEntitySpecs(cap, device.features).filter((spec) => !plan.absorbed.has(spec.entity)));
3778
3798
  }
3779
3799
  const assembled = assemble(device, specs);
3780
3800
  return {
@@ -4636,6 +4656,15 @@ function isCameraCommand(command) {
4636
4656
  function isCapCommand(command) {
4637
4657
  return !isCameraCommand(command);
4638
4658
  }
4659
+ var APPLIED = { ok: true };
4660
+ /** The 422 body of a command that was not applied: the reason travels with it. */
4661
+ function commandNotAppliedBody(refusal) {
4662
+ return {
4663
+ error: "command not applied",
4664
+ reason: refusal.reason,
4665
+ ...refusal.message !== void 0 ? { message: refusal.message } : {}
4666
+ };
4667
+ }
4639
4668
  /**
4640
4669
  * Make the call the command names.
4641
4670
  *
@@ -4915,18 +4944,29 @@ async function call(slice, capName, make) {
4915
4944
  *
4916
4945
  * Idempotent: a store without the key yields an empty plan, which writes nothing.
4917
4946
  */
4918
- /** The settings scope of this addon (`<addonId>:addon-settings`). */
4919
- var HA_EXPORT_SETTINGS_SCOPE = "homeassistant-export:addon-settings";
4947
+ /** The addon id, the prefix of its rows in the canonical settings table. */
4948
+ var HA_EXPORT_ADDON_ID = "homeassistant-export";
4949
+ /**
4950
+ * Where the addon's global settings live: the canonical `addon-settings` table, one row per
4951
+ * key, id `<addonId>.<key>`, data `{ addonId, key, value }` (never a per-addon scoped table,
4952
+ * which exists only when something lazily declared it).
4953
+ */
4954
+ var ADDON_SETTINGS_TABLE = "addon-settings";
4920
4955
  var RETIRED_URL_KEYS = ["publicBaseUrl"];
4921
4956
  async function planDropPublicBaseUrl(ports) {
4922
4957
  const stored = await ports.readStored();
4923
4958
  const present = RETIRED_URL_KEYS.filter((key) => key in stored);
4924
4959
  if (present.length === 0) return { edits: [] };
4960
+ const rowId = (key) => `${HA_EXPORT_ADDON_ID}.${key}`;
4925
4961
  return { edits: [{
4926
4962
  op: "deleteKeys",
4927
- scope: HA_EXPORT_SETTINGS_SCOPE,
4928
- keys: [...present],
4929
- precondition: preconditionOf(Object.fromEntries(present.map((key) => [key, stored[key]])))
4963
+ scope: ADDON_SETTINGS_TABLE,
4964
+ keys: present.map(rowId),
4965
+ precondition: preconditionOf(Object.fromEntries(present.map((key) => [rowId(key), {
4966
+ addonId: HA_EXPORT_ADDON_ID,
4967
+ key,
4968
+ value: stored[key]
4969
+ }])))
4930
4970
  }] };
4931
4971
  }
4932
4972
  //#endregion
@@ -9122,9 +9162,23 @@ var HaExportAddon = class extends BaseAddon {
9122
9162
  reply.send({ error: `unroutable command (${resolution.reason})` });
9123
9163
  return;
9124
9164
  }
9125
- const applied = await this.dispatch(resolution.command);
9126
- reply.status(applied ? 200 : 422);
9127
- reply.send(applied ? {} : { error: "command not applied" });
9165
+ const outcome = await this.dispatch(resolution.command);
9166
+ if (outcome.ok) {
9167
+ reply.status(200);
9168
+ reply.send({});
9169
+ return;
9170
+ }
9171
+ this.ctx.logger.warn("ha-export: command answered 422", {
9172
+ tags: { deviceId: exported.deviceId },
9173
+ meta: {
9174
+ topic,
9175
+ entity: parsed.entity,
9176
+ reason: outcome.reason,
9177
+ message: outcome.message
9178
+ }
9179
+ });
9180
+ reply.status(422);
9181
+ reply.send(commandNotAppliedBody(outcome));
9128
9182
  }
9129
9183
  async dispatch(command) {
9130
9184
  try {
@@ -9138,7 +9192,7 @@ var HaExportAddon = class extends BaseAddon {
9138
9192
  */
9139
9193
  if (isCapCommand(command)) {
9140
9194
  const outcome = await applyCapCommand(await this.ctx.fetchDevice(command.deviceId), command);
9141
- if (outcome.ok) return true;
9195
+ if (outcome.ok) return APPLIED;
9142
9196
  if (outcome.reason === "no-provider") return this.noProvider(command.deviceId, outcome.capName);
9143
9197
  this.ctx.logger.warn("ha-export: the device refused the command", {
9144
9198
  tags: { deviceId: command.deviceId },
@@ -9148,7 +9202,11 @@ var HaExportAddon = class extends BaseAddon {
9148
9202
  ...outcome.error !== void 0 ? { error: outcome.error } : {}
9149
9203
  }
9150
9204
  });
9151
- return false;
9205
+ return {
9206
+ ok: false,
9207
+ reason: outcome.reason,
9208
+ ...outcome.error !== void 0 ? { message: outcome.error } : {}
9209
+ };
9152
9210
  }
9153
9211
  switch (command.kind) {
9154
9212
  case "camera-function": {
@@ -9158,7 +9216,11 @@ var HaExportAddon = class extends BaseAddon {
9158
9216
  tags: { deviceId: command.deviceId },
9159
9217
  meta: { switchId: command.switchId }
9160
9218
  });
9161
- return false;
9219
+ return {
9220
+ ok: false,
9221
+ reason: "unknown-function",
9222
+ message: "the switch id is not a camera function"
9223
+ };
9162
9224
  }
9163
9225
  /**
9164
9226
  * A FRESH read for this one device — the pass-level caches are up to
@@ -9168,7 +9230,11 @@ var HaExportAddon = class extends BaseAddon {
9168
9230
  const device = await this.ctx.api.deviceManager.getDevice.query({ deviceId: command.deviceId });
9169
9231
  if (device === null) {
9170
9232
  this.ctx.logger.warn("ha-export: dropping a camera-function command, the device is not in the registry", { tags: { deviceId: command.deviceId } });
9171
- return false;
9233
+ return {
9234
+ ok: false,
9235
+ reason: "device-not-found",
9236
+ message: "the device is not in the registry"
9237
+ };
9172
9238
  }
9173
9239
  const privacySlice = await this.ctx.api.deviceState.getCapSlice.query({
9174
9240
  deviceId: command.deviceId,
@@ -9204,22 +9270,26 @@ var HaExportAddon = class extends BaseAddon {
9204
9270
  tags: { deviceId: command.deviceId },
9205
9271
  meta: { switchId: parsed.data }
9206
9272
  });
9207
- return false;
9273
+ return {
9274
+ ok: false,
9275
+ reason: "function-unavailable",
9276
+ message: "the camera function is not available"
9277
+ };
9208
9278
  }
9209
9279
  await applyCameraFunctionWrite(this.ctx.api, command.deviceId, write, sourceNodeId);
9210
- return true;
9280
+ return APPLIED;
9211
9281
  }
9212
9282
  case "reboot": {
9213
9283
  const device = await this.ctx.fetchDevice(command.deviceId);
9214
9284
  if (device.reboot === void 0) return this.noProvider(command.deviceId, "reboot");
9215
9285
  await device.reboot.reboot({});
9216
- return true;
9286
+ return APPLIED;
9217
9287
  }
9218
9288
  case "ptz-move": {
9219
9289
  const device = await this.ctx.fetchDevice(command.deviceId);
9220
9290
  if (device.ptz === void 0) return this.noProvider(command.deviceId, "ptz");
9221
9291
  await device.ptz.move(PTZ_MOVE_BY_DIRECTION[command.direction] ?? {});
9222
- return true;
9292
+ return APPLIED;
9223
9293
  }
9224
9294
  case "ptz-preset": {
9225
9295
  const device = await this.ctx.fetchDevice(command.deviceId);
@@ -9230,10 +9300,14 @@ var HaExportAddon = class extends BaseAddon {
9230
9300
  tags: { deviceId: command.deviceId },
9231
9301
  meta: { preset: command.preset }
9232
9302
  });
9233
- return false;
9303
+ return {
9304
+ ok: false,
9305
+ reason: "no-such-preset",
9306
+ message: "no preset by that name"
9307
+ };
9234
9308
  }
9235
9309
  await device.ptz.goToPreset({ presetId: preset.id });
9236
- return true;
9310
+ return APPLIED;
9237
9311
  }
9238
9312
  }
9239
9313
  } catch (err) {
@@ -9249,7 +9323,11 @@ var HaExportAddon = class extends BaseAddon {
9249
9323
  error: errMsg(err)
9250
9324
  }
9251
9325
  });
9252
- return false;
9326
+ return {
9327
+ ok: false,
9328
+ reason: "error",
9329
+ message: errMsg(err)
9330
+ };
9253
9331
  }
9254
9332
  }
9255
9333
  /** A cap the device does not provide. Refused, and never silently. */
@@ -9258,7 +9336,11 @@ var HaExportAddon = class extends BaseAddon {
9258
9336
  tags: { deviceId },
9259
9337
  meta: { capName }
9260
9338
  });
9261
- return false;
9339
+ return {
9340
+ ok: false,
9341
+ reason: "no-provider",
9342
+ message: `the device has no provider for ${capName}`
9343
+ };
9262
9344
  }
9263
9345
  /**
9264
9346
  * A public, signed, expiring route Home Assistant can fetch bytes from.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-homeassistant",
3
- "version": "1.2.648",
3
+ "version": "1.2.650",
4
4
  "description": "Home Assistant device provider addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",
@@ -127,7 +127,7 @@
127
127
  "description": "Device provider (import) + native entity export (push). Two addons, one bundle, separate processes."
128
128
  },
129
129
  "requires": {
130
- "server": ">=1.2.733"
130
+ "server": ">=1.2.740"
131
131
  }
132
132
  },
133
133
  "files": [