@camstack/addon-provider-homeassistant 1.2.649 → 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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.650] - 2026-11-02
11
+
12
+ ### Changed
13
+
14
+ - Home Assistant export: a cover offers position and tilt only when the device declares them, shows unavailable while its provider is offline, and a refused command now answers 422 with the reason
15
+ - A cover exports the stop control to Home Assistant only when the device declares cover-stoppable, and a Home Assistant cover with the STOP bit declares it.
16
+
10
17
  ## [1.2.649] - 2026-11-02
11
18
 
12
19
  ### Changed
package/dist/addon.js CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_ha_broker_source = require("./ha-broker-source-CE3nHkyW.js");
2
+ const require_ha_broker_source = require("./ha-broker-source-DUj9pUWr.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
@@ -6328,6 +6328,7 @@ function mapCover(attributes, deviceClass) {
6328
6328
  const features = [];
6329
6329
  const sf = readNumericAttr(attributes, "supported_features");
6330
6330
  if (sf !== null && (sf & 4) !== 0) features.push(require_ha_broker_source.DeviceFeature.CoverPositionable);
6331
+ if (sf !== null && (sf & 8) !== 0) features.push(require_ha_broker_source.DeviceFeature.CoverStoppable);
6331
6332
  if (sf !== null && (sf & 128) !== 0) features.push(require_ha_broker_source.DeviceFeature.CoverTilt);
6332
6333
  return {
6333
6334
  deviceType: require_ha_broker_source.DeviceType.Cover,
package/dist/addon.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as humiditySensorCapability, $t as discriminatedUnion, A as brokerCapability, At as tamperCapability, C as alarmPanelCapability, Ct as runningFromMediaPlayer, D as bestLocationMatch, E as batteryCapability, Et as scriptRunnerCapability, F as carbonMonoxideCapability, Ft as vacuumControlCapability, G as deviceAdoptionCapability, Gt as defineEntryKind, H as controlCapability, Ht as baseUnitOf, I as climateControlCapability, It as valveCapability, J as eventEmitterCapability, Kt as defineRecordKinds, L as colorCapability, Lt as vibrationCapability, M as buildDeviceAdminUrl, Mt as tryConvertDelta, N as buttonCapability, Nt as tryConvertUnit, O as binaryCapability, Ot as smokeCapability, P as calendarSourceCapability, Pt as updateCapability, Q as humidifierCapability, Qt as boolean, R as connectivityCapability, Rt as waterHeaterCapability, S as airQualitySensorCapability, St as runningFromLawnMower, T as automationControlCapability, Tt as runningStateCapability, U as coverCapability, Ut as createEvent, V as contactCapability, Vt as RecordSecretUnavailableError, W as deviceAdminLinkCapability, X as floodCapability, Xt as _enum, Y as fanControlCapability, Z as gasCapability, Zt as array, a as BaseDevice, an as union, at as motionCapability, b as accessoriesCapability, bt as resolveAttachmentUrls, cn as DeviceFeature, ct as notifierCapability, dn as errMsg, dt as parseAdminBase, en as literal, et as imageCapability, fn as EventCategory, ft as powerMeterCapability, gt as pressureSensorCapability, ht as presenceCapability, in as string, it as mediaPlayerCapability, j as buildAddonRouteProvider, jt as temperatureSensorCapability, k as brightnessCapability, kt as switchCapability, ln as DeviceRole, lt as numericSensorCapability, mt as prepareNotification, nn as object, nt as lawnMowerControlCapability, o as BaseDeviceProvider, on as unknown, ot as normalizeUnit, p as EnumSensorDateTimeFormatSchema, q as enumSensorCapability, qt as entryBody, rn as record, rt as lockControlCapability, sn as url, st as notificationOutputCapability, t as HA_BROKER_KIND, tn as number, tt as ingestReading, un as DeviceType, v as RUNNING_STATE_COLD, vt as publicEndpointReaderFrom, w as ambientLightSensorCapability, wt as runningFromVacuumState, x as addonRoutesCapability, y as TargetSchema, yt as quantityFromUnit, zt as weatherCapability } from "./ha-broker-source-D3FBA6uG.mjs";
1
+ import { $ as humiditySensorCapability, $t as discriminatedUnion, A as brokerCapability, At as tamperCapability, C as alarmPanelCapability, Ct as runningFromMediaPlayer, D as bestLocationMatch, E as batteryCapability, Et as scriptRunnerCapability, F as carbonMonoxideCapability, Ft as vacuumControlCapability, G as deviceAdoptionCapability, Gt as defineEntryKind, H as controlCapability, Ht as baseUnitOf, I as climateControlCapability, It as valveCapability, J as eventEmitterCapability, Kt as defineRecordKinds, L as colorCapability, Lt as vibrationCapability, M as buildDeviceAdminUrl, Mt as tryConvertDelta, N as buttonCapability, Nt as tryConvertUnit, O as binaryCapability, Ot as smokeCapability, P as calendarSourceCapability, Pt as updateCapability, Q as humidifierCapability, Qt as boolean, R as connectivityCapability, Rt as waterHeaterCapability, S as airQualitySensorCapability, St as runningFromLawnMower, T as automationControlCapability, Tt as runningStateCapability, U as coverCapability, Ut as createEvent, V as contactCapability, Vt as RecordSecretUnavailableError, W as deviceAdminLinkCapability, X as floodCapability, Xt as _enum, Y as fanControlCapability, Z as gasCapability, Zt as array, a as BaseDevice, an as union, at as motionCapability, b as accessoriesCapability, bt as resolveAttachmentUrls, cn as DeviceFeature, ct as notifierCapability, dn as errMsg, dt as parseAdminBase, en as literal, et as imageCapability, fn as EventCategory, ft as powerMeterCapability, gt as pressureSensorCapability, ht as presenceCapability, in as string, it as mediaPlayerCapability, j as buildAddonRouteProvider, jt as temperatureSensorCapability, k as brightnessCapability, kt as switchCapability, ln as DeviceRole, lt as numericSensorCapability, mt as prepareNotification, nn as object, nt as lawnMowerControlCapability, o as BaseDeviceProvider, on as unknown, ot as normalizeUnit, p as EnumSensorDateTimeFormatSchema, q as enumSensorCapability, qt as entryBody, rn as record, rt as lockControlCapability, sn as url, st as notificationOutputCapability, t as HA_BROKER_KIND, tn as number, tt as ingestReading, un as DeviceType, v as RUNNING_STATE_COLD, vt as publicEndpointReaderFrom, w as ambientLightSensorCapability, wt as runningFromVacuumState, x as addonRoutesCapability, y as TargetSchema, yt as quantityFromUnit, zt as weatherCapability } from "./ha-broker-source-B2v2dnPe.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
@@ -6327,6 +6327,7 @@ function mapCover(attributes, deviceClass) {
6327
6327
  const features = [];
6328
6328
  const sf = readNumericAttr(attributes, "supported_features");
6329
6329
  if (sf !== null && (sf & 4) !== 0) features.push(DeviceFeature.CoverPositionable);
6330
+ if (sf !== null && (sf & 8) !== 0) features.push(DeviceFeature.CoverStoppable);
6330
6331
  if (sf !== null && (sf & 128) !== 0) features.push(DeviceFeature.CoverTilt);
6331
6332
  return {
6332
6333
  deviceType: DeviceType.Cover,
@@ -784,7 +784,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
784
784
  return EventCategory;
785
785
  }({});
786
786
  //#endregion
787
- //#region ../types/dist/device-type-BOWLtoOt.mjs
787
+ //#region ../types/dist/device-type-CfHw_quR.mjs
788
788
  /**
789
789
  import { errMsg } from '@camstack/types'
790
790
  * Extract a human-readable message from an unknown error value.
@@ -973,6 +973,9 @@ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
973
973
  DeviceFeature["CoverPositionable"] = "cover-positionable";
974
974
  /** Cover exposes slat-tilt control. Gates the tilt slider UI. */
975
975
  DeviceFeature["CoverTilt"] = "cover-tilt";
976
+ /** Cover has a real stop: `stop` halts the travel in place. Gates the stop
977
+ * control in exporters; a provider whose `stop` is a no-op never declares it. */
978
+ DeviceFeature["CoverStoppable"] = "cover-stoppable";
976
979
  /** Valve exposes intermediate position control (0..100). Gates the
977
980
  * position slider / drag surface UI. */
978
981
  DeviceFeature["ValvePositionable"] = "valve-positionable";
@@ -5971,7 +5974,7 @@ var ZodIssueCode = {
5971
5974
  var ZodFirstPartyTypeKind;
5972
5975
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5973
5976
  //#endregion
5974
- //#region ../types/dist/sleep-CdJHPm-k.mjs
5977
+ //#region ../types/dist/sleep-QsoHs66J.mjs
5975
5978
  /**
5976
5979
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5977
5980
  * window to float samples (D455).
@@ -11757,7 +11760,7 @@ object({
11757
11760
  params: record(string(), union([string(), number()])).optional()
11758
11761
  });
11759
11762
  //#endregion
11760
- //#region ../types/dist/composition-BDUoQsw9.mjs
11763
+ //#region ../types/dist/composition-Dh3RfMx5.mjs
11761
11764
  /**
11762
11765
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11763
11766
  *
@@ -55740,15 +55743,9 @@ var AvailableIntegrationTypeSchema = object({
55740
55743
  * branch on CAP — never on addon name. `device-adoption` integrations
55741
55744
  * (Home Assistant, …) route through the broker step (Approach A);
55742
55745
  * `device-provider` integrations (Reolink/ONVIF) keep the
55743
- * legacy config → discovery flow. `connection` is an addon that only holds a
55744
- * connection as typed records (`integration-instances`, D985): no devices,
55745
- * no discovery.
55746
+ * legacy config → discovery flow.
55746
55747
  */
55747
- kind: _enum([
55748
- "device-adoption",
55749
- "device-provider",
55750
- "connection"
55751
- ]),
55748
+ kind: _enum(["device-adoption", "device-provider"]),
55752
55749
  /**
55753
55750
  * For `device-adoption` addons: the broker kind to create/link
55754
55751
  * (e.g. `home-assistant`), declared in the addon manifest. `null` for
@@ -55862,18 +55859,17 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
55862
55859
  });
55863
55860
  /**
55864
55861
  * `integration-instances` - the connection of an addon that holds ONE or SEVERAL
55865
- * (an account, a relay, a destination) as typed records, shown on the
55866
- * Integrations page like every other integration (D985, D986).
55862
+ * (an account, a relay, a destination) as typed records (D985, D986). The addon's
55863
+ * own function page reads and writes them (Logging for Loki); the Integrations
55864
+ * page lists only the integrations that bring devices (D996).
55867
55865
  *
55868
- * A collection of providers: each addon that holds connections registers one
55869
- * and the hub aggregator (`integrations.*`) reaches a provider with the pinned
55870
- * `addonId`. It is internal and has no route: the operator-facing surface is
55871
- * the `integrations` cap, which keeps ONE list.
55866
+ * A collection of providers: each addon that holds connections registers one and a
55867
+ * caller reaches a provider with the pinned `addonId`.
55872
55868
  *
55873
- * Identity: the instance id IS the `integrations` registry id (`int_NNNN`,
55874
- * immutable, already in the `/integrations/<id>` URL). A second operator-chosen
55875
- * slug would be a second authority over one identity (D224). `name` is the
55876
- * editable label.
55869
+ * Identity: the instance id is immutable and opaque to the caller. A record the
55870
+ * Loki migration made keeps the `integrations` registry id it was created under
55871
+ * (`int_NNNN`); a single instance made by its function page takes the addon id.
55872
+ * `name` is the editable label.
55877
55873
  *
55878
55874
  * Secrets: every field the instance kind marks `.meta({ secret: true })` reaches
55879
55875
  * the form as a `SecretFieldView`, never text (D789). A patch value omitted, `''`
@@ -55914,13 +55910,25 @@ var IntegrationInstancePatchSchema = object({
55914
55910
  /** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
55915
55911
  values: record(string(), unknown())
55916
55912
  });
55917
- method(_void(), IntegrationInstanceDescriptionSchema), method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3), method(_void(), array(IntegrationInstanceSchema).readonly()), method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable()), method(object({
55913
+ 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({
55918
55914
  id: IntegrationInstanceIdSchema,
55919
55915
  name: string().min(1)
55920
- }), object({ id: string() }), { kind: "mutation" }), method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), { kind: "mutation" }), method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), { kind: "mutation" }), optionalMethod(object({
55916
+ }), object({ id: string() }), {
55917
+ kind: "mutation",
55918
+ auth: "admin"
55919
+ }), method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
55920
+ kind: "mutation",
55921
+ auth: "admin"
55922
+ }), method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
55923
+ kind: "mutation",
55924
+ auth: "admin"
55925
+ }), optionalMethod(object({
55921
55926
  id: IntegrationInstanceIdSchema.nullable(),
55922
55927
  values: record(string(), unknown())
55923
- }), TestConnectionResultSchema$1, { kind: "mutation" });
55928
+ }), TestConnectionResultSchema$1, {
55929
+ kind: "mutation",
55930
+ auth: "admin"
55931
+ });
55924
55932
  /**
55925
55933
  * Two-way audio / talk-back cap. Clients open a WebRTC audio-only
55926
55934
  * session (server-initiated offer pattern, identical to the main
@@ -66689,7 +66697,14 @@ var NcMediaPolicySchema = object({
66689
66697
  */
66690
66698
  profile: CamProfileSchema.optional(),
66691
66699
  /** See {@link NcLinkedCameraModeSchema}. */
66692
- linkedCameras: NcLinkedCameraModeSchema.optional()
66700
+ linkedCameras: NcLinkedCameraModeSchema.optional(),
66701
+ /**
66702
+ * Sensor rules only: may this rule wake a SLEEPING battery camera to take
66703
+ * the linked still it attaches (D998)? Absent means yes, an explicit `false`
66704
+ * opts out. Deliberately not a Zod default: a rule that never chose keeps
66705
+ * following the hub's. One bounded wake per event; nothing is attached later.
66706
+ */
66707
+ wakeSleepingCamera: boolean().optional()
66693
66708
  });
66694
66709
  /**
66695
66710
  * Cooldown GRANULARITY over the subject's class — how much a fired
@@ -75270,7 +75285,16 @@ var smokeCapability = {
75270
75285
  * future fields without breaking the existing call sites.
75271
75286
  */
75272
75287
  var EmailAddressSchema = email();
75288
+ /**
75289
+ * Which relay of a multi-relay provider answers (D996). Absent = the provider's first / default
75290
+ * relay, so a caller written before relays existed keeps working unchanged. A provider with one
75291
+ * fixed relay ignores it. This is the integration instance id (`int_NNNN`), NOT the addon id: the
75292
+ * collection pin `addonId` still picks the provider, `instanceId` picks the relay inside it.
75293
+ */
75294
+ var InstanceIdSchema = string().min(1).optional();
75295
+ var RelayPinSchema = object({ instanceId: InstanceIdSchema });
75273
75296
  var SendEmailInputSchema = object({
75297
+ instanceId: InstanceIdSchema,
75274
75298
  to: union([EmailAddressSchema, array(EmailAddressSchema).min(1)]),
75275
75299
  cc: array(EmailAddressSchema).optional(),
75276
75300
  bcc: array(EmailAddressSchema).optional(),
@@ -75306,11 +75330,11 @@ method(SendEmailInputSchema, SendEmailResultSchema, {
75306
75330
  kind: "mutation",
75307
75331
  auth: "admin",
75308
75332
  access: "create"
75309
- }), method(_void(), SmtpStatusSchema, {
75333
+ }), method(RelayPinSchema, SmtpStatusSchema, {
75310
75334
  kind: "mutation",
75311
75335
  auth: "admin",
75312
75336
  access: "view"
75313
- }), method(_void(), SmtpStatusSchema, { auth: "admin" });
75337
+ }), method(RelayPinSchema, SmtpStatusSchema, { auth: "admin" });
75314
75338
  /**
75315
75339
  * A single device snapshot returned as base64 JPEG/PNG.
75316
75340
  *
@@ -75342,10 +75366,12 @@ DeviceType.Camera, method(object({
75342
75366
  * even when the cache is well within the device's
75343
75367
  * `snapshotMaxAgeS` window.
75344
75368
  *
75345
- * **`force` is an OPERATOR signal, not a freshness preference.** On a
75369
+ * **`force` is a demand with a named principal, not a freshness
75370
+ * preference** — an operator button, the scheduled refresh (D847), or a
75371
+ * notification rule that left the camera's wake allowed (D998). On a
75346
75372
  * battery camera it is the one thing that walks past the wrapper's
75347
- * sleep gate and wakes the camera, so a background caller — a poller,
75348
- * an event handler, a thumbnail — must NEVER set it. Every such caller
75373
+ * sleep gate and wakes the camera, so a poller, a thumbnail or a
75374
+ * prestream must NEVER set it. Every such caller
75349
75375
  * gets the cached frame, which on a sleeping battery camera is the
75350
75376
  * correct answer: stale but honest beats woken.
75351
75377
  */
@@ -80195,7 +80221,7 @@ function runningFromMediaPlayer(state, previous, fetchedAt, transitionAt) {
80195
80221
  if (state === "paused") return nativeSlice(false, "paused", previous, transitionAtMs, fetchedAt, null);
80196
80222
  return nativeSlice(false, state, previous, transitionAtMs, fetchedAt, null);
80197
80223
  }
80198
- DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
80224
+ DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
80199
80225
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
80200
80226
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
80201
80227
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
@@ -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-CfHw_quR.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";
@@ -5972,7 +5975,7 @@ var ZodIssueCode = {
5972
5975
  var ZodFirstPartyTypeKind;
5973
5976
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5974
5977
  //#endregion
5975
- //#region ../types/dist/sleep-CdJHPm-k.mjs
5978
+ //#region ../types/dist/sleep-QsoHs66J.mjs
5976
5979
  /**
5977
5980
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5978
5981
  * window to float samples (D455).
@@ -11758,7 +11761,7 @@ object({
11758
11761
  params: record(string(), union([string(), number()])).optional()
11759
11762
  });
11760
11763
  //#endregion
11761
- //#region ../types/dist/composition-BDUoQsw9.mjs
11764
+ //#region ../types/dist/composition-Dh3RfMx5.mjs
11762
11765
  /**
11763
11766
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11764
11767
  *
@@ -55741,15 +55744,9 @@ var AvailableIntegrationTypeSchema = object({
55741
55744
  * branch on CAP — never on addon name. `device-adoption` integrations
55742
55745
  * (Home Assistant, …) route through the broker step (Approach A);
55743
55746
  * `device-provider` integrations (Reolink/ONVIF) keep the
55744
- * legacy config → discovery flow. `connection` is an addon that only holds a
55745
- * connection as typed records (`integration-instances`, D985): no devices,
55746
- * no discovery.
55747
+ * legacy config → discovery flow.
55747
55748
  */
55748
- kind: _enum([
55749
- "device-adoption",
55750
- "device-provider",
55751
- "connection"
55752
- ]),
55749
+ kind: _enum(["device-adoption", "device-provider"]),
55753
55750
  /**
55754
55751
  * For `device-adoption` addons: the broker kind to create/link
55755
55752
  * (e.g. `home-assistant`), declared in the addon manifest. `null` for
@@ -55863,18 +55860,17 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
55863
55860
  });
55864
55861
  /**
55865
55862
  * `integration-instances` - the connection of an addon that holds ONE or SEVERAL
55866
- * (an account, a relay, a destination) as typed records, shown on the
55867
- * Integrations page like every other integration (D985, D986).
55863
+ * (an account, a relay, a destination) as typed records (D985, D986). The addon's
55864
+ * own function page reads and writes them (Logging for Loki); the Integrations
55865
+ * page lists only the integrations that bring devices (D996).
55868
55866
  *
55869
- * A collection of providers: each addon that holds connections registers one
55870
- * and the hub aggregator (`integrations.*`) reaches a provider with the pinned
55871
- * `addonId`. It is internal and has no route: the operator-facing surface is
55872
- * the `integrations` cap, which keeps ONE list.
55867
+ * A collection of providers: each addon that holds connections registers one and a
55868
+ * caller reaches a provider with the pinned `addonId`.
55873
55869
  *
55874
- * Identity: the instance id IS the `integrations` registry id (`int_NNNN`,
55875
- * immutable, already in the `/integrations/<id>` URL). A second operator-chosen
55876
- * slug would be a second authority over one identity (D224). `name` is the
55877
- * editable label.
55870
+ * Identity: the instance id is immutable and opaque to the caller. A record the
55871
+ * Loki migration made keeps the `integrations` registry id it was created under
55872
+ * (`int_NNNN`); a single instance made by its function page takes the addon id.
55873
+ * `name` is the editable label.
55878
55874
  *
55879
55875
  * Secrets: every field the instance kind marks `.meta({ secret: true })` reaches
55880
55876
  * the form as a `SecretFieldView`, never text (D789). A patch value omitted, `''`
@@ -55915,13 +55911,25 @@ var IntegrationInstancePatchSchema = object({
55915
55911
  /** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
55916
55912
  values: record(string(), unknown())
55917
55913
  });
55918
- method(_void(), IntegrationInstanceDescriptionSchema), method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3), method(_void(), array(IntegrationInstanceSchema).readonly()), method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable()), method(object({
55914
+ 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({
55919
55915
  id: IntegrationInstanceIdSchema,
55920
55916
  name: string().min(1)
55921
- }), object({ id: string() }), { kind: "mutation" }), method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), { kind: "mutation" }), method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), { kind: "mutation" }), optionalMethod(object({
55917
+ }), object({ id: string() }), {
55918
+ kind: "mutation",
55919
+ auth: "admin"
55920
+ }), method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
55921
+ kind: "mutation",
55922
+ auth: "admin"
55923
+ }), method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
55924
+ kind: "mutation",
55925
+ auth: "admin"
55926
+ }), optionalMethod(object({
55922
55927
  id: IntegrationInstanceIdSchema.nullable(),
55923
55928
  values: record(string(), unknown())
55924
- }), TestConnectionResultSchema$1, { kind: "mutation" });
55929
+ }), TestConnectionResultSchema$1, {
55930
+ kind: "mutation",
55931
+ auth: "admin"
55932
+ });
55925
55933
  /**
55926
55934
  * Two-way audio / talk-back cap. Clients open a WebRTC audio-only
55927
55935
  * session (server-initiated offer pattern, identical to the main
@@ -66690,7 +66698,14 @@ var NcMediaPolicySchema = object({
66690
66698
  */
66691
66699
  profile: CamProfileSchema.optional(),
66692
66700
  /** See {@link NcLinkedCameraModeSchema}. */
66693
- linkedCameras: NcLinkedCameraModeSchema.optional()
66701
+ linkedCameras: NcLinkedCameraModeSchema.optional(),
66702
+ /**
66703
+ * Sensor rules only: may this rule wake a SLEEPING battery camera to take
66704
+ * the linked still it attaches (D998)? Absent means yes, an explicit `false`
66705
+ * opts out. Deliberately not a Zod default: a rule that never chose keeps
66706
+ * following the hub's. One bounded wake per event; nothing is attached later.
66707
+ */
66708
+ wakeSleepingCamera: boolean().optional()
66694
66709
  });
66695
66710
  /**
66696
66711
  * Cooldown GRANULARITY over the subject's class — how much a fired
@@ -75271,7 +75286,16 @@ var smokeCapability = {
75271
75286
  * future fields without breaking the existing call sites.
75272
75287
  */
75273
75288
  var EmailAddressSchema = email();
75289
+ /**
75290
+ * Which relay of a multi-relay provider answers (D996). Absent = the provider's first / default
75291
+ * relay, so a caller written before relays existed keeps working unchanged. A provider with one
75292
+ * fixed relay ignores it. This is the integration instance id (`int_NNNN`), NOT the addon id: the
75293
+ * collection pin `addonId` still picks the provider, `instanceId` picks the relay inside it.
75294
+ */
75295
+ var InstanceIdSchema = string().min(1).optional();
75296
+ var RelayPinSchema = object({ instanceId: InstanceIdSchema });
75274
75297
  var SendEmailInputSchema = object({
75298
+ instanceId: InstanceIdSchema,
75275
75299
  to: union([EmailAddressSchema, array(EmailAddressSchema).min(1)]),
75276
75300
  cc: array(EmailAddressSchema).optional(),
75277
75301
  bcc: array(EmailAddressSchema).optional(),
@@ -75307,11 +75331,11 @@ method(SendEmailInputSchema, SendEmailResultSchema, {
75307
75331
  kind: "mutation",
75308
75332
  auth: "admin",
75309
75333
  access: "create"
75310
- }), method(_void(), SmtpStatusSchema, {
75334
+ }), method(RelayPinSchema, SmtpStatusSchema, {
75311
75335
  kind: "mutation",
75312
75336
  auth: "admin",
75313
75337
  access: "view"
75314
- }), method(_void(), SmtpStatusSchema, { auth: "admin" });
75338
+ }), method(RelayPinSchema, SmtpStatusSchema, { auth: "admin" });
75315
75339
  /**
75316
75340
  * A single device snapshot returned as base64 JPEG/PNG.
75317
75341
  *
@@ -75343,10 +75367,12 @@ DeviceType.Camera, method(object({
75343
75367
  * even when the cache is well within the device's
75344
75368
  * `snapshotMaxAgeS` window.
75345
75369
  *
75346
- * **`force` is an OPERATOR signal, not a freshness preference.** On a
75370
+ * **`force` is a demand with a named principal, not a freshness
75371
+ * preference** — an operator button, the scheduled refresh (D847), or a
75372
+ * notification rule that left the camera's wake allowed (D998). On a
75347
75373
  * battery camera it is the one thing that walks past the wrapper's
75348
- * sleep gate and wakes the camera, so a background caller — a poller,
75349
- * an event handler, a thumbnail — must NEVER set it. Every such caller
75374
+ * sleep gate and wakes the camera, so a poller, a thumbnail or a
75375
+ * prestream must NEVER set it. Every such caller
75350
75376
  * gets the cached frame, which on a sleeping battery camera is the
75351
75377
  * correct answer: stale but honest beats woken.
75352
75378
  */
@@ -80196,7 +80222,7 @@ function runningFromMediaPlayer(state, previous, fetchedAt, transitionAt) {
80196
80222
  if (state === "paused") return nativeSlice(false, "paused", previous, transitionAtMs, fetchedAt, null);
80197
80223
  return nativeSlice(false, state, previous, transitionAtMs, fetchedAt, null);
80198
80224
  }
80199
- DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
80225
+ DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
80200
80226
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
80201
80227
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
80202
80228
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
@@ -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-CE3nHkyW.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
  *
@@ -9168,9 +9197,23 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
9168
9197
  reply.send({ error: `unroutable command (${resolution.reason})` });
9169
9198
  return;
9170
9199
  }
9171
- const applied = await this.dispatch(resolution.command);
9172
- reply.status(applied ? 200 : 422);
9173
- 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));
9174
9217
  }
9175
9218
  async dispatch(command) {
9176
9219
  try {
@@ -9184,7 +9227,7 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
9184
9227
  */
9185
9228
  if (isCapCommand(command)) {
9186
9229
  const outcome = await applyCapCommand(await this.ctx.fetchDevice(command.deviceId), command);
9187
- if (outcome.ok) return true;
9230
+ if (outcome.ok) return APPLIED;
9188
9231
  if (outcome.reason === "no-provider") return this.noProvider(command.deviceId, outcome.capName);
9189
9232
  this.ctx.logger.warn("ha-export: the device refused the command", {
9190
9233
  tags: { deviceId: command.deviceId },
@@ -9194,7 +9237,11 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
9194
9237
  ...outcome.error !== void 0 ? { error: outcome.error } : {}
9195
9238
  }
9196
9239
  });
9197
- return false;
9240
+ return {
9241
+ ok: false,
9242
+ reason: outcome.reason,
9243
+ ...outcome.error !== void 0 ? { message: outcome.error } : {}
9244
+ };
9198
9245
  }
9199
9246
  switch (command.kind) {
9200
9247
  case "camera-function": {
@@ -9204,7 +9251,11 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
9204
9251
  tags: { deviceId: command.deviceId },
9205
9252
  meta: { switchId: command.switchId }
9206
9253
  });
9207
- return false;
9254
+ return {
9255
+ ok: false,
9256
+ reason: "unknown-function",
9257
+ message: "the switch id is not a camera function"
9258
+ };
9208
9259
  }
9209
9260
  /**
9210
9261
  * A FRESH read for this one device — the pass-level caches are up to
@@ -9214,7 +9265,11 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
9214
9265
  const device = await this.ctx.api.deviceManager.getDevice.query({ deviceId: command.deviceId });
9215
9266
  if (device === null) {
9216
9267
  this.ctx.logger.warn("ha-export: dropping a camera-function command, the device is not in the registry", { tags: { deviceId: command.deviceId } });
9217
- return false;
9268
+ return {
9269
+ ok: false,
9270
+ reason: "device-not-found",
9271
+ message: "the device is not in the registry"
9272
+ };
9218
9273
  }
9219
9274
  const privacySlice = await this.ctx.api.deviceState.getCapSlice.query({
9220
9275
  deviceId: command.deviceId,
@@ -9250,22 +9305,26 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
9250
9305
  tags: { deviceId: command.deviceId },
9251
9306
  meta: { switchId: parsed.data }
9252
9307
  });
9253
- return false;
9308
+ return {
9309
+ ok: false,
9310
+ reason: "function-unavailable",
9311
+ message: "the camera function is not available"
9312
+ };
9254
9313
  }
9255
9314
  await applyCameraFunctionWrite(this.ctx.api, command.deviceId, write, sourceNodeId);
9256
- return true;
9315
+ return APPLIED;
9257
9316
  }
9258
9317
  case "reboot": {
9259
9318
  const device = await this.ctx.fetchDevice(command.deviceId);
9260
9319
  if (device.reboot === void 0) return this.noProvider(command.deviceId, "reboot");
9261
9320
  await device.reboot.reboot({});
9262
- return true;
9321
+ return APPLIED;
9263
9322
  }
9264
9323
  case "ptz-move": {
9265
9324
  const device = await this.ctx.fetchDevice(command.deviceId);
9266
9325
  if (device.ptz === void 0) return this.noProvider(command.deviceId, "ptz");
9267
9326
  await device.ptz.move(PTZ_MOVE_BY_DIRECTION[command.direction] ?? {});
9268
- return true;
9327
+ return APPLIED;
9269
9328
  }
9270
9329
  case "ptz-preset": {
9271
9330
  const device = await this.ctx.fetchDevice(command.deviceId);
@@ -9276,10 +9335,14 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
9276
9335
  tags: { deviceId: command.deviceId },
9277
9336
  meta: { preset: command.preset }
9278
9337
  });
9279
- return false;
9338
+ return {
9339
+ ok: false,
9340
+ reason: "no-such-preset",
9341
+ message: "no preset by that name"
9342
+ };
9280
9343
  }
9281
9344
  await device.ptz.goToPreset({ presetId: preset.id });
9282
- return true;
9345
+ return APPLIED;
9283
9346
  }
9284
9347
  }
9285
9348
  } catch (err) {
@@ -9295,7 +9358,11 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
9295
9358
  error: errMsg(err)
9296
9359
  }
9297
9360
  });
9298
- return false;
9361
+ return {
9362
+ ok: false,
9363
+ reason: "error",
9364
+ message: errMsg(err)
9365
+ };
9299
9366
  }
9300
9367
  }
9301
9368
  /** A cap the device does not provide. Refused, and never silently. */
@@ -9304,7 +9371,11 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
9304
9371
  tags: { deviceId },
9305
9372
  meta: { capName }
9306
9373
  });
9307
- return false;
9374
+ return {
9375
+ ok: false,
9376
+ reason: "no-provider",
9377
+ message: `the device has no provider for ${capName}`
9378
+ };
9308
9379
  }
9309
9380
  /**
9310
9381
  * A public, signed, expiring route Home Assistant can fetch bytes from.
@@ -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-D3FBA6uG.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
  *
@@ -9133,9 +9162,23 @@ var HaExportAddon = class extends BaseAddon {
9133
9162
  reply.send({ error: `unroutable command (${resolution.reason})` });
9134
9163
  return;
9135
9164
  }
9136
- const applied = await this.dispatch(resolution.command);
9137
- reply.status(applied ? 200 : 422);
9138
- 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));
9139
9182
  }
9140
9183
  async dispatch(command) {
9141
9184
  try {
@@ -9149,7 +9192,7 @@ var HaExportAddon = class extends BaseAddon {
9149
9192
  */
9150
9193
  if (isCapCommand(command)) {
9151
9194
  const outcome = await applyCapCommand(await this.ctx.fetchDevice(command.deviceId), command);
9152
- if (outcome.ok) return true;
9195
+ if (outcome.ok) return APPLIED;
9153
9196
  if (outcome.reason === "no-provider") return this.noProvider(command.deviceId, outcome.capName);
9154
9197
  this.ctx.logger.warn("ha-export: the device refused the command", {
9155
9198
  tags: { deviceId: command.deviceId },
@@ -9159,7 +9202,11 @@ var HaExportAddon = class extends BaseAddon {
9159
9202
  ...outcome.error !== void 0 ? { error: outcome.error } : {}
9160
9203
  }
9161
9204
  });
9162
- return false;
9205
+ return {
9206
+ ok: false,
9207
+ reason: outcome.reason,
9208
+ ...outcome.error !== void 0 ? { message: outcome.error } : {}
9209
+ };
9163
9210
  }
9164
9211
  switch (command.kind) {
9165
9212
  case "camera-function": {
@@ -9169,7 +9216,11 @@ var HaExportAddon = class extends BaseAddon {
9169
9216
  tags: { deviceId: command.deviceId },
9170
9217
  meta: { switchId: command.switchId }
9171
9218
  });
9172
- return false;
9219
+ return {
9220
+ ok: false,
9221
+ reason: "unknown-function",
9222
+ message: "the switch id is not a camera function"
9223
+ };
9173
9224
  }
9174
9225
  /**
9175
9226
  * A FRESH read for this one device — the pass-level caches are up to
@@ -9179,7 +9230,11 @@ var HaExportAddon = class extends BaseAddon {
9179
9230
  const device = await this.ctx.api.deviceManager.getDevice.query({ deviceId: command.deviceId });
9180
9231
  if (device === null) {
9181
9232
  this.ctx.logger.warn("ha-export: dropping a camera-function command, the device is not in the registry", { tags: { deviceId: command.deviceId } });
9182
- return false;
9233
+ return {
9234
+ ok: false,
9235
+ reason: "device-not-found",
9236
+ message: "the device is not in the registry"
9237
+ };
9183
9238
  }
9184
9239
  const privacySlice = await this.ctx.api.deviceState.getCapSlice.query({
9185
9240
  deviceId: command.deviceId,
@@ -9215,22 +9270,26 @@ var HaExportAddon = class extends BaseAddon {
9215
9270
  tags: { deviceId: command.deviceId },
9216
9271
  meta: { switchId: parsed.data }
9217
9272
  });
9218
- return false;
9273
+ return {
9274
+ ok: false,
9275
+ reason: "function-unavailable",
9276
+ message: "the camera function is not available"
9277
+ };
9219
9278
  }
9220
9279
  await applyCameraFunctionWrite(this.ctx.api, command.deviceId, write, sourceNodeId);
9221
- return true;
9280
+ return APPLIED;
9222
9281
  }
9223
9282
  case "reboot": {
9224
9283
  const device = await this.ctx.fetchDevice(command.deviceId);
9225
9284
  if (device.reboot === void 0) return this.noProvider(command.deviceId, "reboot");
9226
9285
  await device.reboot.reboot({});
9227
- return true;
9286
+ return APPLIED;
9228
9287
  }
9229
9288
  case "ptz-move": {
9230
9289
  const device = await this.ctx.fetchDevice(command.deviceId);
9231
9290
  if (device.ptz === void 0) return this.noProvider(command.deviceId, "ptz");
9232
9291
  await device.ptz.move(PTZ_MOVE_BY_DIRECTION[command.direction] ?? {});
9233
- return true;
9292
+ return APPLIED;
9234
9293
  }
9235
9294
  case "ptz-preset": {
9236
9295
  const device = await this.ctx.fetchDevice(command.deviceId);
@@ -9241,10 +9300,14 @@ var HaExportAddon = class extends BaseAddon {
9241
9300
  tags: { deviceId: command.deviceId },
9242
9301
  meta: { preset: command.preset }
9243
9302
  });
9244
- return false;
9303
+ return {
9304
+ ok: false,
9305
+ reason: "no-such-preset",
9306
+ message: "no preset by that name"
9307
+ };
9245
9308
  }
9246
9309
  await device.ptz.goToPreset({ presetId: preset.id });
9247
- return true;
9310
+ return APPLIED;
9248
9311
  }
9249
9312
  }
9250
9313
  } catch (err) {
@@ -9260,7 +9323,11 @@ var HaExportAddon = class extends BaseAddon {
9260
9323
  error: errMsg(err)
9261
9324
  }
9262
9325
  });
9263
- return false;
9326
+ return {
9327
+ ok: false,
9328
+ reason: "error",
9329
+ message: errMsg(err)
9330
+ };
9264
9331
  }
9265
9332
  }
9266
9333
  /** A cap the device does not provide. Refused, and never silently. */
@@ -9269,7 +9336,11 @@ var HaExportAddon = class extends BaseAddon {
9269
9336
  tags: { deviceId },
9270
9337
  meta: { capName }
9271
9338
  });
9272
- return false;
9339
+ return {
9340
+ ok: false,
9341
+ reason: "no-provider",
9342
+ message: `the device has no provider for ${capName}`
9343
+ };
9273
9344
  }
9274
9345
  /**
9275
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.649",
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.738"
130
+ "server": ">=1.2.740"
131
131
  }
132
132
  },
133
133
  "files": [