@camstack/addon-provider-enpal 0.1.20 → 0.1.21

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,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.21] - 2026-11-14
11
+
12
+ ### Changed
13
+
14
+ - Enpal: the grid frequency shows in Hz, and the LTE RSRQ and SNR in dB
15
+ - The integration shows the official Enpal icon
16
+ - House and heat-pump meters are named apart, and the energy system reports battery health.
17
+
10
18
  ## [0.1.20] - 2026-11-13
11
19
 
12
20
  ### Changed
Binary file
package/dist/addon.js CHANGED
@@ -6066,7 +6066,7 @@ var ZodIssueCode = {
6066
6066
  var ZodFirstPartyTypeKind;
6067
6067
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
6068
6068
  //#endregion
6069
- //#region ../types/dist/sleep-Cykc2bzi.mjs
6069
+ //#region ../types/dist/sleep-D7hCDd7y.mjs
6070
6070
  /**
6071
6071
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6072
6072
  * window to float samples (D455).
@@ -10377,6 +10377,8 @@ var READING_QUANTITIES = [
10377
10377
  "information",
10378
10378
  "sound-level",
10379
10379
  "signal-level",
10380
+ "signal-ratio",
10381
+ "frequency",
10380
10382
  "colour-temperature",
10381
10383
  "angle",
10382
10384
  "bitrate",
@@ -10420,6 +10422,8 @@ var QUANTITY_BASE = {
10420
10422
  information: "B",
10421
10423
  "sound-level": "dBFS",
10422
10424
  "signal-level": "dBm",
10425
+ "signal-ratio": "dB",
10426
+ frequency: "Hz",
10423
10427
  "colour-temperature": "mired",
10424
10428
  angle: "°",
10425
10429
  bitrate: "kbps",
@@ -10772,41 +10776,57 @@ var CAP_VOCABULARY_BY_NAME = {
10772
10776
  en: "Switch",
10773
10777
  it: "Interruttore"
10774
10778
  },
10775
- actions: [{
10776
- id: "switch.on",
10777
- method: "setState",
10778
- label: {
10779
- en: "Turn on",
10780
- it: "Accendi"
10781
- },
10782
- phrase: {
10783
- en: "Turn on {device}",
10784
- it: "Accendi {device}"
10785
- },
10786
- icon: "power",
10787
- args: { on: true },
10788
- effect: {
10789
- field: "on",
10790
- value: true
10791
- }
10792
- }, {
10793
- id: "switch.off",
10794
- method: "setState",
10795
- label: {
10796
- en: "Turn off",
10797
- it: "Spegni"
10779
+ actions: [
10780
+ {
10781
+ id: "switch.on",
10782
+ method: "setState",
10783
+ label: {
10784
+ en: "Turn on",
10785
+ it: "Accendi"
10786
+ },
10787
+ phrase: {
10788
+ en: "Turn on {device}",
10789
+ it: "Accendi {device}"
10790
+ },
10791
+ icon: "power",
10792
+ args: { on: true },
10793
+ effect: {
10794
+ field: "on",
10795
+ value: true
10796
+ }
10798
10797
  },
10799
- phrase: {
10800
- en: "Turn off {device}",
10801
- it: "Spegni {device}"
10798
+ {
10799
+ id: "switch.off",
10800
+ method: "setState",
10801
+ label: {
10802
+ en: "Turn off",
10803
+ it: "Spegni"
10804
+ },
10805
+ phrase: {
10806
+ en: "Turn off {device}",
10807
+ it: "Spegni {device}"
10808
+ },
10809
+ icon: "power-off",
10810
+ args: { on: false },
10811
+ effect: {
10812
+ field: "on",
10813
+ value: false
10814
+ }
10802
10815
  },
10803
- icon: "power-off",
10804
- args: { on: false },
10805
- effect: {
10806
- field: "on",
10807
- value: false
10816
+ {
10817
+ id: "switch.toggle",
10818
+ method: "toggle",
10819
+ label: {
10820
+ en: "Toggle",
10821
+ it: "Commuta"
10822
+ },
10823
+ phrase: {
10824
+ en: "Toggle {device}",
10825
+ it: "Commuta {device}"
10826
+ },
10827
+ icon: "toggle-right"
10808
10828
  }
10809
- }],
10829
+ ],
10810
10830
  state: [{
10811
10831
  kind: "boolean",
10812
10832
  field: "on",
@@ -20339,6 +20359,10 @@ Object.freeze({
20339
20359
  {
20340
20360
  "path": "storage.dischargedTodayKwh",
20341
20361
  "valueType": "number"
20362
+ },
20363
+ {
20364
+ "path": "storage.healthPct",
20365
+ "valueType": "number"
20342
20366
  }
20343
20367
  ],
20344
20368
  "enum-sensor": [{
@@ -20719,6 +20743,10 @@ Object.freeze({
20719
20743
  "path": "isMuted",
20720
20744
  "valueType": "boolean"
20721
20745
  },
20746
+ {
20747
+ "path": "muteReadback",
20748
+ "valueType": "boolean"
20749
+ },
20722
20750
  {
20723
20751
  "path": "source",
20724
20752
  "valueType": "string"
@@ -22839,7 +22867,7 @@ var NodeColorIdSchema = _enum([
22839
22867
  "node-h"
22840
22868
  ]);
22841
22869
  //#endregion
22842
- //#region ../types/dist/convert-BAzH15HU.mjs
22870
+ //#region ../types/dist/convert-BwMfbk5b.mjs
22843
22871
  /**
22844
22872
  * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
22845
22873
  * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
@@ -23172,6 +23200,21 @@ var UNIT_TABLE = {
23172
23200
  factor: 1,
23173
23201
  offset: 0
23174
23202
  },
23203
+ dB: {
23204
+ dimension: "signal-ratio",
23205
+ factor: 1,
23206
+ offset: 0
23207
+ },
23208
+ Hz: {
23209
+ dimension: "frequency",
23210
+ factor: 1,
23211
+ offset: 0
23212
+ },
23213
+ kHz: {
23214
+ dimension: "frequency",
23215
+ factor: 1e3,
23216
+ offset: 0
23217
+ },
23175
23218
  mired: {
23176
23219
  dimension: "colour-temperature",
23177
23220
  factor: 1,
@@ -55494,7 +55537,9 @@ var ClusterUpdateFailureReasonSchema = _enum([
55494
55537
  "status-unreadable",
55495
55538
  "addon-update-failed",
55496
55539
  "addons-not-converged",
55497
- "interrupted"
55540
+ "interrupted",
55541
+ "timeout-stall",
55542
+ "timeout-ceiling"
55498
55543
  ]);
55499
55544
  var ClusterUpdateStepSchema = object({
55500
55545
  kind: ClusterUpdateStepKindSchema,
@@ -57061,6 +57106,12 @@ var MediaPlayerStatusSchema = object({
57061
57106
  /** Mute toggle distinct from volume=0. Null when no mute surface.
57062
57107
  * Pair with `DeviceFeature.MediaPlayerMute`. */
57063
57108
  isMuted: boolean().nullable(),
57109
+ /**
57110
+ * True only when the provider READS the mute state back from the device.
57111
+ * Absent or false: mute is a momentary key. A non-null `isMuted` is not a
57112
+ * reading on its own — an infrared send can leave a value without this flag.
57113
+ */
57114
+ muteReadback: boolean().optional(),
57064
57115
  /** Active source / input. Empty when no source surface. */
57065
57116
  source: string(),
57066
57117
  /** Selectable sources. Empty when no source surface.
@@ -59764,9 +59815,15 @@ var SiteZoneSchema = object({
59764
59815
  locationKeys: array(string())
59765
59816
  });
59766
59817
  var DeviceSearchRequiresSchema = object({
59767
- /** Capability names the field wants a value of. */
59768
- caps: array(string()).readonly(),
59769
- /** `any` (default): a device with one of them is compatible. `all`: it must hold every one. */
59818
+ /** Capability names the field wants a value of. Absent when the field wants actions only. */
59819
+ caps: array(string()).readonly().optional(),
59820
+ /**
59821
+ * Vocabulary action ids (`cover.open`) the device must offer. Compatible means
59822
+ * the device's bindings resolve those actions; `values` stays empty when no
59823
+ * cap was required. `mode` applies the same way as for `caps`.
59824
+ */
59825
+ actions: array(string()).readonly().optional(),
59826
+ /** `any` (default): one of the named caps, and one of the named actions, is enough. `all`: every one. */
59770
59827
  mode: _enum(["any", "all"]).optional()
59771
59828
  });
59772
59829
  var DeviceSearchFiltersSchema = object({
@@ -63117,7 +63174,9 @@ var EnergyStorageSchema = object({
63117
63174
  /** Lifetime discharged, kWh. */
63118
63175
  dischargedKwh: number$1().nullable().meta(readingMeta("energy")),
63119
63176
  chargedTodayKwh: number$1().nullable().meta(readingMeta("energy")),
63120
- dischargedTodayKwh: number$1().nullable().meta(readingMeta("energy"))
63177
+ dischargedTodayKwh: number$1().nullable().meta(readingMeta("energy")),
63178
+ /** State of health, 0..100. Null when the system did not report it. */
63179
+ healthPct: number$1().nullable().meta(readingMeta("percentage"))
63121
63180
  });
63122
63181
  var EnergySystemStatusSchema = object({
63123
63182
  solar: EnergySolarSchema.nullable(),
@@ -63229,6 +63288,12 @@ var energySystemCapability = {
63229
63288
  kind: "counter",
63230
63289
  unit: "kWh",
63231
63290
  displayName: "Storage discharged"
63291
+ },
63292
+ "storage.healthPct": {
63293
+ kind: "gauge",
63294
+ unit: "%",
63295
+ deadband: 1,
63296
+ displayName: "Storage health"
63232
63297
  }
63233
63298
  } },
63234
63299
  volatileStateFields: ["lastFetchedAt"],
@@ -66123,7 +66188,7 @@ var HeroModelSchema = object({
66123
66188
  *
66124
66189
  * - `extras` are the further inline controls of the types that carry more than one
66125
66190
  * ({@link ROW_EXTRAS_BY_TYPE}): climate setpoint, a robot's return to base,
66126
- * a player's volume and remote. Each is a control the model already has, named by id,
66191
+ * a player's volume and remote, an automation's enabled switch. Each is a control the model already has, named by id,
66127
66192
  * in that order and at most {@link ROW_EXTRAS_MAX}; one the model lacks, or lists in
66128
66193
  * `gated`, is absent, and the one `primary` already is never repeats. Empty = the
66129
66194
  * row of an older server: `primary` alone.
@@ -66183,7 +66248,7 @@ new Set([
66183
66248
  "valve"
66184
66249
  ]);
66185
66250
  DeviceType.Climate;
66186
- DeviceType.Climate, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer;
66251
+ DeviceType.Climate, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.Automation;
66187
66252
  /**
66188
66253
  * hero-layout — what a device's hero says (D1023). Read-only: the FIXED
66189
66254
  * layouts live in `@camstack/types` and nothing here stores or edits one.
@@ -79306,6 +79371,13 @@ var NcHistoryEntrySchema = object({
79306
79371
  error: string().optional(),
79307
79372
  subject: NcHistorySubjectSchema,
79308
79373
  /**
79374
+ * The title and body actually sent, frozen at delivery. Absent on a row
79375
+ * still pending, a row suppressed before a notification was built, and a
79376
+ * row delivered before this field shipped.
79377
+ */
79378
+ title: string().optional(),
79379
+ body: string().optional(),
79380
+ /**
79309
79381
  * Ids of the artefacts (still, then gif, then clip) this row's successful
79310
79382
  * delivery indexed in the artefact library — a REFERENCE, never the bytes
79311
79383
  * (an artefact is often megabytes; this row is durable JSON rewritten on
@@ -87616,7 +87688,8 @@ var ServerUpdateActionResultSchema = object({
87616
87688
  outcome: ServerUpdateOutcomeSchema.optional(),
87617
87689
  /**
87618
87690
  * Stable, greppable cause for `refused` / `failed` (the engine's
87619
- * `UpdateRefusalReason`, `staging-failed`, …). `null` on success.
87691
+ * `UpdateRefusalReason`, `staging-failed`, `timeout-stall`,
87692
+ * `timeout-ceiling`). `null` on success.
87620
87693
  */
87621
87694
  reason: string().nullable().optional(),
87622
87695
  /**
@@ -87738,6 +87811,14 @@ var smokeCapability = {
87738
87811
  }]
87739
87812
  }
87740
87813
  };
87814
+ var EmailImagePartSchema = object({
87815
+ ticket: PeerBytesTicketSchema,
87816
+ mime: string().min(1),
87817
+ name: string().min(1),
87818
+ contentId: string().min(1),
87819
+ placement: _enum(["inline", "attachment"]),
87820
+ deviceId: number$1().int().nonnegative().optional()
87821
+ });
87741
87822
  /**
87742
87823
  * `smtp-provider` — pluggable email delivery surface.
87743
87824
  *
@@ -87747,8 +87828,8 @@ var smokeCapability = {
87747
87828
  * ready provider OR target a specific addonId.
87748
87829
  *
87749
87830
  * The shape stays small on purpose — `sendEmail` is enough for the
87750
- * 90% case. Attachments / HTML templating / multi-part are room for
87751
- * future fields without breaking the existing call sites.
87831
+ * 90% case. An image is not a byte field on this method (D613): `images`
87832
+ * names `peerBytes` tickets the sender opens and folds into one MIME body.
87752
87833
  */
87753
87834
  var EmailAddressSchema = email();
87754
87835
  /**
@@ -87775,7 +87856,12 @@ var SendEmailInputSchema = object({
87775
87856
  * for clients that strip HTML — including most spam filters). */
87776
87857
  text: string(),
87777
87858
  /** Optional HTML body. Renders alongside `text` as multi-part. */
87778
- html: string().optional()
87859
+ html: string().optional(),
87860
+ /**
87861
+ * Images the caller already offered over `peerBytes`. Absent = a mail with
87862
+ * no image. The sender opens each ticket; one it cannot read is left out.
87863
+ */
87864
+ images: array(EmailImagePartSchema).max(3).optional()
87779
87865
  });
87780
87866
  var SendEmailResultSchema = object({
87781
87867
  messageId: string(),
@@ -88805,34 +88891,46 @@ var switchCapability = {
88805
88891
  DeviceType.Light,
88806
88892
  DeviceType.Fan
88807
88893
  ],
88808
- methods: { setState: method(object({
88809
- deviceId: number$1(),
88810
- on: boolean()
88811
- }), _void(), {
88812
- kind: "mutation",
88813
- auth: "protected",
88814
- actions: [{
88815
- id: "switch.on",
88816
- label: t("Turn on", "Accendi"),
88817
- phrase: t("Turn on {device}", "Accendi {device}"),
88818
- icon: "power",
88819
- effect: {
88820
- field: "on",
88821
- value: true
88822
- },
88823
- args: { on: true }
88824
- }, {
88825
- id: "switch.off",
88826
- label: t("Turn off", "Spegni"),
88827
- phrase: t("Turn off {device}", "Spegni {device}"),
88828
- icon: "power-off",
88829
- effect: {
88830
- field: "on",
88831
- value: false
88832
- },
88833
- args: { on: false }
88834
- }]
88835
- }) },
88894
+ methods: {
88895
+ setState: method(object({
88896
+ deviceId: number$1(),
88897
+ on: boolean()
88898
+ }), _void(), {
88899
+ kind: "mutation",
88900
+ auth: "protected",
88901
+ actions: [{
88902
+ id: "switch.on",
88903
+ label: t("Turn on", "Accendi"),
88904
+ phrase: t("Turn on {device}", "Accendi {device}"),
88905
+ icon: "power",
88906
+ effect: {
88907
+ field: "on",
88908
+ value: true
88909
+ },
88910
+ args: { on: true }
88911
+ }, {
88912
+ id: "switch.off",
88913
+ label: t("Turn off", "Spegni"),
88914
+ phrase: t("Turn off {device}", "Spegni {device}"),
88915
+ icon: "power-off",
88916
+ effect: {
88917
+ field: "on",
88918
+ value: false
88919
+ },
88920
+ args: { on: false }
88921
+ }]
88922
+ }),
88923
+ toggle: optionalMethod(object({ deviceId: number$1() }), _void(), {
88924
+ kind: "mutation",
88925
+ auth: "protected",
88926
+ actions: [{
88927
+ id: "switch.toggle",
88928
+ label: t("Toggle", "Commuta"),
88929
+ phrase: t("Toggle {device}", "Commuta {device}"),
88930
+ icon: "toggle-right"
88931
+ }]
88932
+ })
88933
+ },
88836
88934
  events: { onStateChanged: {
88837
88935
  data: object({
88838
88936
  deviceId: number$1(),
@@ -96646,7 +96744,11 @@ var temperatureContribution = contribution("temperature-sensor", TemperatureSens
96646
96744
  var humidityContribution = contribution("humidity-sensor", HumiditySensorStatusSchema, (slice) => measured(sensorFragment(slice?.humidity ?? null, "humidity", "droplets", "Humidity", "Umidità", `${Math.round(slice?.humidity ?? 0)}%`), slice?.humidity == null ? emptyMeasurement("humidity") : humidityMeasurement(slice.humidity, slice.lastFetchedAt)));
96647
96745
  var pressureContribution = contribution("pressure-sensor", PressureSensorStatusSchema, (slice) => sensorFragment(slice?.pressure ?? null, "pressure", "gauge", "Pressure", "Pressione", `${Math.round(slice?.pressure ?? 0)} hPa`));
96648
96746
  var illuminanceContribution = contribution("ambient-light-sensor", AmbientLightSensorStatusSchema, (slice) => sensorFragment(slice?.illuminance ?? null, "illuminance", "sun", "Light", "Luce", `${Math.round(slice?.illuminance ?? 0)} lx`));
96649
- var numericContribution = contribution("numeric-sensor", NumericSensorStatusSchema, (slice) => sensorFragment(slice?.value ?? null, "reading", "gauge", "Value", "Valore", String(Number((slice?.value ?? 0).toFixed(2)))));
96747
+ var numericContribution = contribution("numeric-sensor", NumericSensorStatusSchema, (slice, ctx) => {
96748
+ const unit = ctx.quantity == null ? void 0 : baseUnitOf(ctx.quantity);
96749
+ const text = String(Number((slice?.value ?? 0).toFixed(2)));
96750
+ return sensorFragment(slice?.value ?? null, "reading", "gauge", "Value", "Valore", unit === void 0 ? text : `${text} ${unit}`);
96751
+ });
96650
96752
  var enumContribution = contribution("enum-sensor", EnumSensorStatusSchema, (slice) => {
96651
96753
  if (slice === void 0 || slice.value === "") return measured({ reading: reading("sensor", "unknown") }, emptyMeasurement("text"));
96652
96754
  if (slice.timestamp === true) return { reading: reading("sensor", "on") };
@@ -106916,6 +107018,13 @@ Object.freeze({
106916
107018
  access: "create",
106917
107019
  role: "viewer"
106918
107020
  },
107021
+ "switch.toggle": {
107022
+ capName: "switch",
107023
+ capScope: "device",
107024
+ addonId: null,
107025
+ access: "create",
107026
+ role: "viewer"
107027
+ },
106919
107028
  "system.debugWatch": {
106920
107029
  capName: "system",
106921
107030
  capScope: "system",
@@ -110743,6 +110852,12 @@ Object.freeze({
110743
110852
  form: "single",
110744
110853
  optional: false
110745
110854
  }],
110855
+ "smtpProvider.sendEmail": [{
110856
+ name: "images",
110857
+ form: "object-array",
110858
+ optional: true,
110859
+ itemField: "deviceId"
110860
+ }],
110746
110861
  "snapshot.applyDeviceSettingsPatch": [{
110747
110862
  name: "deviceId",
110748
110863
  form: "single",
@@ -110909,6 +111024,11 @@ Object.freeze({
110909
111024
  form: "single",
110910
111025
  optional: false
110911
111026
  }],
111027
+ "switch.toggle": [{
111028
+ name: "deviceId",
111029
+ form: "single",
111030
+ optional: false
111031
+ }],
110912
111032
  "system.debugWatch": [{
110913
111033
  name: "deviceIds",
110914
111034
  form: "array",
@@ -111772,6 +111892,28 @@ var STATE_SUMMARY_VARS = [
111772
111892
  systemEventKinds: [...SUMMARISABLE],
111773
111893
  systemEventDeliveries: ["summary"]
111774
111894
  },
111895
+ {
111896
+ name: "offlineCount",
111897
+ label: "Summary: how many went offline",
111898
+ example: "2",
111899
+ group: "system",
111900
+ families: ["rule"],
111901
+ fields: [...RULE_TEXT],
111902
+ kinds: ["system"],
111903
+ systemEventKinds: [...SUMMARISABLE],
111904
+ systemEventDeliveries: ["summary"]
111905
+ },
111906
+ {
111907
+ name: "onlineCount",
111908
+ label: "Summary: how many came back",
111909
+ example: "5",
111910
+ group: "system",
111911
+ families: ["rule"],
111912
+ fields: [...RULE_TEXT],
111913
+ kinds: ["system"],
111914
+ systemEventKinds: [...SUMMARISABLE],
111915
+ systemEventDeliveries: ["summary"]
111916
+ },
111775
111917
  {
111776
111918
  name: "summaryCount",
111777
111919
  label: "Summary: items reported",
@@ -111813,6 +111955,7 @@ var STATE_SUMMARY_VARS = [
111813
111955
  ([...RULE_TEXT], [...COMBINED_ALARM]),
111814
111956
  ([...RULE_TEXT], [...COMBINED_ALARM]),
111815
111957
  [...RULE_TEXT],
111958
+ [...RULE_TEXT],
111816
111959
  ([...RULE_TEXT], [...COMBINED_ALARM]),
111817
111960
  ([...RULE_TEXT], [...PACKAGE_UPDATES]),
111818
111961
  [...RULE_TEXT],
@@ -112930,6 +113073,21 @@ function toBase(value, unit, quantity) {
112930
113073
  }
112931
113074
  //#endregion
112932
113075
  //#region src/enpal-catalog.ts
113076
+ /**
113077
+ * The two meters the box both called "Consumption". English is the device
113078
+ * name (one string, like every other catalog entry); Italian is the addon
113079
+ * catalogue key `catalog.<id>.name`.
113080
+ */
113081
+ var ENPAL_CONSUMPTION_NAMES = {
113082
+ house: {
113083
+ en: "House consumption",
113084
+ it: "Consumo casa"
113085
+ },
113086
+ heatpump: {
113087
+ en: "Heat pump consumption",
113088
+ it: "Consumo pompa di calore"
113089
+ }
113090
+ };
112933
113091
  var point = (quantity, ...sources) => ({
112934
113092
  quantity,
112935
113093
  sources: sources.map((s) => typeof s === "string" ? { key: s } : s)
@@ -113030,7 +113188,7 @@ var ENPAL_SPECS = [
113030
113188
  {
113031
113189
  kind: "power-meter",
113032
113190
  id: "house",
113033
- name: "Consumption",
113191
+ name: ENPAL_CONSUMPTION_NAMES.house.en,
113034
113192
  role: DeviceRole.PowerSensor,
113035
113193
  power: point("power", "Power.Consumption.Total"),
113036
113194
  kwh: point("energy", "Energy.Consumption.Total.Lifetime")
@@ -113038,7 +113196,7 @@ var ENPAL_SPECS = [
113038
113196
  {
113039
113197
  kind: "power-meter",
113040
113198
  id: "heatpump",
113041
- name: "Consumption",
113199
+ name: ENPAL_CONSUMPTION_NAMES.heatpump.en,
113042
113200
  role: DeviceRole.PowerSensor,
113043
113201
  power: point("power", "Heatpump.Power.Consumption.Total"),
113044
113202
  kwh: point("energy", "Heatpump.Energy.Consumption.Total.Lifetime")
@@ -113115,11 +113273,8 @@ var ENPAL_SPECS = [
113115
113273
  id: "grid-frequency",
113116
113274
  name: "Frequency",
113117
113275
  role: DeviceRole.NumericSensor,
113118
- quantity: "index",
113119
- value: {
113120
- ...point("index", "Frequency.Grid"),
113121
- ignoreUnit: true
113122
- }
113276
+ quantity: "frequency",
113277
+ value: point("frequency", "Frequency.Grid")
113123
113278
  },
113124
113279
  numeric("grid-feed-max", "Maximum feed-in power", "power", "Power.Grid.Maximum.Feed", DeviceRole.PowerSensor),
113125
113280
  numeric("house-energy-day", "Consumption today", "energy", "Energy.Consumption.Total.Day", DeviceRole.EnergySensor),
@@ -113153,9 +113308,9 @@ var ENPAL_SPECS = [
113153
113308
  text("lte-connection-type", "Connection type", "LTE.Connection.Type"),
113154
113309
  numeric("lte-quality", "LTE quality", "percentage", "LTE.Quality"),
113155
113310
  numeric("lte-rsrp", "RSRP", "signal-level", "LTE.RSRP"),
113156
- numeric("lte-rsrq", "RSRQ", "index", "LTE.RSRQ"),
113311
+ numeric("lte-rsrq", "RSRQ", "signal-ratio", "LTE.RSRQ"),
113157
113312
  numeric("lte-rssi", "RSSI", "signal-level", "LTE.RSSI"),
113158
- numeric("lte-snr", "SNR", "index", "LTE.SNR"),
113313
+ numeric("lte-snr", "SNR", "signal-ratio", "LTE.SNR"),
113159
113314
  text("lte-failover", "Failover result", "LTE.Failover.Result"),
113160
113315
  text("lte-signal-check", "Signal check", "LTE.Cronny.Result"),
113161
113316
  text("wifi-state", "Wi-Fi state", "WiFi.State"),
@@ -113196,7 +113351,7 @@ function resolve(target, snapshot) {
113196
113351
  for (const source of target.sources) {
113197
113352
  const reading = snapshot.readings.get(source.key);
113198
113353
  if (reading === void 0) continue;
113199
- const converted = toBase(reading.value, target.ignoreUnit === true ? void 0 : reading.unit, target.quantity);
113354
+ const converted = toBase(reading.value, reading.unit, target.quantity);
113200
113355
  if (!converted.ok) {
113201
113356
  refusal = `${source.key}: ${converted.reason} (unit ${reading.unit ?? "none"})`;
113202
113357
  continue;
@@ -113669,7 +113824,8 @@ function computeEnergySystemStatus(snapshot, options, now) {
113669
113824
  chargedKwh: read.meter("battery-charged"),
113670
113825
  dischargedKwh: read.meter("battery-discharged"),
113671
113826
  chargedTodayKwh: read.numeric("battery-charge-day"),
113672
- dischargedTodayKwh: read.numeric("battery-discharge-day")
113827
+ dischargedTodayKwh: read.numeric("battery-discharge-day"),
113828
+ healthPct: read.numeric("battery-health")
113673
113829
  };
113674
113830
  return {
113675
113831
  status: {
package/dist/addon.mjs CHANGED
@@ -6065,7 +6065,7 @@ var ZodIssueCode = {
6065
6065
  var ZodFirstPartyTypeKind;
6066
6066
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
6067
6067
  //#endregion
6068
- //#region ../types/dist/sleep-Cykc2bzi.mjs
6068
+ //#region ../types/dist/sleep-D7hCDd7y.mjs
6069
6069
  /**
6070
6070
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6071
6071
  * window to float samples (D455).
@@ -10376,6 +10376,8 @@ var READING_QUANTITIES = [
10376
10376
  "information",
10377
10377
  "sound-level",
10378
10378
  "signal-level",
10379
+ "signal-ratio",
10380
+ "frequency",
10379
10381
  "colour-temperature",
10380
10382
  "angle",
10381
10383
  "bitrate",
@@ -10419,6 +10421,8 @@ var QUANTITY_BASE = {
10419
10421
  information: "B",
10420
10422
  "sound-level": "dBFS",
10421
10423
  "signal-level": "dBm",
10424
+ "signal-ratio": "dB",
10425
+ frequency: "Hz",
10422
10426
  "colour-temperature": "mired",
10423
10427
  angle: "°",
10424
10428
  bitrate: "kbps",
@@ -10771,41 +10775,57 @@ var CAP_VOCABULARY_BY_NAME = {
10771
10775
  en: "Switch",
10772
10776
  it: "Interruttore"
10773
10777
  },
10774
- actions: [{
10775
- id: "switch.on",
10776
- method: "setState",
10777
- label: {
10778
- en: "Turn on",
10779
- it: "Accendi"
10780
- },
10781
- phrase: {
10782
- en: "Turn on {device}",
10783
- it: "Accendi {device}"
10784
- },
10785
- icon: "power",
10786
- args: { on: true },
10787
- effect: {
10788
- field: "on",
10789
- value: true
10790
- }
10791
- }, {
10792
- id: "switch.off",
10793
- method: "setState",
10794
- label: {
10795
- en: "Turn off",
10796
- it: "Spegni"
10778
+ actions: [
10779
+ {
10780
+ id: "switch.on",
10781
+ method: "setState",
10782
+ label: {
10783
+ en: "Turn on",
10784
+ it: "Accendi"
10785
+ },
10786
+ phrase: {
10787
+ en: "Turn on {device}",
10788
+ it: "Accendi {device}"
10789
+ },
10790
+ icon: "power",
10791
+ args: { on: true },
10792
+ effect: {
10793
+ field: "on",
10794
+ value: true
10795
+ }
10797
10796
  },
10798
- phrase: {
10799
- en: "Turn off {device}",
10800
- it: "Spegni {device}"
10797
+ {
10798
+ id: "switch.off",
10799
+ method: "setState",
10800
+ label: {
10801
+ en: "Turn off",
10802
+ it: "Spegni"
10803
+ },
10804
+ phrase: {
10805
+ en: "Turn off {device}",
10806
+ it: "Spegni {device}"
10807
+ },
10808
+ icon: "power-off",
10809
+ args: { on: false },
10810
+ effect: {
10811
+ field: "on",
10812
+ value: false
10813
+ }
10801
10814
  },
10802
- icon: "power-off",
10803
- args: { on: false },
10804
- effect: {
10805
- field: "on",
10806
- value: false
10815
+ {
10816
+ id: "switch.toggle",
10817
+ method: "toggle",
10818
+ label: {
10819
+ en: "Toggle",
10820
+ it: "Commuta"
10821
+ },
10822
+ phrase: {
10823
+ en: "Toggle {device}",
10824
+ it: "Commuta {device}"
10825
+ },
10826
+ icon: "toggle-right"
10807
10827
  }
10808
- }],
10828
+ ],
10809
10829
  state: [{
10810
10830
  kind: "boolean",
10811
10831
  field: "on",
@@ -20338,6 +20358,10 @@ Object.freeze({
20338
20358
  {
20339
20359
  "path": "storage.dischargedTodayKwh",
20340
20360
  "valueType": "number"
20361
+ },
20362
+ {
20363
+ "path": "storage.healthPct",
20364
+ "valueType": "number"
20341
20365
  }
20342
20366
  ],
20343
20367
  "enum-sensor": [{
@@ -20718,6 +20742,10 @@ Object.freeze({
20718
20742
  "path": "isMuted",
20719
20743
  "valueType": "boolean"
20720
20744
  },
20745
+ {
20746
+ "path": "muteReadback",
20747
+ "valueType": "boolean"
20748
+ },
20721
20749
  {
20722
20750
  "path": "source",
20723
20751
  "valueType": "string"
@@ -22838,7 +22866,7 @@ var NodeColorIdSchema = _enum([
22838
22866
  "node-h"
22839
22867
  ]);
22840
22868
  //#endregion
22841
- //#region ../types/dist/convert-BAzH15HU.mjs
22869
+ //#region ../types/dist/convert-BwMfbk5b.mjs
22842
22870
  /**
22843
22871
  * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
22844
22872
  * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
@@ -23171,6 +23199,21 @@ var UNIT_TABLE = {
23171
23199
  factor: 1,
23172
23200
  offset: 0
23173
23201
  },
23202
+ dB: {
23203
+ dimension: "signal-ratio",
23204
+ factor: 1,
23205
+ offset: 0
23206
+ },
23207
+ Hz: {
23208
+ dimension: "frequency",
23209
+ factor: 1,
23210
+ offset: 0
23211
+ },
23212
+ kHz: {
23213
+ dimension: "frequency",
23214
+ factor: 1e3,
23215
+ offset: 0
23216
+ },
23174
23217
  mired: {
23175
23218
  dimension: "colour-temperature",
23176
23219
  factor: 1,
@@ -55493,7 +55536,9 @@ var ClusterUpdateFailureReasonSchema = _enum([
55493
55536
  "status-unreadable",
55494
55537
  "addon-update-failed",
55495
55538
  "addons-not-converged",
55496
- "interrupted"
55539
+ "interrupted",
55540
+ "timeout-stall",
55541
+ "timeout-ceiling"
55497
55542
  ]);
55498
55543
  var ClusterUpdateStepSchema = object({
55499
55544
  kind: ClusterUpdateStepKindSchema,
@@ -57060,6 +57105,12 @@ var MediaPlayerStatusSchema = object({
57060
57105
  /** Mute toggle distinct from volume=0. Null when no mute surface.
57061
57106
  * Pair with `DeviceFeature.MediaPlayerMute`. */
57062
57107
  isMuted: boolean().nullable(),
57108
+ /**
57109
+ * True only when the provider READS the mute state back from the device.
57110
+ * Absent or false: mute is a momentary key. A non-null `isMuted` is not a
57111
+ * reading on its own — an infrared send can leave a value without this flag.
57112
+ */
57113
+ muteReadback: boolean().optional(),
57063
57114
  /** Active source / input. Empty when no source surface. */
57064
57115
  source: string(),
57065
57116
  /** Selectable sources. Empty when no source surface.
@@ -59763,9 +59814,15 @@ var SiteZoneSchema = object({
59763
59814
  locationKeys: array(string())
59764
59815
  });
59765
59816
  var DeviceSearchRequiresSchema = object({
59766
- /** Capability names the field wants a value of. */
59767
- caps: array(string()).readonly(),
59768
- /** `any` (default): a device with one of them is compatible. `all`: it must hold every one. */
59817
+ /** Capability names the field wants a value of. Absent when the field wants actions only. */
59818
+ caps: array(string()).readonly().optional(),
59819
+ /**
59820
+ * Vocabulary action ids (`cover.open`) the device must offer. Compatible means
59821
+ * the device's bindings resolve those actions; `values` stays empty when no
59822
+ * cap was required. `mode` applies the same way as for `caps`.
59823
+ */
59824
+ actions: array(string()).readonly().optional(),
59825
+ /** `any` (default): one of the named caps, and one of the named actions, is enough. `all`: every one. */
59769
59826
  mode: _enum(["any", "all"]).optional()
59770
59827
  });
59771
59828
  var DeviceSearchFiltersSchema = object({
@@ -63116,7 +63173,9 @@ var EnergyStorageSchema = object({
63116
63173
  /** Lifetime discharged, kWh. */
63117
63174
  dischargedKwh: number$1().nullable().meta(readingMeta("energy")),
63118
63175
  chargedTodayKwh: number$1().nullable().meta(readingMeta("energy")),
63119
- dischargedTodayKwh: number$1().nullable().meta(readingMeta("energy"))
63176
+ dischargedTodayKwh: number$1().nullable().meta(readingMeta("energy")),
63177
+ /** State of health, 0..100. Null when the system did not report it. */
63178
+ healthPct: number$1().nullable().meta(readingMeta("percentage"))
63120
63179
  });
63121
63180
  var EnergySystemStatusSchema = object({
63122
63181
  solar: EnergySolarSchema.nullable(),
@@ -63228,6 +63287,12 @@ var energySystemCapability = {
63228
63287
  kind: "counter",
63229
63288
  unit: "kWh",
63230
63289
  displayName: "Storage discharged"
63290
+ },
63291
+ "storage.healthPct": {
63292
+ kind: "gauge",
63293
+ unit: "%",
63294
+ deadband: 1,
63295
+ displayName: "Storage health"
63231
63296
  }
63232
63297
  } },
63233
63298
  volatileStateFields: ["lastFetchedAt"],
@@ -66122,7 +66187,7 @@ var HeroModelSchema = object({
66122
66187
  *
66123
66188
  * - `extras` are the further inline controls of the types that carry more than one
66124
66189
  * ({@link ROW_EXTRAS_BY_TYPE}): climate setpoint, a robot's return to base,
66125
- * a player's volume and remote. Each is a control the model already has, named by id,
66190
+ * a player's volume and remote, an automation's enabled switch. Each is a control the model already has, named by id,
66126
66191
  * in that order and at most {@link ROW_EXTRAS_MAX}; one the model lacks, or lists in
66127
66192
  * `gated`, is absent, and the one `primary` already is never repeats. Empty = the
66128
66193
  * row of an older server: `primary` alone.
@@ -66182,7 +66247,7 @@ new Set([
66182
66247
  "valve"
66183
66248
  ]);
66184
66249
  DeviceType.Climate;
66185
- DeviceType.Climate, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer;
66250
+ DeviceType.Climate, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.Automation;
66186
66251
  /**
66187
66252
  * hero-layout — what a device's hero says (D1023). Read-only: the FIXED
66188
66253
  * layouts live in `@camstack/types` and nothing here stores or edits one.
@@ -79305,6 +79370,13 @@ var NcHistoryEntrySchema = object({
79305
79370
  error: string().optional(),
79306
79371
  subject: NcHistorySubjectSchema,
79307
79372
  /**
79373
+ * The title and body actually sent, frozen at delivery. Absent on a row
79374
+ * still pending, a row suppressed before a notification was built, and a
79375
+ * row delivered before this field shipped.
79376
+ */
79377
+ title: string().optional(),
79378
+ body: string().optional(),
79379
+ /**
79308
79380
  * Ids of the artefacts (still, then gif, then clip) this row's successful
79309
79381
  * delivery indexed in the artefact library — a REFERENCE, never the bytes
79310
79382
  * (an artefact is often megabytes; this row is durable JSON rewritten on
@@ -87615,7 +87687,8 @@ var ServerUpdateActionResultSchema = object({
87615
87687
  outcome: ServerUpdateOutcomeSchema.optional(),
87616
87688
  /**
87617
87689
  * Stable, greppable cause for `refused` / `failed` (the engine's
87618
- * `UpdateRefusalReason`, `staging-failed`, …). `null` on success.
87690
+ * `UpdateRefusalReason`, `staging-failed`, `timeout-stall`,
87691
+ * `timeout-ceiling`). `null` on success.
87619
87692
  */
87620
87693
  reason: string().nullable().optional(),
87621
87694
  /**
@@ -87737,6 +87810,14 @@ var smokeCapability = {
87737
87810
  }]
87738
87811
  }
87739
87812
  };
87813
+ var EmailImagePartSchema = object({
87814
+ ticket: PeerBytesTicketSchema,
87815
+ mime: string().min(1),
87816
+ name: string().min(1),
87817
+ contentId: string().min(1),
87818
+ placement: _enum(["inline", "attachment"]),
87819
+ deviceId: number$1().int().nonnegative().optional()
87820
+ });
87740
87821
  /**
87741
87822
  * `smtp-provider` — pluggable email delivery surface.
87742
87823
  *
@@ -87746,8 +87827,8 @@ var smokeCapability = {
87746
87827
  * ready provider OR target a specific addonId.
87747
87828
  *
87748
87829
  * The shape stays small on purpose — `sendEmail` is enough for the
87749
- * 90% case. Attachments / HTML templating / multi-part are room for
87750
- * future fields without breaking the existing call sites.
87830
+ * 90% case. An image is not a byte field on this method (D613): `images`
87831
+ * names `peerBytes` tickets the sender opens and folds into one MIME body.
87751
87832
  */
87752
87833
  var EmailAddressSchema = email();
87753
87834
  /**
@@ -87774,7 +87855,12 @@ var SendEmailInputSchema = object({
87774
87855
  * for clients that strip HTML — including most spam filters). */
87775
87856
  text: string(),
87776
87857
  /** Optional HTML body. Renders alongside `text` as multi-part. */
87777
- html: string().optional()
87858
+ html: string().optional(),
87859
+ /**
87860
+ * Images the caller already offered over `peerBytes`. Absent = a mail with
87861
+ * no image. The sender opens each ticket; one it cannot read is left out.
87862
+ */
87863
+ images: array(EmailImagePartSchema).max(3).optional()
87778
87864
  });
87779
87865
  var SendEmailResultSchema = object({
87780
87866
  messageId: string(),
@@ -88804,34 +88890,46 @@ var switchCapability = {
88804
88890
  DeviceType.Light,
88805
88891
  DeviceType.Fan
88806
88892
  ],
88807
- methods: { setState: method(object({
88808
- deviceId: number$1(),
88809
- on: boolean()
88810
- }), _void(), {
88811
- kind: "mutation",
88812
- auth: "protected",
88813
- actions: [{
88814
- id: "switch.on",
88815
- label: t("Turn on", "Accendi"),
88816
- phrase: t("Turn on {device}", "Accendi {device}"),
88817
- icon: "power",
88818
- effect: {
88819
- field: "on",
88820
- value: true
88821
- },
88822
- args: { on: true }
88823
- }, {
88824
- id: "switch.off",
88825
- label: t("Turn off", "Spegni"),
88826
- phrase: t("Turn off {device}", "Spegni {device}"),
88827
- icon: "power-off",
88828
- effect: {
88829
- field: "on",
88830
- value: false
88831
- },
88832
- args: { on: false }
88833
- }]
88834
- }) },
88893
+ methods: {
88894
+ setState: method(object({
88895
+ deviceId: number$1(),
88896
+ on: boolean()
88897
+ }), _void(), {
88898
+ kind: "mutation",
88899
+ auth: "protected",
88900
+ actions: [{
88901
+ id: "switch.on",
88902
+ label: t("Turn on", "Accendi"),
88903
+ phrase: t("Turn on {device}", "Accendi {device}"),
88904
+ icon: "power",
88905
+ effect: {
88906
+ field: "on",
88907
+ value: true
88908
+ },
88909
+ args: { on: true }
88910
+ }, {
88911
+ id: "switch.off",
88912
+ label: t("Turn off", "Spegni"),
88913
+ phrase: t("Turn off {device}", "Spegni {device}"),
88914
+ icon: "power-off",
88915
+ effect: {
88916
+ field: "on",
88917
+ value: false
88918
+ },
88919
+ args: { on: false }
88920
+ }]
88921
+ }),
88922
+ toggle: optionalMethod(object({ deviceId: number$1() }), _void(), {
88923
+ kind: "mutation",
88924
+ auth: "protected",
88925
+ actions: [{
88926
+ id: "switch.toggle",
88927
+ label: t("Toggle", "Commuta"),
88928
+ phrase: t("Toggle {device}", "Commuta {device}"),
88929
+ icon: "toggle-right"
88930
+ }]
88931
+ })
88932
+ },
88835
88933
  events: { onStateChanged: {
88836
88934
  data: object({
88837
88935
  deviceId: number$1(),
@@ -96645,7 +96743,11 @@ var temperatureContribution = contribution("temperature-sensor", TemperatureSens
96645
96743
  var humidityContribution = contribution("humidity-sensor", HumiditySensorStatusSchema, (slice) => measured(sensorFragment(slice?.humidity ?? null, "humidity", "droplets", "Humidity", "Umidità", `${Math.round(slice?.humidity ?? 0)}%`), slice?.humidity == null ? emptyMeasurement("humidity") : humidityMeasurement(slice.humidity, slice.lastFetchedAt)));
96646
96744
  var pressureContribution = contribution("pressure-sensor", PressureSensorStatusSchema, (slice) => sensorFragment(slice?.pressure ?? null, "pressure", "gauge", "Pressure", "Pressione", `${Math.round(slice?.pressure ?? 0)} hPa`));
96647
96745
  var illuminanceContribution = contribution("ambient-light-sensor", AmbientLightSensorStatusSchema, (slice) => sensorFragment(slice?.illuminance ?? null, "illuminance", "sun", "Light", "Luce", `${Math.round(slice?.illuminance ?? 0)} lx`));
96648
- var numericContribution = contribution("numeric-sensor", NumericSensorStatusSchema, (slice) => sensorFragment(slice?.value ?? null, "reading", "gauge", "Value", "Valore", String(Number((slice?.value ?? 0).toFixed(2)))));
96746
+ var numericContribution = contribution("numeric-sensor", NumericSensorStatusSchema, (slice, ctx) => {
96747
+ const unit = ctx.quantity == null ? void 0 : baseUnitOf(ctx.quantity);
96748
+ const text = String(Number((slice?.value ?? 0).toFixed(2)));
96749
+ return sensorFragment(slice?.value ?? null, "reading", "gauge", "Value", "Valore", unit === void 0 ? text : `${text} ${unit}`);
96750
+ });
96649
96751
  var enumContribution = contribution("enum-sensor", EnumSensorStatusSchema, (slice) => {
96650
96752
  if (slice === void 0 || slice.value === "") return measured({ reading: reading("sensor", "unknown") }, emptyMeasurement("text"));
96651
96753
  if (slice.timestamp === true) return { reading: reading("sensor", "on") };
@@ -106915,6 +107017,13 @@ Object.freeze({
106915
107017
  access: "create",
106916
107018
  role: "viewer"
106917
107019
  },
107020
+ "switch.toggle": {
107021
+ capName: "switch",
107022
+ capScope: "device",
107023
+ addonId: null,
107024
+ access: "create",
107025
+ role: "viewer"
107026
+ },
106918
107027
  "system.debugWatch": {
106919
107028
  capName: "system",
106920
107029
  capScope: "system",
@@ -110742,6 +110851,12 @@ Object.freeze({
110742
110851
  form: "single",
110743
110852
  optional: false
110744
110853
  }],
110854
+ "smtpProvider.sendEmail": [{
110855
+ name: "images",
110856
+ form: "object-array",
110857
+ optional: true,
110858
+ itemField: "deviceId"
110859
+ }],
110745
110860
  "snapshot.applyDeviceSettingsPatch": [{
110746
110861
  name: "deviceId",
110747
110862
  form: "single",
@@ -110908,6 +111023,11 @@ Object.freeze({
110908
111023
  form: "single",
110909
111024
  optional: false
110910
111025
  }],
111026
+ "switch.toggle": [{
111027
+ name: "deviceId",
111028
+ form: "single",
111029
+ optional: false
111030
+ }],
110911
111031
  "system.debugWatch": [{
110912
111032
  name: "deviceIds",
110913
111033
  form: "array",
@@ -111771,6 +111891,28 @@ var STATE_SUMMARY_VARS = [
111771
111891
  systemEventKinds: [...SUMMARISABLE],
111772
111892
  systemEventDeliveries: ["summary"]
111773
111893
  },
111894
+ {
111895
+ name: "offlineCount",
111896
+ label: "Summary: how many went offline",
111897
+ example: "2",
111898
+ group: "system",
111899
+ families: ["rule"],
111900
+ fields: [...RULE_TEXT],
111901
+ kinds: ["system"],
111902
+ systemEventKinds: [...SUMMARISABLE],
111903
+ systemEventDeliveries: ["summary"]
111904
+ },
111905
+ {
111906
+ name: "onlineCount",
111907
+ label: "Summary: how many came back",
111908
+ example: "5",
111909
+ group: "system",
111910
+ families: ["rule"],
111911
+ fields: [...RULE_TEXT],
111912
+ kinds: ["system"],
111913
+ systemEventKinds: [...SUMMARISABLE],
111914
+ systemEventDeliveries: ["summary"]
111915
+ },
111774
111916
  {
111775
111917
  name: "summaryCount",
111776
111918
  label: "Summary: items reported",
@@ -111812,6 +111954,7 @@ var STATE_SUMMARY_VARS = [
111812
111954
  ([...RULE_TEXT], [...COMBINED_ALARM]),
111813
111955
  ([...RULE_TEXT], [...COMBINED_ALARM]),
111814
111956
  [...RULE_TEXT],
111957
+ [...RULE_TEXT],
111815
111958
  ([...RULE_TEXT], [...COMBINED_ALARM]),
111816
111959
  ([...RULE_TEXT], [...PACKAGE_UPDATES]),
111817
111960
  [...RULE_TEXT],
@@ -112929,6 +113072,21 @@ function toBase(value, unit, quantity) {
112929
113072
  }
112930
113073
  //#endregion
112931
113074
  //#region src/enpal-catalog.ts
113075
+ /**
113076
+ * The two meters the box both called "Consumption". English is the device
113077
+ * name (one string, like every other catalog entry); Italian is the addon
113078
+ * catalogue key `catalog.<id>.name`.
113079
+ */
113080
+ var ENPAL_CONSUMPTION_NAMES = {
113081
+ house: {
113082
+ en: "House consumption",
113083
+ it: "Consumo casa"
113084
+ },
113085
+ heatpump: {
113086
+ en: "Heat pump consumption",
113087
+ it: "Consumo pompa di calore"
113088
+ }
113089
+ };
112932
113090
  var point = (quantity, ...sources) => ({
112933
113091
  quantity,
112934
113092
  sources: sources.map((s) => typeof s === "string" ? { key: s } : s)
@@ -113029,7 +113187,7 @@ var ENPAL_SPECS = [
113029
113187
  {
113030
113188
  kind: "power-meter",
113031
113189
  id: "house",
113032
- name: "Consumption",
113190
+ name: ENPAL_CONSUMPTION_NAMES.house.en,
113033
113191
  role: DeviceRole.PowerSensor,
113034
113192
  power: point("power", "Power.Consumption.Total"),
113035
113193
  kwh: point("energy", "Energy.Consumption.Total.Lifetime")
@@ -113037,7 +113195,7 @@ var ENPAL_SPECS = [
113037
113195
  {
113038
113196
  kind: "power-meter",
113039
113197
  id: "heatpump",
113040
- name: "Consumption",
113198
+ name: ENPAL_CONSUMPTION_NAMES.heatpump.en,
113041
113199
  role: DeviceRole.PowerSensor,
113042
113200
  power: point("power", "Heatpump.Power.Consumption.Total"),
113043
113201
  kwh: point("energy", "Heatpump.Energy.Consumption.Total.Lifetime")
@@ -113114,11 +113272,8 @@ var ENPAL_SPECS = [
113114
113272
  id: "grid-frequency",
113115
113273
  name: "Frequency",
113116
113274
  role: DeviceRole.NumericSensor,
113117
- quantity: "index",
113118
- value: {
113119
- ...point("index", "Frequency.Grid"),
113120
- ignoreUnit: true
113121
- }
113275
+ quantity: "frequency",
113276
+ value: point("frequency", "Frequency.Grid")
113122
113277
  },
113123
113278
  numeric("grid-feed-max", "Maximum feed-in power", "power", "Power.Grid.Maximum.Feed", DeviceRole.PowerSensor),
113124
113279
  numeric("house-energy-day", "Consumption today", "energy", "Energy.Consumption.Total.Day", DeviceRole.EnergySensor),
@@ -113152,9 +113307,9 @@ var ENPAL_SPECS = [
113152
113307
  text("lte-connection-type", "Connection type", "LTE.Connection.Type"),
113153
113308
  numeric("lte-quality", "LTE quality", "percentage", "LTE.Quality"),
113154
113309
  numeric("lte-rsrp", "RSRP", "signal-level", "LTE.RSRP"),
113155
- numeric("lte-rsrq", "RSRQ", "index", "LTE.RSRQ"),
113310
+ numeric("lte-rsrq", "RSRQ", "signal-ratio", "LTE.RSRQ"),
113156
113311
  numeric("lte-rssi", "RSSI", "signal-level", "LTE.RSSI"),
113157
- numeric("lte-snr", "SNR", "index", "LTE.SNR"),
113312
+ numeric("lte-snr", "SNR", "signal-ratio", "LTE.SNR"),
113158
113313
  text("lte-failover", "Failover result", "LTE.Failover.Result"),
113159
113314
  text("lte-signal-check", "Signal check", "LTE.Cronny.Result"),
113160
113315
  text("wifi-state", "Wi-Fi state", "WiFi.State"),
@@ -113195,7 +113350,7 @@ function resolve(target, snapshot) {
113195
113350
  for (const source of target.sources) {
113196
113351
  const reading = snapshot.readings.get(source.key);
113197
113352
  if (reading === void 0) continue;
113198
- const converted = toBase(reading.value, target.ignoreUnit === true ? void 0 : reading.unit, target.quantity);
113353
+ const converted = toBase(reading.value, reading.unit, target.quantity);
113199
113354
  if (!converted.ok) {
113200
113355
  refusal = `${source.key}: ${converted.reason} (unit ${reading.unit ?? "none"})`;
113201
113356
  continue;
@@ -113668,7 +113823,8 @@ function computeEnergySystemStatus(snapshot, options, now) {
113668
113823
  chargedKwh: read.meter("battery-charged"),
113669
113824
  dischargedKwh: read.meter("battery-discharged"),
113670
113825
  chargedTodayKwh: read.numeric("battery-charge-day"),
113671
- dischargedTodayKwh: read.numeric("battery-discharge-day")
113826
+ dischargedTodayKwh: read.numeric("battery-discharge-day"),
113827
+ healthPct: read.numeric("battery-health")
113672
113828
  };
113673
113829
  return {
113674
113830
  status: {
package/i18n/en.json CHANGED
@@ -15,5 +15,7 @@
15
15
  "connection.s_source.pagePort.label": "Device page port",
16
16
  "connection.s_polling.title": "Polling",
17
17
  "connection.s_polling.pollIntervalMs.label": "Poll interval (ms)",
18
- "connection.s_polling.invertBatteryPower.label": "Invert battery power sign"
18
+ "connection.s_polling.invertBatteryPower.label": "Invert battery power sign",
19
+ "catalog.house.name": "House consumption",
20
+ "catalog.heatpump.name": "Heat pump consumption"
19
21
  }
package/i18n/it.json CHANGED
@@ -15,5 +15,7 @@
15
15
  "connection.s_source.pagePort.label": "Porta pagina dispositivo",
16
16
  "connection.s_polling.title": "Polling",
17
17
  "connection.s_polling.pollIntervalMs.label": "Intervallo di polling (ms)",
18
- "connection.s_polling.invertBatteryPower.label": "Inverti il segno della potenza batteria"
18
+ "connection.s_polling.invertBatteryPower.label": "Inverti il segno della potenza batteria",
19
+ "catalog.house.name": "Consumo casa",
20
+ "catalog.heatpump.name": "Consumo pompa di calore"
19
21
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-enpal",
3
- "version": "0.1.20",
3
+ "version": "0.1.21",
4
4
  "description": "Enpal solar-box device-provider addon for CamStack — PV, battery, grid, house and heat-pump readings from the box's own web-page channel, its local InfluxDB or its device page",
5
5
  "keywords": [
6
6
  "camstack",
@@ -42,7 +42,7 @@
42
42
  "version": "0.1.0",
43
43
  "description": "Add an Enpal solar box as an integration (its own web-page channel, InfluxDB or the device page) and adopt its equipment: photovoltaic, battery, grid, house and heat pump",
44
44
  "entry": "./dist/addon.js",
45
- "icon": "assets/icon.svg",
45
+ "icon": "assets/icon.png",
46
46
  "color": "#f5a623",
47
47
  "instanceMode": "multiple",
48
48
  "mode": "account",
@@ -66,7 +66,7 @@
66
66
  }
67
67
  ],
68
68
  "requires": {
69
- "server": ">=1.2.858"
69
+ "server": ">=1.2.863"
70
70
  }
71
71
  },
72
72
  "files": [
package/assets/icon.svg DELETED
@@ -1,8 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
2
- <!-- Enpal mark: navy tile and amber E from the public favicon. -->
3
- <rect width="24" height="24" fill="#072543"/>
4
- <rect x="5" y="5" width="3" height="14" fill="#FFB000"/>
5
- <rect x="5" y="5" width="14" height="3" fill="#FFB000"/>
6
- <rect x="5" y="10.5" width="9" height="3" fill="#FFB000"/>
7
- <rect x="5" y="16" width="14" height="3" fill="#FFB000"/>
8
- </svg>