@camstack/addon-provider-homeassistant 1.2.649 → 1.2.651

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,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.651] - 2026-11-02
11
+
12
+ ### Changed
13
+
14
+ - A Home Assistant restart no longer parks the link for 10 minutes: a 404 must persist for a minute before the component counts as missing, and a suspended link is now logged and reported on every reconcile
15
+
16
+ ## [1.2.650] - 2026-11-02
17
+
18
+ ### Changed
19
+
20
+ - 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
21
+ - 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.
22
+
10
23
  ## [1.2.649] - 2026-11-02
11
24
 
12
25
  ### 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-D4_TtF2V.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-CCmZ9I6P.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
  *
@@ -12971,6 +12974,73 @@ var NodeColorIdSchema = _enum([
12971
12974
  "node-g",
12972
12975
  "node-h"
12973
12976
  ]);
12977
+ //#endregion
12978
+ //#region ../types/dist/index.mjs
12979
+ var SMTP_INBOUND_DEFAULT_PORT = 2525;
12980
+ var SMTP_INBOUND_DEFAULT_DOMAIN = "camstack.local";
12981
+ var SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES = 25 * 1024 * 1024;
12982
+ /** The text excerpt a message carries on the bus. */
12983
+ var SMTP_INBOUND_TEXT_MAX_CHARS = 4096;
12984
+ var PortSchema = number().int().min(1).max(65535);
12985
+ /** A recipient local-part: what stands before the `@`. */
12986
+ var SmtpRecipientSchema = string().min(1).max(64).regex(/^[A-Za-z0-9._+-]+$/);
12987
+ object({
12988
+ port: PortSchema.default(SMTP_INBOUND_DEFAULT_PORT),
12989
+ bindHost: string().min(1).default("0.0.0.0"),
12990
+ domain: string().min(1).default(SMTP_INBOUND_DEFAULT_DOMAIN),
12991
+ requireAuth: boolean().default(true),
12992
+ username: string().default(""),
12993
+ password: string().default("").meta({ secret: true }),
12994
+ starttls: boolean().default(false),
12995
+ maxMessageBytes: number().int().positive().default(SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES)
12996
+ });
12997
+ object({
12998
+ host: string().min(1).nullable(),
12999
+ hostReason: string().nullable(),
13000
+ port: PortSchema,
13001
+ domain: string().min(1),
13002
+ requireAuth: boolean(),
13003
+ username: string(),
13004
+ password: string(),
13005
+ starttls: boolean()
13006
+ });
13007
+ object({
13008
+ recipients: array(SmtpRecipientSchema).min(1),
13009
+ /**
13010
+ * Who is subscribing. A second `subscribe` with the same id REPLACES that subscriber's
13011
+ * recipients (the way a consumer that restarted, or lost a device, declares its set again);
13012
+ * a recipient held by another subscriber is refused with `smtp-inbound/recipient-claimed`.
13013
+ * Absent, the subscriber is anonymous and unique.
13014
+ */
13015
+ subscriberId: string().min(1).max(64).optional()
13016
+ });
13017
+ var SmtpInboundAttachmentSchema = object({
13018
+ filename: string(),
13019
+ contentType: string(),
13020
+ bytes: number().int().nonnegative()
13021
+ });
13022
+ /** The payload of one `broker.message` (the key is the recipient local-part). */
13023
+ var SmtpInboundMessageSchema = object({
13024
+ /** Monotonic per mailbox; a gap tells a consumer it missed an event. */
13025
+ seq: number().int().positive(),
13026
+ messageId: string(),
13027
+ receivedAt: number().int().nonnegative(),
13028
+ from: string(),
13029
+ to: string(),
13030
+ subject: string(),
13031
+ text: string().max(SMTP_INBOUND_TEXT_MAX_CHARS),
13032
+ attachments: array(SmtpInboundAttachmentSchema)
13033
+ });
13034
+ object({
13035
+ /** The newest seq issued so far (0 before any mail). */
13036
+ lastSeq: number().int().nonnegative(),
13037
+ /** The oldest seq still retained; messages below it are gone. */
13038
+ oldestSeq: number().int().nonnegative(),
13039
+ messages: array(object({
13040
+ key: string(),
13041
+ message: SmtpInboundMessageSchema
13042
+ }))
13043
+ });
12974
13044
  new Set([
12975
13045
  "track",
12976
13046
  "face",
@@ -55740,15 +55810,9 @@ var AvailableIntegrationTypeSchema = object({
55740
55810
  * branch on CAP — never on addon name. `device-adoption` integrations
55741
55811
  * (Home Assistant, …) route through the broker step (Approach A);
55742
55812
  * `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.
55813
+ * legacy config → discovery flow.
55746
55814
  */
55747
- kind: _enum([
55748
- "device-adoption",
55749
- "device-provider",
55750
- "connection"
55751
- ]),
55815
+ kind: _enum(["device-adoption", "device-provider"]),
55752
55816
  /**
55753
55817
  * For `device-adoption` addons: the broker kind to create/link
55754
55818
  * (e.g. `home-assistant`), declared in the addon manifest. `null` for
@@ -55862,18 +55926,17 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
55862
55926
  });
55863
55927
  /**
55864
55928
  * `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).
55929
+ * (an account, a relay, a destination) as typed records (D985, D986). The addon's
55930
+ * own function page reads and writes them (Logging for Loki); the Integrations
55931
+ * page lists only the integrations that bring devices (D996).
55867
55932
  *
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.
55933
+ * A collection of providers: each addon that holds connections registers one and a
55934
+ * caller reaches a provider with the pinned `addonId`.
55872
55935
  *
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.
55936
+ * Identity: the instance id is immutable and opaque to the caller. A record the
55937
+ * Loki migration made keeps the `integrations` registry id it was created under
55938
+ * (`int_NNNN`); a single instance made by its function page takes the addon id.
55939
+ * `name` is the editable label.
55877
55940
  *
55878
55941
  * Secrets: every field the instance kind marks `.meta({ secret: true })` reaches
55879
55942
  * the form as a `SecretFieldView`, never text (D789). A patch value omitted, `''`
@@ -55904,6 +55967,11 @@ var IntegrationInstanceSchema = object({
55904
55967
  name: string(),
55905
55968
  /** The addon's own rule (an URL is present, a client id is present...). */
55906
55969
  configured: boolean(),
55970
+ /**
55971
+ * The instance's own on/off switch (D996): always present, derived from the record body
55972
+ * (`enabled !== false`, so a row written before the field existed is on).
55973
+ */
55974
+ enabled: boolean(),
55907
55975
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
55908
55976
  formValues: record(string(), unknown()),
55909
55977
  status: IntegrationInstanceStatusSchema.optional()
@@ -55914,13 +55982,31 @@ var IntegrationInstancePatchSchema = object({
55914
55982
  /** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
55915
55983
  values: record(string(), unknown())
55916
55984
  });
55917
- method(_void(), IntegrationInstanceDescriptionSchema), method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3), method(_void(), array(IntegrationInstanceSchema).readonly()), method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable()), method(object({
55985
+ 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
55986
  id: IntegrationInstanceIdSchema,
55919
55987
  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({
55988
+ }), object({ id: string() }), {
55989
+ kind: "mutation",
55990
+ auth: "admin"
55991
+ }), method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
55992
+ kind: "mutation",
55993
+ auth: "admin"
55994
+ }), method(object({
55995
+ id: IntegrationInstanceIdSchema,
55996
+ enabled: boolean()
55997
+ }), object({ ok: literal(true) }), {
55998
+ kind: "mutation",
55999
+ auth: "admin"
56000
+ }), method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
56001
+ kind: "mutation",
56002
+ auth: "admin"
56003
+ }), optionalMethod(object({
55921
56004
  id: IntegrationInstanceIdSchema.nullable(),
55922
56005
  values: record(string(), unknown())
55923
- }), TestConnectionResultSchema$1, { kind: "mutation" });
56006
+ }), TestConnectionResultSchema$1, {
56007
+ kind: "mutation",
56008
+ auth: "admin"
56009
+ });
55924
56010
  /**
55925
56011
  * Two-way audio / talk-back cap. Clients open a WebRTC audio-only
55926
56012
  * session (server-initiated offer pattern, identical to the main
@@ -66689,7 +66775,14 @@ var NcMediaPolicySchema = object({
66689
66775
  */
66690
66776
  profile: CamProfileSchema.optional(),
66691
66777
  /** See {@link NcLinkedCameraModeSchema}. */
66692
- linkedCameras: NcLinkedCameraModeSchema.optional()
66778
+ linkedCameras: NcLinkedCameraModeSchema.optional(),
66779
+ /**
66780
+ * Sensor rules only: may this rule wake a SLEEPING battery camera to take
66781
+ * the linked still it attaches (D998)? Absent means yes, an explicit `false`
66782
+ * opts out. Deliberately not a Zod default: a rule that never chose keeps
66783
+ * following the hub's. One bounded wake per event; nothing is attached later.
66784
+ */
66785
+ wakeSleepingCamera: boolean().optional()
66693
66786
  });
66694
66787
  /**
66695
66788
  * Cooldown GRANULARITY over the subject's class — how much a fired
@@ -75270,7 +75363,16 @@ var smokeCapability = {
75270
75363
  * future fields without breaking the existing call sites.
75271
75364
  */
75272
75365
  var EmailAddressSchema = email();
75366
+ /**
75367
+ * Which relay of a multi-relay provider answers (D996). Absent = the provider's first / default
75368
+ * relay, so a caller written before relays existed keeps working unchanged. A provider with one
75369
+ * fixed relay ignores it. This is the integration instance id (`int_NNNN`), NOT the addon id: the
75370
+ * collection pin `addonId` still picks the provider, `instanceId` picks the relay inside it.
75371
+ */
75372
+ var InstanceIdSchema = string().min(1).optional();
75373
+ var RelayPinSchema = object({ instanceId: InstanceIdSchema });
75273
75374
  var SendEmailInputSchema = object({
75375
+ instanceId: InstanceIdSchema,
75274
75376
  to: union([EmailAddressSchema, array(EmailAddressSchema).min(1)]),
75275
75377
  cc: array(EmailAddressSchema).optional(),
75276
75378
  bcc: array(EmailAddressSchema).optional(),
@@ -75306,11 +75408,11 @@ method(SendEmailInputSchema, SendEmailResultSchema, {
75306
75408
  kind: "mutation",
75307
75409
  auth: "admin",
75308
75410
  access: "create"
75309
- }), method(_void(), SmtpStatusSchema, {
75411
+ }), method(RelayPinSchema, SmtpStatusSchema, {
75310
75412
  kind: "mutation",
75311
75413
  auth: "admin",
75312
75414
  access: "view"
75313
- }), method(_void(), SmtpStatusSchema, { auth: "admin" });
75415
+ }), method(RelayPinSchema, SmtpStatusSchema, { auth: "admin" });
75314
75416
  /**
75315
75417
  * A single device snapshot returned as base64 JPEG/PNG.
75316
75418
  *
@@ -75342,10 +75444,12 @@ DeviceType.Camera, method(object({
75342
75444
  * even when the cache is well within the device's
75343
75445
  * `snapshotMaxAgeS` window.
75344
75446
  *
75345
- * **`force` is an OPERATOR signal, not a freshness preference.** On a
75447
+ * **`force` is a demand with a named principal, not a freshness
75448
+ * preference** — an operator button, the scheduled refresh (D847), or a
75449
+ * notification rule that left the camera's wake allowed (D998). On a
75346
75450
  * 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
75451
+ * sleep gate and wakes the camera, so a poller, a thumbnail or a
75452
+ * prestream must NEVER set it. Every such caller
75349
75453
  * gets the cached frame, which on a sleeping battery camera is the
75350
75454
  * correct answer: stale but honest beats woken.
75351
75455
  */
@@ -80195,7 +80299,7 @@ function runningFromMediaPlayer(state, previous, fetchedAt, transitionAt) {
80195
80299
  if (state === "paused") return nativeSlice(false, "paused", previous, transitionAtMs, fetchedAt, null);
80196
80300
  return nativeSlice(false, state, previous, transitionAtMs, fetchedAt, null);
80197
80301
  }
80198
- DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
80302
+ DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
80199
80303
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
80200
80304
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
80201
80305
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
@@ -85420,6 +85524,13 @@ Object.freeze({
85420
85524
  access: "view",
85421
85525
  role: "admin"
85422
85526
  },
85527
+ "integrationInstances.setInstanceEnabled": {
85528
+ capName: "integration-instances",
85529
+ capScope: "system",
85530
+ addonId: null,
85531
+ access: "create",
85532
+ role: "admin"
85533
+ },
85423
85534
  "integrationInstances.testInstance": {
85424
85535
  capName: "integration-instances",
85425
85536
  capScope: "system",
@@ -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
  *
@@ -12972,6 +12975,73 @@ var NodeColorIdSchema = _enum([
12972
12975
  "node-g",
12973
12976
  "node-h"
12974
12977
  ]);
12978
+ //#endregion
12979
+ //#region ../types/dist/index.mjs
12980
+ var SMTP_INBOUND_DEFAULT_PORT = 2525;
12981
+ var SMTP_INBOUND_DEFAULT_DOMAIN = "camstack.local";
12982
+ var SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES = 25 * 1024 * 1024;
12983
+ /** The text excerpt a message carries on the bus. */
12984
+ var SMTP_INBOUND_TEXT_MAX_CHARS = 4096;
12985
+ var PortSchema = number().int().min(1).max(65535);
12986
+ /** A recipient local-part: what stands before the `@`. */
12987
+ var SmtpRecipientSchema = string().min(1).max(64).regex(/^[A-Za-z0-9._+-]+$/);
12988
+ object({
12989
+ port: PortSchema.default(SMTP_INBOUND_DEFAULT_PORT),
12990
+ bindHost: string().min(1).default("0.0.0.0"),
12991
+ domain: string().min(1).default(SMTP_INBOUND_DEFAULT_DOMAIN),
12992
+ requireAuth: boolean().default(true),
12993
+ username: string().default(""),
12994
+ password: string().default("").meta({ secret: true }),
12995
+ starttls: boolean().default(false),
12996
+ maxMessageBytes: number().int().positive().default(SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES)
12997
+ });
12998
+ object({
12999
+ host: string().min(1).nullable(),
13000
+ hostReason: string().nullable(),
13001
+ port: PortSchema,
13002
+ domain: string().min(1),
13003
+ requireAuth: boolean(),
13004
+ username: string(),
13005
+ password: string(),
13006
+ starttls: boolean()
13007
+ });
13008
+ object({
13009
+ recipients: array(SmtpRecipientSchema).min(1),
13010
+ /**
13011
+ * Who is subscribing. A second `subscribe` with the same id REPLACES that subscriber's
13012
+ * recipients (the way a consumer that restarted, or lost a device, declares its set again);
13013
+ * a recipient held by another subscriber is refused with `smtp-inbound/recipient-claimed`.
13014
+ * Absent, the subscriber is anonymous and unique.
13015
+ */
13016
+ subscriberId: string().min(1).max(64).optional()
13017
+ });
13018
+ var SmtpInboundAttachmentSchema = object({
13019
+ filename: string(),
13020
+ contentType: string(),
13021
+ bytes: number().int().nonnegative()
13022
+ });
13023
+ /** The payload of one `broker.message` (the key is the recipient local-part). */
13024
+ var SmtpInboundMessageSchema = object({
13025
+ /** Monotonic per mailbox; a gap tells a consumer it missed an event. */
13026
+ seq: number().int().positive(),
13027
+ messageId: string(),
13028
+ receivedAt: number().int().nonnegative(),
13029
+ from: string(),
13030
+ to: string(),
13031
+ subject: string(),
13032
+ text: string().max(SMTP_INBOUND_TEXT_MAX_CHARS),
13033
+ attachments: array(SmtpInboundAttachmentSchema)
13034
+ });
13035
+ object({
13036
+ /** The newest seq issued so far (0 before any mail). */
13037
+ lastSeq: number().int().nonnegative(),
13038
+ /** The oldest seq still retained; messages below it are gone. */
13039
+ oldestSeq: number().int().nonnegative(),
13040
+ messages: array(object({
13041
+ key: string(),
13042
+ message: SmtpInboundMessageSchema
13043
+ }))
13044
+ });
12975
13045
  new Set([
12976
13046
  "track",
12977
13047
  "face",
@@ -55741,15 +55811,9 @@ var AvailableIntegrationTypeSchema = object({
55741
55811
  * branch on CAP — never on addon name. `device-adoption` integrations
55742
55812
  * (Home Assistant, …) route through the broker step (Approach A);
55743
55813
  * `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.
55814
+ * legacy config → discovery flow.
55747
55815
  */
55748
- kind: _enum([
55749
- "device-adoption",
55750
- "device-provider",
55751
- "connection"
55752
- ]),
55816
+ kind: _enum(["device-adoption", "device-provider"]),
55753
55817
  /**
55754
55818
  * For `device-adoption` addons: the broker kind to create/link
55755
55819
  * (e.g. `home-assistant`), declared in the addon manifest. `null` for
@@ -55863,18 +55927,17 @@ method(_void(), array(IntegrationWithStateSchema)), method(object({ id: string()
55863
55927
  });
55864
55928
  /**
55865
55929
  * `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).
55930
+ * (an account, a relay, a destination) as typed records (D985, D986). The addon's
55931
+ * own function page reads and writes them (Logging for Loki); the Integrations
55932
+ * page lists only the integrations that bring devices (D996).
55868
55933
  *
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.
55934
+ * A collection of providers: each addon that holds connections registers one and a
55935
+ * caller reaches a provider with the pinned `addonId`.
55873
55936
  *
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.
55937
+ * Identity: the instance id is immutable and opaque to the caller. A record the
55938
+ * Loki migration made keeps the `integrations` registry id it was created under
55939
+ * (`int_NNNN`); a single instance made by its function page takes the addon id.
55940
+ * `name` is the editable label.
55878
55941
  *
55879
55942
  * Secrets: every field the instance kind marks `.meta({ secret: true })` reaches
55880
55943
  * the form as a `SecretFieldView`, never text (D789). A patch value omitted, `''`
@@ -55905,6 +55968,11 @@ var IntegrationInstanceSchema = object({
55905
55968
  name: string(),
55906
55969
  /** The addon's own rule (an URL is present, a client id is present...). */
55907
55970
  configured: boolean(),
55971
+ /**
55972
+ * The instance's own on/off switch (D996): always present, derived from the record body
55973
+ * (`enabled !== false`, so a row written before the field existed is on).
55974
+ */
55975
+ enabled: boolean(),
55908
55976
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
55909
55977
  formValues: record(string(), unknown()),
55910
55978
  status: IntegrationInstanceStatusSchema.optional()
@@ -55915,13 +55983,31 @@ var IntegrationInstancePatchSchema = object({
55915
55983
  /** Omitted / '' / redaction token keeps a secret; `{ $secretClear: true }` clears it. */
55916
55984
  values: record(string(), unknown())
55917
55985
  });
55918
- method(_void(), IntegrationInstanceDescriptionSchema), method(object({ id: IntegrationInstanceIdSchema.nullable() }), ConfigSchemaPassthrough$3), method(_void(), array(IntegrationInstanceSchema).readonly()), method(object({ id: IntegrationInstanceIdSchema }), IntegrationInstanceSchema.nullable()), method(object({
55986
+ 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
55987
  id: IntegrationInstanceIdSchema,
55920
55988
  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({
55989
+ }), object({ id: string() }), {
55990
+ kind: "mutation",
55991
+ auth: "admin"
55992
+ }), method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
55993
+ kind: "mutation",
55994
+ auth: "admin"
55995
+ }), method(object({
55996
+ id: IntegrationInstanceIdSchema,
55997
+ enabled: boolean()
55998
+ }), object({ ok: literal(true) }), {
55999
+ kind: "mutation",
56000
+ auth: "admin"
56001
+ }), method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
56002
+ kind: "mutation",
56003
+ auth: "admin"
56004
+ }), optionalMethod(object({
55922
56005
  id: IntegrationInstanceIdSchema.nullable(),
55923
56006
  values: record(string(), unknown())
55924
- }), TestConnectionResultSchema$1, { kind: "mutation" });
56007
+ }), TestConnectionResultSchema$1, {
56008
+ kind: "mutation",
56009
+ auth: "admin"
56010
+ });
55925
56011
  /**
55926
56012
  * Two-way audio / talk-back cap. Clients open a WebRTC audio-only
55927
56013
  * session (server-initiated offer pattern, identical to the main
@@ -66690,7 +66776,14 @@ var NcMediaPolicySchema = object({
66690
66776
  */
66691
66777
  profile: CamProfileSchema.optional(),
66692
66778
  /** See {@link NcLinkedCameraModeSchema}. */
66693
- linkedCameras: NcLinkedCameraModeSchema.optional()
66779
+ linkedCameras: NcLinkedCameraModeSchema.optional(),
66780
+ /**
66781
+ * Sensor rules only: may this rule wake a SLEEPING battery camera to take
66782
+ * the linked still it attaches (D998)? Absent means yes, an explicit `false`
66783
+ * opts out. Deliberately not a Zod default: a rule that never chose keeps
66784
+ * following the hub's. One bounded wake per event; nothing is attached later.
66785
+ */
66786
+ wakeSleepingCamera: boolean().optional()
66694
66787
  });
66695
66788
  /**
66696
66789
  * Cooldown GRANULARITY over the subject's class — how much a fired
@@ -75271,7 +75364,16 @@ var smokeCapability = {
75271
75364
  * future fields without breaking the existing call sites.
75272
75365
  */
75273
75366
  var EmailAddressSchema = email();
75367
+ /**
75368
+ * Which relay of a multi-relay provider answers (D996). Absent = the provider's first / default
75369
+ * relay, so a caller written before relays existed keeps working unchanged. A provider with one
75370
+ * fixed relay ignores it. This is the integration instance id (`int_NNNN`), NOT the addon id: the
75371
+ * collection pin `addonId` still picks the provider, `instanceId` picks the relay inside it.
75372
+ */
75373
+ var InstanceIdSchema = string().min(1).optional();
75374
+ var RelayPinSchema = object({ instanceId: InstanceIdSchema });
75274
75375
  var SendEmailInputSchema = object({
75376
+ instanceId: InstanceIdSchema,
75275
75377
  to: union([EmailAddressSchema, array(EmailAddressSchema).min(1)]),
75276
75378
  cc: array(EmailAddressSchema).optional(),
75277
75379
  bcc: array(EmailAddressSchema).optional(),
@@ -75307,11 +75409,11 @@ method(SendEmailInputSchema, SendEmailResultSchema, {
75307
75409
  kind: "mutation",
75308
75410
  auth: "admin",
75309
75411
  access: "create"
75310
- }), method(_void(), SmtpStatusSchema, {
75412
+ }), method(RelayPinSchema, SmtpStatusSchema, {
75311
75413
  kind: "mutation",
75312
75414
  auth: "admin",
75313
75415
  access: "view"
75314
- }), method(_void(), SmtpStatusSchema, { auth: "admin" });
75416
+ }), method(RelayPinSchema, SmtpStatusSchema, { auth: "admin" });
75315
75417
  /**
75316
75418
  * A single device snapshot returned as base64 JPEG/PNG.
75317
75419
  *
@@ -75343,10 +75445,12 @@ DeviceType.Camera, method(object({
75343
75445
  * even when the cache is well within the device's
75344
75446
  * `snapshotMaxAgeS` window.
75345
75447
  *
75346
- * **`force` is an OPERATOR signal, not a freshness preference.** On a
75448
+ * **`force` is a demand with a named principal, not a freshness
75449
+ * preference** — an operator button, the scheduled refresh (D847), or a
75450
+ * notification rule that left the camera's wake allowed (D998). On a
75347
75451
  * 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
75452
+ * sleep gate and wakes the camera, so a poller, a thumbnail or a
75453
+ * prestream must NEVER set it. Every such caller
75350
75454
  * gets the cached frame, which on a sleeping battery camera is the
75351
75455
  * correct answer: stale but honest beats woken.
75352
75456
  */
@@ -80196,7 +80300,7 @@ function runningFromMediaPlayer(state, previous, fetchedAt, transitionAt) {
80196
80300
  if (state === "paused") return nativeSlice(false, "paused", previous, transitionAtMs, fetchedAt, null);
80197
80301
  return nativeSlice(false, state, previous, transitionAtMs, fetchedAt, null);
80198
80302
  }
80199
- DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
80303
+ DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
80200
80304
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
80201
80305
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
80202
80306
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
@@ -85421,6 +85525,13 @@ Object.freeze({
85421
85525
  access: "view",
85422
85526
  role: "admin"
85423
85527
  },
85528
+ "integrationInstances.setInstanceEnabled": {
85529
+ capName: "integration-instances",
85530
+ capScope: "system",
85531
+ addonId: null,
85532
+ access: "create",
85533
+ role: "admin"
85534
+ },
85424
85535
  "integrationInstances.testInstance": {
85425
85536
  capName: "integration-instances",
85426
85537
  capScope: "system",