@camstack/addon-notifiers 1.2.627 → 1.2.628

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,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.628] - 2026-11-02
11
+
12
+ ### Changed
13
+
14
+ - An email target's relay is now a picker of the chosen provider's relays by name, with 'Default relay' as the empty choice; a relay that no longer exists shows as 'missing relay <id>'.
15
+
10
16
  ## [1.2.627] - 2026-11-02
11
17
 
12
18
  ### Changed
package/dist/addon.js CHANGED
@@ -815,7 +815,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
815
815
  return EventCategory;
816
816
  }({});
817
817
  //#endregion
818
- //#region ../types/dist/device-type-BOWLtoOt.mjs
818
+ //#region ../types/dist/device-type-CfHw_quR.mjs
819
819
  /**
820
820
  import { errMsg } from '@camstack/types'
821
821
  * Extract a human-readable message from an unknown error value.
@@ -1004,6 +1004,9 @@ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
1004
1004
  DeviceFeature["CoverPositionable"] = "cover-positionable";
1005
1005
  /** Cover exposes slat-tilt control. Gates the tilt slider UI. */
1006
1006
  DeviceFeature["CoverTilt"] = "cover-tilt";
1007
+ /** Cover has a real stop: `stop` halts the travel in place. Gates the stop
1008
+ * control in exporters; a provider whose `stop` is a no-op never declares it. */
1009
+ DeviceFeature["CoverStoppable"] = "cover-stoppable";
1007
1010
  /** Valve exposes intermediate position control (0..100). Gates the
1008
1011
  * position slider / drag surface UI. */
1009
1012
  DeviceFeature["ValvePositionable"] = "valve-positionable";
@@ -6002,7 +6005,7 @@ var ZodIssueCode = {
6002
6005
  var ZodFirstPartyTypeKind;
6003
6006
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
6004
6007
  //#endregion
6005
- //#region ../types/dist/sleep-CdJHPm-k.mjs
6008
+ //#region ../types/dist/sleep-QsoHs66J.mjs
6006
6009
  /**
6007
6010
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6008
6011
  * window to float samples (D455).
@@ -11669,7 +11672,7 @@ function userError(code, params, englishFallback) {
11669
11672
  return new CamstackError(code, params, englishFallback);
11670
11673
  }
11671
11674
  //#endregion
11672
- //#region ../types/dist/composition-BDUoQsw9.mjs
11675
+ //#region ../types/dist/composition-Dh3RfMx5.mjs
11673
11676
  /**
11674
11677
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11675
11678
  *
@@ -12883,6 +12886,73 @@ var NodeColorIdSchema = _enum([
12883
12886
  "node-g",
12884
12887
  "node-h"
12885
12888
  ]);
12889
+ //#endregion
12890
+ //#region ../types/dist/index.mjs
12891
+ var SMTP_INBOUND_DEFAULT_PORT = 2525;
12892
+ var SMTP_INBOUND_DEFAULT_DOMAIN = "camstack.local";
12893
+ var SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES = 25 * 1024 * 1024;
12894
+ /** The text excerpt a message carries on the bus. */
12895
+ var SMTP_INBOUND_TEXT_MAX_CHARS = 4096;
12896
+ var PortSchema = number().int().min(1).max(65535);
12897
+ /** A recipient local-part: what stands before the `@`. */
12898
+ var SmtpRecipientSchema = string().min(1).max(64).regex(/^[A-Za-z0-9._+-]+$/);
12899
+ object({
12900
+ port: PortSchema.default(SMTP_INBOUND_DEFAULT_PORT),
12901
+ bindHost: string().min(1).default("0.0.0.0"),
12902
+ domain: string().min(1).default(SMTP_INBOUND_DEFAULT_DOMAIN),
12903
+ requireAuth: boolean().default(true),
12904
+ username: string().default(""),
12905
+ password: string().default("").meta({ secret: true }),
12906
+ starttls: boolean().default(false),
12907
+ maxMessageBytes: number().int().positive().default(SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES)
12908
+ });
12909
+ object({
12910
+ host: string().min(1).nullable(),
12911
+ hostReason: string().nullable(),
12912
+ port: PortSchema,
12913
+ domain: string().min(1),
12914
+ requireAuth: boolean(),
12915
+ username: string(),
12916
+ password: string(),
12917
+ starttls: boolean()
12918
+ });
12919
+ object({
12920
+ recipients: array(SmtpRecipientSchema).min(1),
12921
+ /**
12922
+ * Who is subscribing. A second `subscribe` with the same id REPLACES that subscriber's
12923
+ * recipients (the way a consumer that restarted, or lost a device, declares its set again);
12924
+ * a recipient held by another subscriber is refused with `smtp-inbound/recipient-claimed`.
12925
+ * Absent, the subscriber is anonymous and unique.
12926
+ */
12927
+ subscriberId: string().min(1).max(64).optional()
12928
+ });
12929
+ var SmtpInboundAttachmentSchema = object({
12930
+ filename: string(),
12931
+ contentType: string(),
12932
+ bytes: number().int().nonnegative()
12933
+ });
12934
+ /** The payload of one `broker.message` (the key is the recipient local-part). */
12935
+ var SmtpInboundMessageSchema = object({
12936
+ /** Monotonic per mailbox; a gap tells a consumer it missed an event. */
12937
+ seq: number().int().positive(),
12938
+ messageId: string(),
12939
+ receivedAt: number().int().nonnegative(),
12940
+ from: string(),
12941
+ to: string(),
12942
+ subject: string(),
12943
+ text: string().max(SMTP_INBOUND_TEXT_MAX_CHARS),
12944
+ attachments: array(SmtpInboundAttachmentSchema)
12945
+ });
12946
+ object({
12947
+ /** The newest seq issued so far (0 before any mail). */
12948
+ lastSeq: number().int().nonnegative(),
12949
+ /** The oldest seq still retained; messages below it are gone. */
12950
+ oldestSeq: number().int().nonnegative(),
12951
+ messages: array(object({
12952
+ key: string(),
12953
+ message: SmtpInboundMessageSchema
12954
+ }))
12955
+ });
12886
12956
  new Set([
12887
12957
  "track",
12888
12958
  "face",
@@ -53256,6 +53326,11 @@ var IntegrationInstanceSchema = object({
53256
53326
  name: string(),
53257
53327
  /** The addon's own rule (an URL is present, a client id is present...). */
53258
53328
  configured: boolean(),
53329
+ /**
53330
+ * The instance's own on/off switch (D996): always present, derived from the record body
53331
+ * (`enabled !== false`, so a row written before the field existed is on).
53332
+ */
53333
+ enabled: boolean(),
53259
53334
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
53260
53335
  formValues: record(string(), unknown()),
53261
53336
  status: IntegrationInstanceStatusSchema.optional()
@@ -53275,6 +53350,12 @@ method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }), method
53275
53350
  }), method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
53276
53351
  kind: "mutation",
53277
53352
  auth: "admin"
53353
+ }), method(object({
53354
+ id: IntegrationInstanceIdSchema,
53355
+ enabled: boolean()
53356
+ }), object({ ok: literal(true) }), {
53357
+ kind: "mutation",
53358
+ auth: "admin"
53278
53359
  }), method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
53279
53360
  kind: "mutation",
53280
53361
  auth: "admin"
@@ -63318,7 +63399,14 @@ var NcMediaPolicySchema = object({
63318
63399
  */
63319
63400
  profile: CamProfileSchema.optional(),
63320
63401
  /** See {@link NcLinkedCameraModeSchema}. */
63321
- linkedCameras: NcLinkedCameraModeSchema.optional()
63402
+ linkedCameras: NcLinkedCameraModeSchema.optional(),
63403
+ /**
63404
+ * Sensor rules only: may this rule wake a SLEEPING battery camera to take
63405
+ * the linked still it attaches (D998)? Absent means yes, an explicit `false`
63406
+ * opts out. Deliberately not a Zod default: a rule that never chose keeps
63407
+ * following the hub's. One bounded wake per event; nothing is attached later.
63408
+ */
63409
+ wakeSleepingCamera: boolean().optional()
63322
63410
  });
63323
63411
  /**
63324
63412
  * Cooldown GRANULARITY over the subject's class — how much a fired
@@ -71067,10 +71155,12 @@ DeviceType.Camera, method(object({
71067
71155
  * even when the cache is well within the device's
71068
71156
  * `snapshotMaxAgeS` window.
71069
71157
  *
71070
- * **`force` is an OPERATOR signal, not a freshness preference.** On a
71158
+ * **`force` is a demand with a named principal, not a freshness
71159
+ * preference** — an operator button, the scheduled refresh (D847), or a
71160
+ * notification rule that left the camera's wake allowed (D998). On a
71071
71161
  * battery camera it is the one thing that walks past the wrapper's
71072
- * sleep gate and wakes the camera, so a background caller — a poller,
71073
- * an event handler, a thumbnail — must NEVER set it. Every such caller
71162
+ * sleep gate and wakes the camera, so a poller, a thumbnail or a
71163
+ * prestream must NEVER set it. Every such caller
71074
71164
  * gets the cached frame, which on a sleeping battery camera is the
71075
71165
  * correct answer: stale but honest beats woken.
71076
71166
  */
@@ -75028,7 +75118,7 @@ discriminatedUnion("kind", [object({
75028
75118
  kind: literal("pronto"),
75029
75119
  code: string()
75030
75120
  })]);
75031
- DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
75121
+ DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
75032
75122
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
75033
75123
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
75034
75124
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
@@ -78690,6 +78780,13 @@ Object.freeze({
78690
78780
  access: "view",
78691
78781
  role: "admin"
78692
78782
  },
78783
+ "integrationInstances.setInstanceEnabled": {
78784
+ capName: "integration-instances",
78785
+ capScope: "system",
78786
+ addonId: null,
78787
+ access: "create",
78788
+ role: "admin"
78789
+ },
78693
78790
  "integrationInstances.testInstance": {
78694
78791
  capName: "integration-instances",
78695
78792
  capScope: "system",
@@ -90510,12 +90607,19 @@ function buildEmailDescriptor(providers = []) {
90510
90607
  options
90511
90608
  },
90512
90609
  {
90513
- type: "text",
90610
+ type: "select",
90514
90611
  key: "smtpInstanceId",
90515
90612
  label: "Relay (optional)",
90516
90613
  labelKey: "notifiers:emailDescriptor.s_email.smtpInstanceId.label",
90517
- description: "Pin this target to one relay of the provider (its relay id, e.g. relay-1a2b3c4d). Leave empty to use the first configured relay.",
90518
- descriptionKey: "notifiers:emailDescriptor.s_email.smtpInstanceId.description"
90614
+ description: "Pin this target to one relay of the provider. Leave on the default relay to use the first configured one.",
90615
+ descriptionKey: "notifiers:emailDescriptor.s_email.smtpInstanceId.description",
90616
+ options: [],
90617
+ discoverOptions: {
90618
+ kind: EMAIL_KIND,
90619
+ dependsOn: ["smtpAddonId"],
90620
+ noneLabel: "Default relay",
90621
+ missingLabel: "missing relay"
90622
+ }
90519
90623
  },
90520
90624
  {
90521
90625
  type: "text",
@@ -90588,6 +90692,15 @@ function createEmailAdapter(smtp) {
90588
90692
  "smtpAddonId",
90589
90693
  "smtpInstanceId"
90590
90694
  ],
90695
+ async discover(config) {
90696
+ const addonId = config["smtpAddonId"];
90697
+ if (typeof addonId !== "string" || addonId.trim() === "" || smtp.listRelays === void 0) return [];
90698
+ return (await smtp.listRelays(addonId.trim())).map((relay) => ({
90699
+ kind: EMAIL_KIND,
90700
+ suggestedName: relay.name.length > 0 ? relay.name : relay.id,
90701
+ config: { smtpInstanceId: relay.id }
90702
+ }));
90703
+ },
90591
90704
  async sendDirect(config, prepared) {
90592
90705
  try {
90593
90706
  const to = requireTo(config);
@@ -94078,6 +94191,13 @@ var NotifiersAddon = class extends BaseAddon {
94078
94191
  })).messageId };
94079
94192
  },
94080
94193
  listProviders,
94194
+ listRelays: async (addonId) => {
94195
+ if (!(await api.addons.listCapabilityProviders.query({ capName: "integration-instances" })).some((holder) => holder.addonId === addonId)) return [];
94196
+ return (await api.integrationInstances.listInstances.query({ addonId })).map((instance) => ({
94197
+ id: instance.id,
94198
+ name: instance.name
94199
+ }));
94200
+ },
94081
94201
  resolveDefaultAddonId: async () => {
94082
94202
  const providers = await listProviders();
94083
94203
  return providers.find((p) => p.ready === true)?.value ?? providers[0]?.value;
package/dist/addon.mjs CHANGED
@@ -788,7 +788,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
788
788
  return EventCategory;
789
789
  }({});
790
790
  //#endregion
791
- //#region ../types/dist/device-type-BOWLtoOt.mjs
791
+ //#region ../types/dist/device-type-CfHw_quR.mjs
792
792
  /**
793
793
  import { errMsg } from '@camstack/types'
794
794
  * Extract a human-readable message from an unknown error value.
@@ -977,6 +977,9 @@ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
977
977
  DeviceFeature["CoverPositionable"] = "cover-positionable";
978
978
  /** Cover exposes slat-tilt control. Gates the tilt slider UI. */
979
979
  DeviceFeature["CoverTilt"] = "cover-tilt";
980
+ /** Cover has a real stop: `stop` halts the travel in place. Gates the stop
981
+ * control in exporters; a provider whose `stop` is a no-op never declares it. */
982
+ DeviceFeature["CoverStoppable"] = "cover-stoppable";
980
983
  /** Valve exposes intermediate position control (0..100). Gates the
981
984
  * position slider / drag surface UI. */
982
985
  DeviceFeature["ValvePositionable"] = "valve-positionable";
@@ -5975,7 +5978,7 @@ var ZodIssueCode = {
5975
5978
  var ZodFirstPartyTypeKind;
5976
5979
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5977
5980
  //#endregion
5978
- //#region ../types/dist/sleep-CdJHPm-k.mjs
5981
+ //#region ../types/dist/sleep-QsoHs66J.mjs
5979
5982
  /**
5980
5983
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5981
5984
  * window to float samples (D455).
@@ -11642,7 +11645,7 @@ function userError(code, params, englishFallback) {
11642
11645
  return new CamstackError(code, params, englishFallback);
11643
11646
  }
11644
11647
  //#endregion
11645
- //#region ../types/dist/composition-BDUoQsw9.mjs
11648
+ //#region ../types/dist/composition-Dh3RfMx5.mjs
11646
11649
  /**
11647
11650
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11648
11651
  *
@@ -12856,6 +12859,73 @@ var NodeColorIdSchema = _enum([
12856
12859
  "node-g",
12857
12860
  "node-h"
12858
12861
  ]);
12862
+ //#endregion
12863
+ //#region ../types/dist/index.mjs
12864
+ var SMTP_INBOUND_DEFAULT_PORT = 2525;
12865
+ var SMTP_INBOUND_DEFAULT_DOMAIN = "camstack.local";
12866
+ var SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES = 25 * 1024 * 1024;
12867
+ /** The text excerpt a message carries on the bus. */
12868
+ var SMTP_INBOUND_TEXT_MAX_CHARS = 4096;
12869
+ var PortSchema = number().int().min(1).max(65535);
12870
+ /** A recipient local-part: what stands before the `@`. */
12871
+ var SmtpRecipientSchema = string().min(1).max(64).regex(/^[A-Za-z0-9._+-]+$/);
12872
+ object({
12873
+ port: PortSchema.default(SMTP_INBOUND_DEFAULT_PORT),
12874
+ bindHost: string().min(1).default("0.0.0.0"),
12875
+ domain: string().min(1).default(SMTP_INBOUND_DEFAULT_DOMAIN),
12876
+ requireAuth: boolean().default(true),
12877
+ username: string().default(""),
12878
+ password: string().default("").meta({ secret: true }),
12879
+ starttls: boolean().default(false),
12880
+ maxMessageBytes: number().int().positive().default(SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES)
12881
+ });
12882
+ object({
12883
+ host: string().min(1).nullable(),
12884
+ hostReason: string().nullable(),
12885
+ port: PortSchema,
12886
+ domain: string().min(1),
12887
+ requireAuth: boolean(),
12888
+ username: string(),
12889
+ password: string(),
12890
+ starttls: boolean()
12891
+ });
12892
+ object({
12893
+ recipients: array(SmtpRecipientSchema).min(1),
12894
+ /**
12895
+ * Who is subscribing. A second `subscribe` with the same id REPLACES that subscriber's
12896
+ * recipients (the way a consumer that restarted, or lost a device, declares its set again);
12897
+ * a recipient held by another subscriber is refused with `smtp-inbound/recipient-claimed`.
12898
+ * Absent, the subscriber is anonymous and unique.
12899
+ */
12900
+ subscriberId: string().min(1).max(64).optional()
12901
+ });
12902
+ var SmtpInboundAttachmentSchema = object({
12903
+ filename: string(),
12904
+ contentType: string(),
12905
+ bytes: number().int().nonnegative()
12906
+ });
12907
+ /** The payload of one `broker.message` (the key is the recipient local-part). */
12908
+ var SmtpInboundMessageSchema = object({
12909
+ /** Monotonic per mailbox; a gap tells a consumer it missed an event. */
12910
+ seq: number().int().positive(),
12911
+ messageId: string(),
12912
+ receivedAt: number().int().nonnegative(),
12913
+ from: string(),
12914
+ to: string(),
12915
+ subject: string(),
12916
+ text: string().max(SMTP_INBOUND_TEXT_MAX_CHARS),
12917
+ attachments: array(SmtpInboundAttachmentSchema)
12918
+ });
12919
+ object({
12920
+ /** The newest seq issued so far (0 before any mail). */
12921
+ lastSeq: number().int().nonnegative(),
12922
+ /** The oldest seq still retained; messages below it are gone. */
12923
+ oldestSeq: number().int().nonnegative(),
12924
+ messages: array(object({
12925
+ key: string(),
12926
+ message: SmtpInboundMessageSchema
12927
+ }))
12928
+ });
12859
12929
  new Set([
12860
12930
  "track",
12861
12931
  "face",
@@ -53229,6 +53299,11 @@ var IntegrationInstanceSchema = object({
53229
53299
  name: string(),
53230
53300
  /** The addon's own rule (an URL is present, a client id is present...). */
53231
53301
  configured: boolean(),
53302
+ /**
53303
+ * The instance's own on/off switch (D996): always present, derived from the record body
53304
+ * (`enabled !== false`, so a row written before the field existed is on).
53305
+ */
53306
+ enabled: boolean(),
53232
53307
  /** Values for the instance form; a secret is a `SecretFieldView`, never text. */
53233
53308
  formValues: record(string(), unknown()),
53234
53309
  status: IntegrationInstanceStatusSchema.optional()
@@ -53248,6 +53323,12 @@ method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }), method
53248
53323
  }), method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
53249
53324
  kind: "mutation",
53250
53325
  auth: "admin"
53326
+ }), method(object({
53327
+ id: IntegrationInstanceIdSchema,
53328
+ enabled: boolean()
53329
+ }), object({ ok: literal(true) }), {
53330
+ kind: "mutation",
53331
+ auth: "admin"
53251
53332
  }), method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
53252
53333
  kind: "mutation",
53253
53334
  auth: "admin"
@@ -63291,7 +63372,14 @@ var NcMediaPolicySchema = object({
63291
63372
  */
63292
63373
  profile: CamProfileSchema.optional(),
63293
63374
  /** See {@link NcLinkedCameraModeSchema}. */
63294
- linkedCameras: NcLinkedCameraModeSchema.optional()
63375
+ linkedCameras: NcLinkedCameraModeSchema.optional(),
63376
+ /**
63377
+ * Sensor rules only: may this rule wake a SLEEPING battery camera to take
63378
+ * the linked still it attaches (D998)? Absent means yes, an explicit `false`
63379
+ * opts out. Deliberately not a Zod default: a rule that never chose keeps
63380
+ * following the hub's. One bounded wake per event; nothing is attached later.
63381
+ */
63382
+ wakeSleepingCamera: boolean().optional()
63295
63383
  });
63296
63384
  /**
63297
63385
  * Cooldown GRANULARITY over the subject's class — how much a fired
@@ -71040,10 +71128,12 @@ DeviceType.Camera, method(object({
71040
71128
  * even when the cache is well within the device's
71041
71129
  * `snapshotMaxAgeS` window.
71042
71130
  *
71043
- * **`force` is an OPERATOR signal, not a freshness preference.** On a
71131
+ * **`force` is a demand with a named principal, not a freshness
71132
+ * preference** — an operator button, the scheduled refresh (D847), or a
71133
+ * notification rule that left the camera's wake allowed (D998). On a
71044
71134
  * battery camera it is the one thing that walks past the wrapper's
71045
- * sleep gate and wakes the camera, so a background caller — a poller,
71046
- * an event handler, a thumbnail — must NEVER set it. Every such caller
71135
+ * sleep gate and wakes the camera, so a poller, a thumbnail or a
71136
+ * prestream must NEVER set it. Every such caller
71047
71137
  * gets the cached frame, which on a sleeping battery camera is the
71048
71138
  * correct answer: stale but honest beats woken.
71049
71139
  */
@@ -75001,7 +75091,7 @@ discriminatedUnion("kind", [object({
75001
75091
  kind: literal("pronto"),
75002
75092
  code: string()
75003
75093
  })]);
75004
- DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
75094
+ DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverStoppable, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
75005
75095
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
75006
75096
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
75007
75097
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
@@ -78663,6 +78753,13 @@ Object.freeze({
78663
78753
  access: "view",
78664
78754
  role: "admin"
78665
78755
  },
78756
+ "integrationInstances.setInstanceEnabled": {
78757
+ capName: "integration-instances",
78758
+ capScope: "system",
78759
+ addonId: null,
78760
+ access: "create",
78761
+ role: "admin"
78762
+ },
78666
78763
  "integrationInstances.testInstance": {
78667
78764
  capName: "integration-instances",
78668
78765
  capScope: "system",
@@ -90483,12 +90580,19 @@ function buildEmailDescriptor(providers = []) {
90483
90580
  options
90484
90581
  },
90485
90582
  {
90486
- type: "text",
90583
+ type: "select",
90487
90584
  key: "smtpInstanceId",
90488
90585
  label: "Relay (optional)",
90489
90586
  labelKey: "notifiers:emailDescriptor.s_email.smtpInstanceId.label",
90490
- description: "Pin this target to one relay of the provider (its relay id, e.g. relay-1a2b3c4d). Leave empty to use the first configured relay.",
90491
- descriptionKey: "notifiers:emailDescriptor.s_email.smtpInstanceId.description"
90587
+ description: "Pin this target to one relay of the provider. Leave on the default relay to use the first configured one.",
90588
+ descriptionKey: "notifiers:emailDescriptor.s_email.smtpInstanceId.description",
90589
+ options: [],
90590
+ discoverOptions: {
90591
+ kind: EMAIL_KIND,
90592
+ dependsOn: ["smtpAddonId"],
90593
+ noneLabel: "Default relay",
90594
+ missingLabel: "missing relay"
90595
+ }
90492
90596
  },
90493
90597
  {
90494
90598
  type: "text",
@@ -90561,6 +90665,15 @@ function createEmailAdapter(smtp) {
90561
90665
  "smtpAddonId",
90562
90666
  "smtpInstanceId"
90563
90667
  ],
90668
+ async discover(config) {
90669
+ const addonId = config["smtpAddonId"];
90670
+ if (typeof addonId !== "string" || addonId.trim() === "" || smtp.listRelays === void 0) return [];
90671
+ return (await smtp.listRelays(addonId.trim())).map((relay) => ({
90672
+ kind: EMAIL_KIND,
90673
+ suggestedName: relay.name.length > 0 ? relay.name : relay.id,
90674
+ config: { smtpInstanceId: relay.id }
90675
+ }));
90676
+ },
90564
90677
  async sendDirect(config, prepared) {
90565
90678
  try {
90566
90679
  const to = requireTo(config);
@@ -94051,6 +94164,13 @@ var NotifiersAddon = class extends BaseAddon {
94051
94164
  })).messageId };
94052
94165
  },
94053
94166
  listProviders,
94167
+ listRelays: async (addonId) => {
94168
+ if (!(await api.addons.listCapabilityProviders.query({ capName: "integration-instances" })).some((holder) => holder.addonId === addonId)) return [];
94169
+ return (await api.integrationInstances.listInstances.query({ addonId })).map((instance) => ({
94170
+ id: instance.id,
94171
+ name: instance.name
94172
+ }));
94173
+ },
94054
94174
  resolveDefaultAddonId: async () => {
94055
94175
  const providers = await listProviders();
94056
94176
  return providers.find((p) => p.ready === true)?.value ?? providers[0]?.value;
package/i18n/en.json CHANGED
@@ -126,7 +126,7 @@
126
126
  "s_email": {
127
127
  "smtpInstanceId": {
128
128
  "label": "Relay (optional)",
129
- "description": "Pin this target to one relay of the provider (its relay id, e.g. relay-1a2b3c4d). Leave empty to use the first configured relay."
129
+ "description": "Pin this target to one relay of the provider. Leave on the default relay to use the first configured one."
130
130
  }
131
131
  }
132
132
  }
package/i18n/it.json CHANGED
@@ -126,7 +126,7 @@
126
126
  "s_email": {
127
127
  "smtpInstanceId": {
128
128
  "label": "Relay (facoltativo)",
129
- "description": "Vincola questo target a un relay del provider (il suo id di integrazione, es. int_0003, mostrato in Integrazioni). Lascia vuoto per usare il primo relay configurato."
129
+ "description": "Fissa questo target a un relay del provider. Lascia il relay predefinito per usare il primo configurato."
130
130
  }
131
131
  }
132
132
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-notifiers",
3
- "version": "1.2.627",
3
+ "version": "1.2.628",
4
4
  "description": "System notifiers addon for CamStack — a `notification-output` collection provider hosting per-kind notifier adapters (ntfy, pushover, gotify, telegram, discord, webhook, zentik).",
5
5
  "keywords": [
6
6
  "camstack",
@@ -56,7 +56,7 @@
56
56
  }
57
57
  ],
58
58
  "requires": {
59
- "server": ">=1.2.739"
59
+ "server": ">=1.2.741"
60
60
  }
61
61
  },
62
62
  "files": [