@camstack/addon-provider-homeassistant 1.2.631 → 1.2.633

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,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.2.633] - 2026-10-27
11
+
12
+ ### Changed
13
+
14
+ - Shown as "Home Assistant" (was "Home Assistant Provider").
15
+
16
+ ## [1.2.632] - 2026-10-27
17
+
10
18
  ### Added
11
19
 
12
- - Package README and changelog, included in the published files list.
20
+ - Home Assistant: Home Assistant device provider addon for CamStack.
21
+ - Provides the addons Home Assistant, Home Assistant Export.
package/README.md CHANGED
@@ -1,22 +1,42 @@
1
- # @camstack/addon-provider-homeassistant
1
+ # Home Assistant
2
2
 
3
- Home Assistant device provider addon for CamStack
3
+ Home Assistant device provider addon for CamStack. Device provider (import) + native entity export (push). Two addons, one bundle, separate processes.
4
4
 
5
- ## What it does
5
+ <!-- readme:generated:start (derived from the manifest; edit outside this block) -->
6
6
 
7
- - **Home Assistant Provider** (`provider-homeassistant`): Adopt Home Assistant devices into CamStack via WebSocket
8
- - Node placement: `hub-only`.
7
+ Package `@camstack/addon-provider-homeassistant` on npm.
8
+
9
+ ## What it provides
10
+
11
+ - **Home Assistant** (`provider-homeassistant`): Adopt Home Assistant devices into CamStack via WebSocket. Category: messaging.
9
12
  - Capabilities: `broker`, `device-adoption`, `vacuum-control`, `lawn-mower-control`, `running-state`, `valve`, `humidifier`, `water-heater`, `weather`, `image`, `update`, `notification-output`, `addon-routes`, `calendar-source`.
10
- - **Home Assistant Export** (`homeassistant-export`): Pushes CamStack entities to the CamStack Home Assistant custom component over HTTP. Chooses which devices reach which Home Assistant instance; Home Assistant never polls the hub for a value.
11
- - Node placement: `hub-only`.
13
+ - **Home Assistant Export** (`homeassistant-export`): Pushes CamStack entities to the CamStack Home Assistant custom component over HTTP. Chooses which devices reach which Home Assistant instance; Home Assistant never polls the hub for a value. Category: exports.
12
14
  - Capabilities: `device-export`, `addon-routes`, `oauth-integration`.
13
15
 
14
- ## Settings
16
+ ## Configuration
17
+
18
+ Edit these from System, Addons, this addon, Settings (a device's own settings are on its device page). A password field is stored in the keychain.
19
+
20
+ **Addon settings**
15
21
 
16
- Settings this addon exposes are edited from System → Addons → this addon → Settings when they apply to the whole cluster, and from the device page when they belong to one device. A password field is stored in the keychain.
22
+ - Calendars to import: Calendars
23
+ - Home Assistant broker: Brokers are managed separately
24
+ - Home Assistant connection: Auth mode, HA base URL, Long-lived access token, Supervisor token
25
+ - Home Assistant instances: No brokers
26
+ - Connection: Hub URL Home Assistant can reach
27
+ - Advanced: Reconcile interval (s)
17
28
 
18
29
  ## Requirements
19
30
 
20
- - Peer packages already on the node: `@camstack/types`.
21
- - A CamStack hub. An agent is used only when the placement above allows it.
31
+ - Placement: hub-only.
32
+ - A CamStack hub. An agent is used only where the placement above allows it.
33
+
34
+ ## Documentation
35
+
36
+ - [docs/addons/addon-provider-homeassistant.md](https://github.com/camstack/server/blob/main/docs/addons/addon-provider-homeassistant.md)
37
+ - [docs/architecture/device-model.md](https://github.com/camstack/server/blob/main/docs/architecture/device-model.md)
38
+ - [packages/addon-provider-homeassistant/AGENTS.md](https://github.com/camstack/server/blob/main/packages/addon-provider-homeassistant/AGENTS.md)
39
+
40
+ See [CHANGELOG.md](./CHANGELOG.md) for what changed in each version.
22
41
 
42
+ <!-- readme:generated:end -->
package/dist/addon.js CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_dist = require("./dist-DV8KQcWd.js");
2
+ const require_dist = require("./dist-DVkAN1CW.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
@@ -126,7 +126,14 @@ var haSourceSchemaFields = {
126
126
  deviceClass: require_dist.string().optional(),
127
127
  integrationId: require_dist.string().optional(),
128
128
  options: require_dist.array(require_dist.string()).optional(),
129
- features: require_dist.array(require_dist.string()).optional()
129
+ features: require_dist.array(require_dist.string()).optional(),
130
+ /** Sibling meter sensors whose readings this entity carries as `power-meter` (D951). */
131
+ meterEntities: require_dist.object({
132
+ watts: require_dist.string().optional(),
133
+ kwhTotal: require_dist.string().optional(),
134
+ volts: require_dist.string().optional(),
135
+ amps: require_dist.string().optional()
136
+ }).optional()
130
137
  };
131
138
  /** Project a persisted config blob into a typed source, or null when the
132
139
  * blob is not an HA-adopted device (no brokerId). */
@@ -292,6 +299,168 @@ var HaDomain = /* @__PURE__ */ function(HaDomain) {
292
299
  return HaDomain;
293
300
  }({});
294
301
  //#endregion
302
+ //#region src/ha-reading.ts
303
+ /**
304
+ * Convert one sample into the quantity's base unit. A refusal publishes
305
+ * nothing (the caller stores null) and is logged with the device id —
306
+ * an unknown unit is never guessed (D393, D944).
307
+ */
308
+ function takeReading(logger, deviceId, value, rawUnit, quantity) {
309
+ const result = require_dist.ingestReading({
310
+ value,
311
+ rawUnit,
312
+ quantity
313
+ });
314
+ if (result.ok) return result.value;
315
+ logger.warn("numeric reading refused", {
316
+ tags: { deviceId },
317
+ meta: {
318
+ quantity,
319
+ unit: rawUnit ?? "",
320
+ reason: result.reason
321
+ }
322
+ });
323
+ return null;
324
+ }
325
+ //#endregion
326
+ //#region src/ha-meter-fold.ts
327
+ /**
328
+ * Power and energy are a feature of the electrical device itself (D951). A
329
+ * metered plug is exposed by Home Assistant as a switch plus separate
330
+ * power / energy / voltage / current sensors; the switch carries the readings
331
+ * and the sensors stay as children (D865), flagged `foldedInto`.
332
+ */
333
+ /** The `power-meter` status field a reading lands in. */
334
+ var METER_FIELDS = [
335
+ "watts",
336
+ "kwhTotal",
337
+ "volts",
338
+ "amps"
339
+ ];
340
+ var METER_BY_DEVICE_CLASS = {
341
+ power: {
342
+ field: "watts",
343
+ quantity: "power"
344
+ },
345
+ energy: {
346
+ field: "kwhTotal",
347
+ quantity: "energy"
348
+ },
349
+ voltage: {
350
+ field: "volts",
351
+ quantity: "voltage"
352
+ },
353
+ current: {
354
+ field: "amps",
355
+ quantity: "current"
356
+ }
357
+ };
358
+ /** The meter field and quantity an HA `device_class` feeds; undefined for any other class. */
359
+ function meterKindForDeviceClass(deviceClass) {
360
+ return deviceClass === null || deviceClass === void 0 ? void 0 : METER_BY_DEVICE_CLASS[deviceClass];
361
+ }
362
+ /** Status with one reading applied; the clock moves with it. */
363
+ function applyMeterReading(prev, field, value, lastFetchedAt) {
364
+ return {
365
+ ...prev,
366
+ [field]: value,
367
+ lastFetchedAt
368
+ };
369
+ }
370
+ var LOAD_TYPES = new Set(require_dist.powerMeterCapability.deviceTypes.filter((type) => type !== require_dist.DeviceType.Sensor));
371
+ function isLoad(entity) {
372
+ return LOAD_TYPES.has(entity.deviceType) && entity.entityCategory === void 0 && !entity.capabilities.includes("power-meter");
373
+ }
374
+ function meterFieldOf(entity) {
375
+ if (!entity.capabilities.includes("power-meter")) return void 0;
376
+ return meterKindForDeviceClass(entity.deviceClass)?.field;
377
+ }
378
+ /**
379
+ * Fold the device's meter sensors into its electrical load: the first normal
380
+ * control of a load type gains `power-meter` and the entity ids it reads; each
381
+ * sensor it reads is flagged `foldedInto`. A device with no load, or no meter,
382
+ * comes back unchanged.
383
+ */
384
+ function foldMeterEntities(entities) {
385
+ const primary = entities.find(isLoad);
386
+ if (primary === void 0) return [...entities];
387
+ const meters = {};
388
+ for (const entity of entities) {
389
+ const field = meterFieldOf(entity);
390
+ if (field !== void 0 && meters[field] === void 0) meters[field] = entity.entityId;
391
+ }
392
+ const folded = new Set(Object.values(meters));
393
+ if (folded.size === 0) return [...entities];
394
+ return entities.map((entity) => {
395
+ if (entity.entityId === primary.entityId) return {
396
+ ...entity,
397
+ capabilities: [...entity.capabilities, "power-meter"],
398
+ meterEntities: meters
399
+ };
400
+ return folded.has(entity.entityId) ? {
401
+ ...entity,
402
+ foldedInto: primary.entityId
403
+ } : entity;
404
+ });
405
+ }
406
+ //#endregion
407
+ //#region src/ha-meter-carrier.ts
408
+ var COLD_START$20 = { lastFetchedAt: 0 };
409
+ /**
410
+ * The `power-meter` of a device whose power / energy readings come from sibling
411
+ * HA sensors (D951). It registers the cap, names the entities the owner must
412
+ * subscribe to, and turns each of their pushes into one field of the slice.
413
+ * The owner is any HA child that is an electrical load (switch, light, fan, ...);
414
+ * none of them knows how a reading is converted.
415
+ */
416
+ var HaMeterCarrier = class {
417
+ ctx;
418
+ runtimeState;
419
+ logger;
420
+ deviceId;
421
+ fieldByEntity;
422
+ constructor(ctx, runtimeState, logger, deviceId, meters) {
423
+ this.ctx = ctx;
424
+ this.runtimeState = runtimeState;
425
+ this.logger = logger;
426
+ this.deviceId = deviceId;
427
+ this.fieldByEntity = new Map(METER_FIELDS.flatMap((field) => {
428
+ const entityId = meters[field];
429
+ return entityId === void 0 ? [] : [[entityId, field]];
430
+ }));
431
+ this.ctx.registerNativeCap(require_dist.powerMeterCapability, { getStatus: async () => this.runtimeState.getCapState("power-meter") ?? COLD_START$20 });
432
+ this.runtimeState.setCapState("power-meter", {
433
+ ...COLD_START$20,
434
+ ...this.runtimeState.getCapState("power-meter")
435
+ });
436
+ }
437
+ /** The meter entities the owner must subscribe to, beside its own. */
438
+ entityIds() {
439
+ return [...this.fieldByEntity.keys()];
440
+ }
441
+ /** Whether a push belongs to one of the meter entities. */
442
+ carries(entityId) {
443
+ return this.fieldByEntity.has(entityId);
444
+ }
445
+ /** Apply one meter push; an unavailable or unreadable sample changes nothing. */
446
+ push(entityId, state) {
447
+ const field = this.fieldByEntity.get(entityId);
448
+ if (field === void 0) return;
449
+ if (state.state === "unavailable" || state.state === "unknown") return;
450
+ const value = Number(state.state);
451
+ if (!Number.isFinite(value)) return;
452
+ const deviceClass = state.attributes["device_class"];
453
+ const kind = meterKindForDeviceClass(typeof deviceClass === "string" ? deviceClass : null);
454
+ if (kind === void 0 || kind.field !== field) return;
455
+ const unit = state.attributes["unit_of_measurement"];
456
+ const stored = takeReading(this.logger, this.deviceId, value, typeof unit === "string" ? unit : void 0, kind.quantity);
457
+ if (stored === null) return;
458
+ const prev = this.runtimeState.getCapState("power-meter") ?? COLD_START$20;
459
+ const lastFetchedAt = Date.parse(state.last_updated) || Date.now();
460
+ this.runtimeState.setCapState("power-meter", applyMeterReading(prev, field, stored, lastFetchedAt));
461
+ }
462
+ };
463
+ //#endregion
295
464
  //#region src/ha-base-child-device.ts
296
465
  /**
297
466
  * Persisted config shared by every per-domain HA child device class.
@@ -357,6 +526,9 @@ var HaBaseChildDevice = class HaBaseChildDevice extends require_dist.BaseDevice
357
526
  /** Handle to the accessories cap when this device acts as a parent
358
527
  * that hosts entity-children. Null for plain child devices. */
359
528
  accessories = null;
529
+ /** The power-meter this device carries for its sibling meter sensors (D951);
530
+ * null for a device that is not metered through other entities. */
531
+ meterCarrier;
360
532
  constructor(ctx, options = {}) {
361
533
  const persisted = ctx.persistedConfig ?? {};
362
534
  const brokerId = persisted.brokerId;
@@ -372,6 +544,7 @@ var HaBaseChildDevice = class HaBaseChildDevice extends require_dist.BaseDevice
372
544
  this.features = parseDeviceFeatures(persisted.features);
373
545
  this.deviceLogger = ctx.deviceMeta.integrationId !== void 0 ? ctx.logger.withTags({ integrationId: ctx.deviceMeta.integrationId }) ?? ctx.logger : ctx.logger;
374
546
  this.online = true;
547
+ this.meterCarrier = persisted.meterEntities !== void 0 ? new HaMeterCarrier(this.ctx, this.runtimeState, this.deviceLogger, this.id, persisted.meterEntities) : null;
375
548
  this.attachEventBusListener();
376
549
  const childIds = Array.isArray(persisted.childDeviceIds) ? persisted.childDeviceIds : [];
377
550
  if (childIds.length > 0) {
@@ -535,7 +708,7 @@ var HaBaseChildDevice = class HaBaseChildDevice extends require_dist.BaseDevice
535
708
  try {
536
709
  const result = await this.ctx.api.broker.subscribe.mutate({
537
710
  brokerId: this.brokerId,
538
- filter: { entityIds: [this.entityId] }
711
+ filter: { entityIds: [this.entityId, ...this.meterCarrier?.entityIds() ?? []] }
539
712
  });
540
713
  this.brokerSubscriptionId = result.subscriptionId;
541
714
  return true;
@@ -554,8 +727,12 @@ var HaBaseChildDevice = class HaBaseChildDevice extends require_dist.BaseDevice
554
727
  const data = event.data;
555
728
  if (!data) return;
556
729
  if (data.brokerId !== this.brokerId) return;
557
- if (data.key !== this.entityId) return;
558
730
  const state = data.payload;
731
+ if (data.key !== void 0 && this.meterCarrier?.carries(data.key) === true) {
732
+ if (state) this.meterCarrier.push(data.key, state);
733
+ return;
734
+ }
735
+ if (data.key !== this.entityId) return;
559
736
  if (!state) return;
560
737
  if (state.state !== this.lastLoggedState) {
561
738
  this.deviceLogger.debug("HA→device value change", { meta: {
@@ -766,30 +943,6 @@ var HaSwitchDevice = class extends HaBaseChildDevice {
766
943
  }
767
944
  };
768
945
  //#endregion
769
- //#region src/ha-reading.ts
770
- /**
771
- * Convert one sample into the quantity's base unit. A refusal publishes
772
- * nothing (the caller stores null) and is logged with the device id —
773
- * an unknown unit is never guessed (D393, D944).
774
- */
775
- function takeReading(logger, deviceId, value, rawUnit, quantity) {
776
- const result = require_dist.ingestReading({
777
- value,
778
- rawUnit,
779
- quantity
780
- });
781
- if (result.ok) return result.value;
782
- logger.warn("numeric reading refused", {
783
- tags: { deviceId },
784
- meta: {
785
- quantity,
786
- unit: rawUnit ?? "",
787
- reason: result.reason
788
- }
789
- });
790
- return null;
791
- }
792
- //#endregion
793
946
  //#region src/devices/ha-numeric-sensor-device.ts
794
947
  var SUPPORTED_CAP_NAMES$1 = [
795
948
  "temperature-sensor",
@@ -3699,7 +3852,14 @@ var containerEntitySchema = require_dist.object({
3699
3852
  features: require_dist.array(require_dist.string()).optional(),
3700
3853
  name: require_dist.string().optional(),
3701
3854
  dateTimeFormat: require_dist.EnumSensorDateTimeFormatSchema.optional(),
3702
- entityCategory: require_dist._enum(["config", "diagnostic"]).optional()
3855
+ entityCategory: require_dist._enum(["config", "diagnostic"]).optional(),
3856
+ meterEntities: require_dist.object({
3857
+ watts: require_dist.string().optional(),
3858
+ kwhTotal: require_dist.string().optional(),
3859
+ volts: require_dist.string().optional(),
3860
+ amps: require_dist.string().optional()
3861
+ }).optional(),
3862
+ foldedInto: require_dist.string().optional()
3703
3863
  });
3704
3864
  function parseContainerEntities(raw) {
3705
3865
  const result = require_dist.array(containerEntitySchema).safeParse(raw);
@@ -3712,6 +3872,9 @@ function parseContainerEntities(raw) {
3712
3872
  * skipped (the adoption path skips them too).
3713
3873
  */
3714
3874
  function buildContainerEntities(haDevice) {
3875
+ return foldMeterEntities(mapContainerEntities(haDevice));
3876
+ }
3877
+ function mapContainerEntities(haDevice) {
3715
3878
  const out = [];
3716
3879
  for (const ent of haDevice.entities) {
3717
3880
  if (ent.mappedTo === null) continue;
@@ -3742,7 +3905,8 @@ function derivedKey(e) {
3742
3905
  deviceClass: e.deviceClass ?? null,
3743
3906
  options: e.options ?? null,
3744
3907
  features: e.features ?? null,
3745
- dateTimeFormat: e.dateTimeFormat ?? null
3908
+ dateTimeFormat: e.dateTimeFormat ?? null,
3909
+ meterEntities: e.meterEntities ?? null
3746
3910
  });
3747
3911
  }
3748
3912
  /** Stable key over the FULL entry incl. name + entityId + the layout-affecting
@@ -3755,6 +3919,7 @@ function fullKey(e) {
3755
3919
  entityId: e.entityId,
3756
3920
  name: e.name ?? null,
3757
3921
  entityCategory: e.entityCategory ?? null,
3922
+ foldedInto: e.foldedInto ?? null,
3758
3923
  derived: derivedKey(e)
3759
3924
  });
3760
3925
  }
@@ -3766,6 +3931,7 @@ function childConfigPatch(e) {
3766
3931
  ...e.options !== void 0 ? { options: e.options } : {},
3767
3932
  ...e.features !== void 0 ? { features: e.features } : {},
3768
3933
  ...e.dateTimeFormat !== void 0 ? { dateTimeFormat: e.dateTimeFormat } : {},
3934
+ ...e.meterEntities !== void 0 ? { meterEntities: e.meterEntities } : {},
3769
3935
  ...e.role !== void 0 ? { role: e.role } : {}
3770
3936
  };
3771
3937
  }
@@ -3819,7 +3985,7 @@ var SECTION_RANK = {
3819
3985
  * normal control). */
3820
3986
  function sectionForEntity(e) {
3821
3987
  if (e.entityCategory === "config") return SECTION_CONFIGURATION;
3822
- if (e.entityCategory === "diagnostic") return SECTION_DIAGNOSTIC;
3988
+ if (e.entityCategory === "diagnostic" || e.foldedInto !== void 0) return SECTION_DIAGNOSTIC;
3823
3989
  return SECTION_CONTROLS;
3824
3990
  }
3825
3991
  /**
@@ -3872,7 +4038,16 @@ var haEntitySchema = require_dist.object({
3872
4038
  dateTimeFormat: require_dist.EnumSensorDateTimeFormatSchema.optional(),
3873
4039
  /** HA `entity_category` — `config` / `diagnostic` / absent (a normal
3874
4040
  * control). Drives the accordion section the child renders in. */
3875
- entityCategory: require_dist._enum(["config", "diagnostic"]).optional()
4041
+ entityCategory: require_dist._enum(["config", "diagnostic"]).optional(),
4042
+ /** Sibling meter sensors this entity reads as its own power-meter (D951). */
4043
+ meterEntities: require_dist.object({
4044
+ watts: require_dist.string().optional(),
4045
+ kwhTotal: require_dist.string().optional(),
4046
+ volts: require_dist.string().optional(),
4047
+ amps: require_dist.string().optional()
4048
+ }).optional(),
4049
+ /** Set on a meter sensor folded into another entity: sectioned as Diagnostic. */
4050
+ foldedInto: require_dist.string().optional()
3876
4051
  });
3877
4052
  var haContainerSchema = require_dist.object({
3878
4053
  system: require_dist.literal("homeassistant"),
@@ -4187,6 +4362,7 @@ var HaContainerDevice = class HaContainerDevice extends require_dist.BaseDevice
4187
4362
  ...e.options !== void 0 ? { options: e.options } : {},
4188
4363
  ...e.features !== void 0 ? { features: e.features } : {},
4189
4364
  ...e.dateTimeFormat !== void 0 ? { dateTimeFormat: e.dateTimeFormat } : {},
4365
+ ...e.meterEntities !== void 0 ? { meterEntities: e.meterEntities } : {},
4190
4366
  ...e.role !== void 0 ? { role: e.role } : {}
4191
4367
  };
4192
4368
  const factory = (childCtx) => {
package/dist/addon.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as lawnMowerControlCapability, $t as union, A as buttonCapability, At as weatherCapability, B as coverCapability, Bt as defineRecordKinds, C as batteryCapability, Ct as tryConvertDelta, D as brokerCapability, Dt as valveCapability, E as brightnessCapability, Et as vacuumControlCapability, F as connectivityCapability, G as eventEmitterCapability, Gt as array, H as deviceAdoptionCapability, It as RecordSecretUnavailableError, J as gasCapability, Jt as literal, K as fanControlCapability, Kt as boolean, Lt as baseUnitOf, M as carbonMonoxideCapability, Mt as DeviceRole, N as climateControlCapability, Nt as DeviceType, O as buildAddonRouteProvider, Ot as vibrationCapability, P as colorCapability, Pt as errMsg, Q as ingestReading, Qt as string, R as contactCapability, Rt as createEvent, S as automationControlCapability, St as temperatureSensorCapability, T as binaryCapability, Tt as updateCapability, V as deviceAdminLinkCapability, Vt as entryBody, W as enumSensorCapability, Wt as _enum, X as humiditySensorCapability, Xt as object, Y as humidifierCapability, Yt as number, Z as imageCapability, Zt as record, _ as accessoriesCapability, _t as runningStateCapability, at as notifierCapability, b as alarmPanelCapability, bt as switchCapability, ct as parseAdminBase, dt as presenceCapability, en as unknown, et as lockControlCapability, ft as pressureSensorCapability, g as TargetSchema, gt as runningFromVacuumState, h as RUNNING_STATE_COLD, ht as runningFromMediaPlayer, it as notificationOutputCapability, j as calendarSourceCapability, jt as DeviceFeature, k as buildDeviceAdminUrl, kt as waterHeaterCapability, lt as powerMeterCapability, mt as runningFromLawnMower, n as BaseDevice, nn as EventCategory, nt as motionCapability, ot as numericSensorCapability, pt as quantityFromUnit, q as floodCapability, qt as discriminatedUnion, r as BaseDeviceProvider, rt as normalizeUnit, tn as url, tt as mediaPlayerCapability, u as EnumSensorDateTimeFormatSchema, ut as prepareNotification, v as addonRoutesCapability, vt as scriptRunnerCapability, w as bestLocationMatch, wt as tryConvertUnit, x as ambientLightSensorCapability, xt as tamperCapability, y as airQualitySensorCapability, yt as smokeCapability, z as controlCapability, zt as defineEntryKind } from "./dist-CdqN_VJZ.mjs";
1
+ import { $ as lawnMowerControlCapability, $t as union, A as buttonCapability, At as weatherCapability, B as coverCapability, Bt as defineRecordKinds, C as batteryCapability, Ct as tryConvertDelta, D as brokerCapability, Dt as valveCapability, E as brightnessCapability, Et as vacuumControlCapability, F as connectivityCapability, G as eventEmitterCapability, Gt as array, H as deviceAdoptionCapability, It as RecordSecretUnavailableError, J as gasCapability, Jt as literal, K as fanControlCapability, Kt as boolean, Lt as baseUnitOf, M as carbonMonoxideCapability, Mt as DeviceRole, N as climateControlCapability, Nt as DeviceType, O as buildAddonRouteProvider, Ot as vibrationCapability, P as colorCapability, Pt as errMsg, Q as ingestReading, Qt as string, R as contactCapability, Rt as createEvent, S as automationControlCapability, St as temperatureSensorCapability, T as binaryCapability, Tt as updateCapability, V as deviceAdminLinkCapability, Vt as entryBody, W as enumSensorCapability, Wt as _enum, X as humiditySensorCapability, Xt as object, Y as humidifierCapability, Yt as number, Z as imageCapability, Zt as record, _ as accessoriesCapability, _t as runningStateCapability, at as notifierCapability, b as alarmPanelCapability, bt as switchCapability, ct as parseAdminBase, dt as presenceCapability, en as unknown, et as lockControlCapability, ft as pressureSensorCapability, g as TargetSchema, gt as runningFromVacuumState, h as RUNNING_STATE_COLD, ht as runningFromMediaPlayer, it as notificationOutputCapability, j as calendarSourceCapability, jt as DeviceFeature, k as buildDeviceAdminUrl, kt as waterHeaterCapability, lt as powerMeterCapability, mt as runningFromLawnMower, n as BaseDevice, nn as EventCategory, nt as motionCapability, ot as numericSensorCapability, pt as quantityFromUnit, q as floodCapability, qt as discriminatedUnion, r as BaseDeviceProvider, rt as normalizeUnit, tn as url, tt as mediaPlayerCapability, u as EnumSensorDateTimeFormatSchema, ut as prepareNotification, v as addonRoutesCapability, vt as scriptRunnerCapability, w as bestLocationMatch, wt as tryConvertUnit, x as ambientLightSensorCapability, xt as tamperCapability, y as airQualitySensorCapability, yt as smokeCapability, z as controlCapability, zt as defineEntryKind } from "./dist-BVojc0hA.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
@@ -125,7 +125,14 @@ var haSourceSchemaFields = {
125
125
  deviceClass: string().optional(),
126
126
  integrationId: string().optional(),
127
127
  options: array(string()).optional(),
128
- features: array(string()).optional()
128
+ features: array(string()).optional(),
129
+ /** Sibling meter sensors whose readings this entity carries as `power-meter` (D951). */
130
+ meterEntities: object({
131
+ watts: string().optional(),
132
+ kwhTotal: string().optional(),
133
+ volts: string().optional(),
134
+ amps: string().optional()
135
+ }).optional()
129
136
  };
130
137
  /** Project a persisted config blob into a typed source, or null when the
131
138
  * blob is not an HA-adopted device (no brokerId). */
@@ -291,6 +298,168 @@ var HaDomain = /* @__PURE__ */ function(HaDomain) {
291
298
  return HaDomain;
292
299
  }({});
293
300
  //#endregion
301
+ //#region src/ha-reading.ts
302
+ /**
303
+ * Convert one sample into the quantity's base unit. A refusal publishes
304
+ * nothing (the caller stores null) and is logged with the device id —
305
+ * an unknown unit is never guessed (D393, D944).
306
+ */
307
+ function takeReading(logger, deviceId, value, rawUnit, quantity) {
308
+ const result = ingestReading({
309
+ value,
310
+ rawUnit,
311
+ quantity
312
+ });
313
+ if (result.ok) return result.value;
314
+ logger.warn("numeric reading refused", {
315
+ tags: { deviceId },
316
+ meta: {
317
+ quantity,
318
+ unit: rawUnit ?? "",
319
+ reason: result.reason
320
+ }
321
+ });
322
+ return null;
323
+ }
324
+ //#endregion
325
+ //#region src/ha-meter-fold.ts
326
+ /**
327
+ * Power and energy are a feature of the electrical device itself (D951). A
328
+ * metered plug is exposed by Home Assistant as a switch plus separate
329
+ * power / energy / voltage / current sensors; the switch carries the readings
330
+ * and the sensors stay as children (D865), flagged `foldedInto`.
331
+ */
332
+ /** The `power-meter` status field a reading lands in. */
333
+ var METER_FIELDS = [
334
+ "watts",
335
+ "kwhTotal",
336
+ "volts",
337
+ "amps"
338
+ ];
339
+ var METER_BY_DEVICE_CLASS = {
340
+ power: {
341
+ field: "watts",
342
+ quantity: "power"
343
+ },
344
+ energy: {
345
+ field: "kwhTotal",
346
+ quantity: "energy"
347
+ },
348
+ voltage: {
349
+ field: "volts",
350
+ quantity: "voltage"
351
+ },
352
+ current: {
353
+ field: "amps",
354
+ quantity: "current"
355
+ }
356
+ };
357
+ /** The meter field and quantity an HA `device_class` feeds; undefined for any other class. */
358
+ function meterKindForDeviceClass(deviceClass) {
359
+ return deviceClass === null || deviceClass === void 0 ? void 0 : METER_BY_DEVICE_CLASS[deviceClass];
360
+ }
361
+ /** Status with one reading applied; the clock moves with it. */
362
+ function applyMeterReading(prev, field, value, lastFetchedAt) {
363
+ return {
364
+ ...prev,
365
+ [field]: value,
366
+ lastFetchedAt
367
+ };
368
+ }
369
+ var LOAD_TYPES = new Set(powerMeterCapability.deviceTypes.filter((type) => type !== DeviceType.Sensor));
370
+ function isLoad(entity) {
371
+ return LOAD_TYPES.has(entity.deviceType) && entity.entityCategory === void 0 && !entity.capabilities.includes("power-meter");
372
+ }
373
+ function meterFieldOf(entity) {
374
+ if (!entity.capabilities.includes("power-meter")) return void 0;
375
+ return meterKindForDeviceClass(entity.deviceClass)?.field;
376
+ }
377
+ /**
378
+ * Fold the device's meter sensors into its electrical load: the first normal
379
+ * control of a load type gains `power-meter` and the entity ids it reads; each
380
+ * sensor it reads is flagged `foldedInto`. A device with no load, or no meter,
381
+ * comes back unchanged.
382
+ */
383
+ function foldMeterEntities(entities) {
384
+ const primary = entities.find(isLoad);
385
+ if (primary === void 0) return [...entities];
386
+ const meters = {};
387
+ for (const entity of entities) {
388
+ const field = meterFieldOf(entity);
389
+ if (field !== void 0 && meters[field] === void 0) meters[field] = entity.entityId;
390
+ }
391
+ const folded = new Set(Object.values(meters));
392
+ if (folded.size === 0) return [...entities];
393
+ return entities.map((entity) => {
394
+ if (entity.entityId === primary.entityId) return {
395
+ ...entity,
396
+ capabilities: [...entity.capabilities, "power-meter"],
397
+ meterEntities: meters
398
+ };
399
+ return folded.has(entity.entityId) ? {
400
+ ...entity,
401
+ foldedInto: primary.entityId
402
+ } : entity;
403
+ });
404
+ }
405
+ //#endregion
406
+ //#region src/ha-meter-carrier.ts
407
+ var COLD_START$20 = { lastFetchedAt: 0 };
408
+ /**
409
+ * The `power-meter` of a device whose power / energy readings come from sibling
410
+ * HA sensors (D951). It registers the cap, names the entities the owner must
411
+ * subscribe to, and turns each of their pushes into one field of the slice.
412
+ * The owner is any HA child that is an electrical load (switch, light, fan, ...);
413
+ * none of them knows how a reading is converted.
414
+ */
415
+ var HaMeterCarrier = class {
416
+ ctx;
417
+ runtimeState;
418
+ logger;
419
+ deviceId;
420
+ fieldByEntity;
421
+ constructor(ctx, runtimeState, logger, deviceId, meters) {
422
+ this.ctx = ctx;
423
+ this.runtimeState = runtimeState;
424
+ this.logger = logger;
425
+ this.deviceId = deviceId;
426
+ this.fieldByEntity = new Map(METER_FIELDS.flatMap((field) => {
427
+ const entityId = meters[field];
428
+ return entityId === void 0 ? [] : [[entityId, field]];
429
+ }));
430
+ this.ctx.registerNativeCap(powerMeterCapability, { getStatus: async () => this.runtimeState.getCapState("power-meter") ?? COLD_START$20 });
431
+ this.runtimeState.setCapState("power-meter", {
432
+ ...COLD_START$20,
433
+ ...this.runtimeState.getCapState("power-meter")
434
+ });
435
+ }
436
+ /** The meter entities the owner must subscribe to, beside its own. */
437
+ entityIds() {
438
+ return [...this.fieldByEntity.keys()];
439
+ }
440
+ /** Whether a push belongs to one of the meter entities. */
441
+ carries(entityId) {
442
+ return this.fieldByEntity.has(entityId);
443
+ }
444
+ /** Apply one meter push; an unavailable or unreadable sample changes nothing. */
445
+ push(entityId, state) {
446
+ const field = this.fieldByEntity.get(entityId);
447
+ if (field === void 0) return;
448
+ if (state.state === "unavailable" || state.state === "unknown") return;
449
+ const value = Number(state.state);
450
+ if (!Number.isFinite(value)) return;
451
+ const deviceClass = state.attributes["device_class"];
452
+ const kind = meterKindForDeviceClass(typeof deviceClass === "string" ? deviceClass : null);
453
+ if (kind === void 0 || kind.field !== field) return;
454
+ const unit = state.attributes["unit_of_measurement"];
455
+ const stored = takeReading(this.logger, this.deviceId, value, typeof unit === "string" ? unit : void 0, kind.quantity);
456
+ if (stored === null) return;
457
+ const prev = this.runtimeState.getCapState("power-meter") ?? COLD_START$20;
458
+ const lastFetchedAt = Date.parse(state.last_updated) || Date.now();
459
+ this.runtimeState.setCapState("power-meter", applyMeterReading(prev, field, stored, lastFetchedAt));
460
+ }
461
+ };
462
+ //#endregion
294
463
  //#region src/ha-base-child-device.ts
295
464
  /**
296
465
  * Persisted config shared by every per-domain HA child device class.
@@ -356,6 +525,9 @@ var HaBaseChildDevice = class HaBaseChildDevice extends BaseDevice {
356
525
  /** Handle to the accessories cap when this device acts as a parent
357
526
  * that hosts entity-children. Null for plain child devices. */
358
527
  accessories = null;
528
+ /** The power-meter this device carries for its sibling meter sensors (D951);
529
+ * null for a device that is not metered through other entities. */
530
+ meterCarrier;
359
531
  constructor(ctx, options = {}) {
360
532
  const persisted = ctx.persistedConfig ?? {};
361
533
  const brokerId = persisted.brokerId;
@@ -371,6 +543,7 @@ var HaBaseChildDevice = class HaBaseChildDevice extends BaseDevice {
371
543
  this.features = parseDeviceFeatures(persisted.features);
372
544
  this.deviceLogger = ctx.deviceMeta.integrationId !== void 0 ? ctx.logger.withTags({ integrationId: ctx.deviceMeta.integrationId }) ?? ctx.logger : ctx.logger;
373
545
  this.online = true;
546
+ this.meterCarrier = persisted.meterEntities !== void 0 ? new HaMeterCarrier(this.ctx, this.runtimeState, this.deviceLogger, this.id, persisted.meterEntities) : null;
374
547
  this.attachEventBusListener();
375
548
  const childIds = Array.isArray(persisted.childDeviceIds) ? persisted.childDeviceIds : [];
376
549
  if (childIds.length > 0) {
@@ -534,7 +707,7 @@ var HaBaseChildDevice = class HaBaseChildDevice extends BaseDevice {
534
707
  try {
535
708
  const result = await this.ctx.api.broker.subscribe.mutate({
536
709
  brokerId: this.brokerId,
537
- filter: { entityIds: [this.entityId] }
710
+ filter: { entityIds: [this.entityId, ...this.meterCarrier?.entityIds() ?? []] }
538
711
  });
539
712
  this.brokerSubscriptionId = result.subscriptionId;
540
713
  return true;
@@ -553,8 +726,12 @@ var HaBaseChildDevice = class HaBaseChildDevice extends BaseDevice {
553
726
  const data = event.data;
554
727
  if (!data) return;
555
728
  if (data.brokerId !== this.brokerId) return;
556
- if (data.key !== this.entityId) return;
557
729
  const state = data.payload;
730
+ if (data.key !== void 0 && this.meterCarrier?.carries(data.key) === true) {
731
+ if (state) this.meterCarrier.push(data.key, state);
732
+ return;
733
+ }
734
+ if (data.key !== this.entityId) return;
558
735
  if (!state) return;
559
736
  if (state.state !== this.lastLoggedState) {
560
737
  this.deviceLogger.debug("HA→device value change", { meta: {
@@ -765,30 +942,6 @@ var HaSwitchDevice = class extends HaBaseChildDevice {
765
942
  }
766
943
  };
767
944
  //#endregion
768
- //#region src/ha-reading.ts
769
- /**
770
- * Convert one sample into the quantity's base unit. A refusal publishes
771
- * nothing (the caller stores null) and is logged with the device id —
772
- * an unknown unit is never guessed (D393, D944).
773
- */
774
- function takeReading(logger, deviceId, value, rawUnit, quantity) {
775
- const result = ingestReading({
776
- value,
777
- rawUnit,
778
- quantity
779
- });
780
- if (result.ok) return result.value;
781
- logger.warn("numeric reading refused", {
782
- tags: { deviceId },
783
- meta: {
784
- quantity,
785
- unit: rawUnit ?? "",
786
- reason: result.reason
787
- }
788
- });
789
- return null;
790
- }
791
- //#endregion
792
945
  //#region src/devices/ha-numeric-sensor-device.ts
793
946
  var SUPPORTED_CAP_NAMES$1 = [
794
947
  "temperature-sensor",
@@ -3698,7 +3851,14 @@ var containerEntitySchema = object({
3698
3851
  features: array(string()).optional(),
3699
3852
  name: string().optional(),
3700
3853
  dateTimeFormat: EnumSensorDateTimeFormatSchema.optional(),
3701
- entityCategory: _enum(["config", "diagnostic"]).optional()
3854
+ entityCategory: _enum(["config", "diagnostic"]).optional(),
3855
+ meterEntities: object({
3856
+ watts: string().optional(),
3857
+ kwhTotal: string().optional(),
3858
+ volts: string().optional(),
3859
+ amps: string().optional()
3860
+ }).optional(),
3861
+ foldedInto: string().optional()
3702
3862
  });
3703
3863
  function parseContainerEntities(raw) {
3704
3864
  const result = array(containerEntitySchema).safeParse(raw);
@@ -3711,6 +3871,9 @@ function parseContainerEntities(raw) {
3711
3871
  * skipped (the adoption path skips them too).
3712
3872
  */
3713
3873
  function buildContainerEntities(haDevice) {
3874
+ return foldMeterEntities(mapContainerEntities(haDevice));
3875
+ }
3876
+ function mapContainerEntities(haDevice) {
3714
3877
  const out = [];
3715
3878
  for (const ent of haDevice.entities) {
3716
3879
  if (ent.mappedTo === null) continue;
@@ -3741,7 +3904,8 @@ function derivedKey(e) {
3741
3904
  deviceClass: e.deviceClass ?? null,
3742
3905
  options: e.options ?? null,
3743
3906
  features: e.features ?? null,
3744
- dateTimeFormat: e.dateTimeFormat ?? null
3907
+ dateTimeFormat: e.dateTimeFormat ?? null,
3908
+ meterEntities: e.meterEntities ?? null
3745
3909
  });
3746
3910
  }
3747
3911
  /** Stable key over the FULL entry incl. name + entityId + the layout-affecting
@@ -3754,6 +3918,7 @@ function fullKey(e) {
3754
3918
  entityId: e.entityId,
3755
3919
  name: e.name ?? null,
3756
3920
  entityCategory: e.entityCategory ?? null,
3921
+ foldedInto: e.foldedInto ?? null,
3757
3922
  derived: derivedKey(e)
3758
3923
  });
3759
3924
  }
@@ -3765,6 +3930,7 @@ function childConfigPatch(e) {
3765
3930
  ...e.options !== void 0 ? { options: e.options } : {},
3766
3931
  ...e.features !== void 0 ? { features: e.features } : {},
3767
3932
  ...e.dateTimeFormat !== void 0 ? { dateTimeFormat: e.dateTimeFormat } : {},
3933
+ ...e.meterEntities !== void 0 ? { meterEntities: e.meterEntities } : {},
3768
3934
  ...e.role !== void 0 ? { role: e.role } : {}
3769
3935
  };
3770
3936
  }
@@ -3818,7 +3984,7 @@ var SECTION_RANK = {
3818
3984
  * normal control). */
3819
3985
  function sectionForEntity(e) {
3820
3986
  if (e.entityCategory === "config") return SECTION_CONFIGURATION;
3821
- if (e.entityCategory === "diagnostic") return SECTION_DIAGNOSTIC;
3987
+ if (e.entityCategory === "diagnostic" || e.foldedInto !== void 0) return SECTION_DIAGNOSTIC;
3822
3988
  return SECTION_CONTROLS;
3823
3989
  }
3824
3990
  /**
@@ -3871,7 +4037,16 @@ var haEntitySchema = object({
3871
4037
  dateTimeFormat: EnumSensorDateTimeFormatSchema.optional(),
3872
4038
  /** HA `entity_category` — `config` / `diagnostic` / absent (a normal
3873
4039
  * control). Drives the accordion section the child renders in. */
3874
- entityCategory: _enum(["config", "diagnostic"]).optional()
4040
+ entityCategory: _enum(["config", "diagnostic"]).optional(),
4041
+ /** Sibling meter sensors this entity reads as its own power-meter (D951). */
4042
+ meterEntities: object({
4043
+ watts: string().optional(),
4044
+ kwhTotal: string().optional(),
4045
+ volts: string().optional(),
4046
+ amps: string().optional()
4047
+ }).optional(),
4048
+ /** Set on a meter sensor folded into another entity: sectioned as Diagnostic. */
4049
+ foldedInto: string().optional()
3875
4050
  });
3876
4051
  var haContainerSchema = object({
3877
4052
  system: literal("homeassistant"),
@@ -4186,6 +4361,7 @@ var HaContainerDevice = class HaContainerDevice extends BaseDevice {
4186
4361
  ...e.options !== void 0 ? { options: e.options } : {},
4187
4362
  ...e.features !== void 0 ? { features: e.features } : {},
4188
4363
  ...e.dateTimeFormat !== void 0 ? { dateTimeFormat: e.dateTimeFormat } : {},
4364
+ ...e.meterEntities !== void 0 ? { meterEntities: e.meterEntities } : {},
4189
4365
  ...e.role !== void 0 ? { role: e.role } : {}
4190
4366
  };
4191
4367
  const factory = (childCtx) => {
@@ -17613,7 +17613,22 @@ var BrokerProviderInfoSchema = object({
17613
17613
  */
17614
17614
  interactiveConnectPath: string().min(1).optional(),
17615
17615
  /** Relative hub path to disconnect the linked account (same popup pattern). */
17616
- interactiveDisconnectPath: string().min(1).optional()
17616
+ interactiveDisconnectPath: string().min(1).optional(),
17617
+ /**
17618
+ * The kind signs in through a server-held interactive session instead of
17619
+ * (or before) typed credentials. `basePath` is a hub-relative route set
17620
+ * served by the owning addon (`/addon/<id>/...`) speaking the sign-in
17621
+ * contract: `POST {basePath}/start` (body: the form's settings) answers
17622
+ * `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
17623
+ * `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
17624
+ * 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
17625
+ * (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
17626
+ * existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
17627
+ * (`{ sessionId }`) drops the session. The dialog that edits a broker
17628
+ * offers it as the default path; the fields the session fills are
17629
+ * marked `filledBySignIn` in the kind's settings schema.
17630
+ */
17631
+ signIn: object({ basePath: string().min(1) }).optional()
17617
17632
  }))
17618
17633
  });
17619
17634
  var ListInputSchema = object({
@@ -44857,7 +44872,7 @@ var NetworkAccessStatusSchema = object({
44857
44872
  });
44858
44873
  /**
44859
44874
  * Optional, richer endpoint shape returned by providers that expose
44860
- * MORE than one ingress concurrently (Tailscale Ingress with mixed
44875
+ * MORE than one ingress concurrently (Tailscale Funnel with mixed
44861
44876
  * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
44862
44877
  * the originating provider config (mode + sourcePort) so the
44863
44878
  * orchestrator UI can label rows distinctly. Providers that expose only
@@ -58387,7 +58402,8 @@ var BATTERY_DEVICE_TYPES = [
58387
58402
  DeviceType.Switch,
58388
58403
  DeviceType.PetFeeder,
58389
58404
  DeviceType.Container,
58390
- DeviceType.Thermostat
58405
+ DeviceType.Thermostat,
58406
+ DeviceType.EventEmitter
58391
58407
  ];
58392
58408
  /** Generic Customize target for the percentage reading (D936). */
58393
58409
  var batteryCustomize = {
@@ -65272,9 +65288,9 @@ var PowerMeterStatusSchema = object({
65272
65288
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
65273
65289
  kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
65274
65290
  /** Voltage in volts. */
65275
- volts: number().optional().meta(readingMeta("voltage")),
65291
+ volts: number().optional().meta(readingMeta("voltage", { label: "Voltage" })),
65276
65292
  /** Current in amperes. */
65277
- amps: number().optional().meta(readingMeta("current")),
65293
+ amps: number().optional().meta(readingMeta("current", { label: "Current" })),
65278
65294
  /** Apparent power in volt-amperes. */
65279
65295
  va: number().optional().meta(readingMeta("apparent-power")),
65280
65296
  /** Power factor (0..1) if reported. */
@@ -65282,17 +65298,74 @@ var PowerMeterStatusSchema = object({
65282
65298
  /** Ms epoch when the slice was last updated. */
65283
65299
  lastFetchedAt: number()
65284
65300
  });
65301
+ /**
65302
+ * Loads that draw metered mains power (D951). The reading is a feature of the
65303
+ * device itself — a plug, a lamp, a fan, an air conditioner, a boiler, a
65304
+ * vacuum dock — not a separate sensor beside it. A `Sensor` stays: a clamp
65305
+ * meter or a standalone energy monitor IS the device. Cover, lock, valve,
65306
+ * camera and the like are left out: their draw is not what anyone meters.
65307
+ */
65308
+ var POWER_METER_DEVICE_TYPES = [
65309
+ DeviceType.Sensor,
65310
+ DeviceType.Switch,
65311
+ DeviceType.Light,
65312
+ DeviceType.Fan,
65313
+ DeviceType.Climate,
65314
+ DeviceType.Thermostat,
65315
+ DeviceType.Humidifier,
65316
+ DeviceType.WaterHeater,
65317
+ DeviceType.MediaPlayer,
65318
+ DeviceType.Washer,
65319
+ DeviceType.Dryer,
65320
+ DeviceType.Dishwasher,
65321
+ DeviceType.Vacuum,
65322
+ DeviceType.LawnMower,
65323
+ DeviceType.PetFeeder
65324
+ ];
65325
+ /** Everything but a bare `Sensor`: a sensor that takes its reading from another sensor adds nothing. */
65326
+ var POWER_METER_CUSTOMIZE_DEVICE_TYPES = POWER_METER_DEVICE_TYPES.filter((type) => type !== DeviceType.Sensor);
65327
+ /** One scalar target per reading; each is sourced from a device of its own (D951). */
65328
+ function powerTarget(id, unit) {
65329
+ return {
65330
+ id,
65331
+ cardinality: "scalar",
65332
+ vocabulary: {
65333
+ cap: "power-meter",
65334
+ stateField: id
65335
+ },
65336
+ deviceTypes: POWER_METER_CUSTOMIZE_DEVICE_TYPES,
65337
+ apply: "add",
65338
+ path: id,
65339
+ sources: [{
65340
+ kind: "numeric",
65341
+ requiredUnit: unit,
65342
+ transforms: [{ kind: "identity" }]
65343
+ }],
65344
+ companions: [{
65345
+ kind: "clock",
65346
+ path: "lastFetchedAt",
65347
+ when: "always"
65348
+ }],
65349
+ sourceReport: { status: "assignment-status" },
65350
+ advancedExpression: true
65351
+ };
65352
+ }
65353
+ /** "Power and energy" extension: watts, energy, voltage and current from other devices (D951). */
65354
+ var powerMeterCustomize = {
65355
+ version: 1,
65356
+ targets: [
65357
+ powerTarget("watts", "W"),
65358
+ powerTarget("kwhTotal", "kWh"),
65359
+ powerTarget("volts", "V"),
65360
+ powerTarget("amps", "A")
65361
+ ]
65362
+ };
65285
65363
  var powerMeterCapability = {
65286
65364
  name: "power-meter",
65287
65365
  scope: "device",
65288
65366
  deviceNative: true,
65289
65367
  mode: "singleton",
65290
- deviceTypes: [
65291
- DeviceType.Sensor,
65292
- DeviceType.Washer,
65293
- DeviceType.Dryer,
65294
- DeviceType.Dishwasher
65295
- ],
65368
+ deviceTypes: POWER_METER_DEVICE_TYPES,
65296
65369
  methods: {},
65297
65370
  status: {
65298
65371
  schema: PowerMeterStatusSchema,
@@ -65349,7 +65422,8 @@ var powerMeterCapability = {
65349
65422
  } },
65350
65423
  /** Clock fields: written, but excluded from the compare that decides
65351
65424
  * whether persisting is worth a SQLite commit. */
65352
- volatileStateFields: ["lastFetchedAt"]
65425
+ volatileStateFields: ["lastFetchedAt"],
65426
+ customize: powerMeterCustomize
65353
65427
  };
65354
65428
  /**
65355
65429
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -71180,7 +71254,7 @@ function tryConvertDelta(value, from, to) {
71180
71254
  return shifted - origin;
71181
71255
  }
71182
71256
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
71183
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Camera, DeviceType.Camera, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
71257
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Camera, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
71184
71258
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
71185
71259
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
71186
71260
  new Set(CAP_NAMES);
@@ -17614,7 +17614,22 @@ var BrokerProviderInfoSchema = object({
17614
17614
  */
17615
17615
  interactiveConnectPath: string().min(1).optional(),
17616
17616
  /** Relative hub path to disconnect the linked account (same popup pattern). */
17617
- interactiveDisconnectPath: string().min(1).optional()
17617
+ interactiveDisconnectPath: string().min(1).optional(),
17618
+ /**
17619
+ * The kind signs in through a server-held interactive session instead of
17620
+ * (or before) typed credentials. `basePath` is a hub-relative route set
17621
+ * served by the owning addon (`/addon/<id>/...`) speaking the sign-in
17622
+ * contract: `POST {basePath}/start` (body: the form's settings) answers
17623
+ * `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
17624
+ * `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
17625
+ * 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
17626
+ * (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
17627
+ * existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
17628
+ * (`{ sessionId }`) drops the session. The dialog that edits a broker
17629
+ * offers it as the default path; the fields the session fills are
17630
+ * marked `filledBySignIn` in the kind's settings schema.
17631
+ */
17632
+ signIn: object({ basePath: string().min(1) }).optional()
17618
17633
  }))
17619
17634
  });
17620
17635
  var ListInputSchema = object({
@@ -44858,7 +44873,7 @@ var NetworkAccessStatusSchema = object({
44858
44873
  });
44859
44874
  /**
44860
44875
  * Optional, richer endpoint shape returned by providers that expose
44861
- * MORE than one ingress concurrently (Tailscale Ingress with mixed
44876
+ * MORE than one ingress concurrently (Tailscale Funnel with mixed
44862
44877
  * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
44863
44878
  * the originating provider config (mode + sourcePort) so the
44864
44879
  * orchestrator UI can label rows distinctly. Providers that expose only
@@ -58388,7 +58403,8 @@ var BATTERY_DEVICE_TYPES = [
58388
58403
  DeviceType.Switch,
58389
58404
  DeviceType.PetFeeder,
58390
58405
  DeviceType.Container,
58391
- DeviceType.Thermostat
58406
+ DeviceType.Thermostat,
58407
+ DeviceType.EventEmitter
58392
58408
  ];
58393
58409
  /** Generic Customize target for the percentage reading (D936). */
58394
58410
  var batteryCustomize = {
@@ -65273,9 +65289,9 @@ var PowerMeterStatusSchema = object({
65273
65289
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
65274
65290
  kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
65275
65291
  /** Voltage in volts. */
65276
- volts: number().optional().meta(readingMeta("voltage")),
65292
+ volts: number().optional().meta(readingMeta("voltage", { label: "Voltage" })),
65277
65293
  /** Current in amperes. */
65278
- amps: number().optional().meta(readingMeta("current")),
65294
+ amps: number().optional().meta(readingMeta("current", { label: "Current" })),
65279
65295
  /** Apparent power in volt-amperes. */
65280
65296
  va: number().optional().meta(readingMeta("apparent-power")),
65281
65297
  /** Power factor (0..1) if reported. */
@@ -65283,17 +65299,74 @@ var PowerMeterStatusSchema = object({
65283
65299
  /** Ms epoch when the slice was last updated. */
65284
65300
  lastFetchedAt: number()
65285
65301
  });
65302
+ /**
65303
+ * Loads that draw metered mains power (D951). The reading is a feature of the
65304
+ * device itself — a plug, a lamp, a fan, an air conditioner, a boiler, a
65305
+ * vacuum dock — not a separate sensor beside it. A `Sensor` stays: a clamp
65306
+ * meter or a standalone energy monitor IS the device. Cover, lock, valve,
65307
+ * camera and the like are left out: their draw is not what anyone meters.
65308
+ */
65309
+ var POWER_METER_DEVICE_TYPES = [
65310
+ DeviceType.Sensor,
65311
+ DeviceType.Switch,
65312
+ DeviceType.Light,
65313
+ DeviceType.Fan,
65314
+ DeviceType.Climate,
65315
+ DeviceType.Thermostat,
65316
+ DeviceType.Humidifier,
65317
+ DeviceType.WaterHeater,
65318
+ DeviceType.MediaPlayer,
65319
+ DeviceType.Washer,
65320
+ DeviceType.Dryer,
65321
+ DeviceType.Dishwasher,
65322
+ DeviceType.Vacuum,
65323
+ DeviceType.LawnMower,
65324
+ DeviceType.PetFeeder
65325
+ ];
65326
+ /** Everything but a bare `Sensor`: a sensor that takes its reading from another sensor adds nothing. */
65327
+ var POWER_METER_CUSTOMIZE_DEVICE_TYPES = POWER_METER_DEVICE_TYPES.filter((type) => type !== DeviceType.Sensor);
65328
+ /** One scalar target per reading; each is sourced from a device of its own (D951). */
65329
+ function powerTarget(id, unit) {
65330
+ return {
65331
+ id,
65332
+ cardinality: "scalar",
65333
+ vocabulary: {
65334
+ cap: "power-meter",
65335
+ stateField: id
65336
+ },
65337
+ deviceTypes: POWER_METER_CUSTOMIZE_DEVICE_TYPES,
65338
+ apply: "add",
65339
+ path: id,
65340
+ sources: [{
65341
+ kind: "numeric",
65342
+ requiredUnit: unit,
65343
+ transforms: [{ kind: "identity" }]
65344
+ }],
65345
+ companions: [{
65346
+ kind: "clock",
65347
+ path: "lastFetchedAt",
65348
+ when: "always"
65349
+ }],
65350
+ sourceReport: { status: "assignment-status" },
65351
+ advancedExpression: true
65352
+ };
65353
+ }
65354
+ /** "Power and energy" extension: watts, energy, voltage and current from other devices (D951). */
65355
+ var powerMeterCustomize = {
65356
+ version: 1,
65357
+ targets: [
65358
+ powerTarget("watts", "W"),
65359
+ powerTarget("kwhTotal", "kWh"),
65360
+ powerTarget("volts", "V"),
65361
+ powerTarget("amps", "A")
65362
+ ]
65363
+ };
65286
65364
  var powerMeterCapability = {
65287
65365
  name: "power-meter",
65288
65366
  scope: "device",
65289
65367
  deviceNative: true,
65290
65368
  mode: "singleton",
65291
- deviceTypes: [
65292
- DeviceType.Sensor,
65293
- DeviceType.Washer,
65294
- DeviceType.Dryer,
65295
- DeviceType.Dishwasher
65296
- ],
65369
+ deviceTypes: POWER_METER_DEVICE_TYPES,
65297
65370
  methods: {},
65298
65371
  status: {
65299
65372
  schema: PowerMeterStatusSchema,
@@ -65350,7 +65423,8 @@ var powerMeterCapability = {
65350
65423
  } },
65351
65424
  /** Clock fields: written, but excluded from the compare that decides
65352
65425
  * whether persisting is worth a SQLite commit. */
65353
- volatileStateFields: ["lastFetchedAt"]
65426
+ volatileStateFields: ["lastFetchedAt"],
65427
+ customize: powerMeterCustomize
65354
65428
  };
65355
65429
  /**
65356
65430
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -71181,7 +71255,7 @@ function tryConvertDelta(value, from, to) {
71181
71255
  return shifted - origin;
71182
71256
  }
71183
71257
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
71184
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Camera, DeviceType.Camera, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
71258
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Camera, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
71185
71259
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
71186
71260
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
71187
71261
  new Set(CAP_NAMES);
@@ -24,7 +24,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  enumerable: true
25
25
  }) : target, mod));
26
26
  //#endregion
27
- const require_dist = require("../dist-DV8KQcWd.js");
27
+ const require_dist = require("../dist-DVkAN1CW.js");
28
28
  let node_crypto = require("node:crypto");
29
29
  require("node:zlib");
30
30
  let node_fs = require("node:fs");
@@ -1,4 +1,4 @@
1
- import { Ft as BaseAddon, Gt as array, Ht as isReadingQuantity, I as consumableFormOf, L as consumableKindOf, Lt as baseUnitOf, O as buildAddonRouteProvider, Qt as string, U as deviceExportCapability, Ut as nodePin, Xt as object, a as COCO_TO_MACRO, c as ConsumableItemSchema, d as FanDirectionSchema, en as unknown, f as HvacModeSchema, i as CAMERA_SWITCH_ORDER, l as ConsumableStatusSchema, m as PLUG_APPLIANCE_TYPES, nn as EventCategory, o as CameraSwitchIdSchema, p as MediaPlayerRepeatSchema, s as ConsumableFormSchema, st as oauthIntegrationCapability, t as AlarmArmModeSchema, v as addonRoutesCapability } from "../dist-CdqN_VJZ.mjs";
1
+ import { Ft as BaseAddon, Gt as array, Ht as isReadingQuantity, I as consumableFormOf, L as consumableKindOf, Lt as baseUnitOf, O as buildAddonRouteProvider, Qt as string, U as deviceExportCapability, Ut as nodePin, Xt as object, a as COCO_TO_MACRO, c as ConsumableItemSchema, d as FanDirectionSchema, en as unknown, f as HvacModeSchema, i as CAMERA_SWITCH_ORDER, l as ConsumableStatusSchema, m as PLUG_APPLIANCE_TYPES, nn as EventCategory, o as CameraSwitchIdSchema, p as MediaPlayerRepeatSchema, s as ConsumableFormSchema, st as oauthIntegrationCapability, t as AlarmArmModeSchema, v as addonRoutesCapability } from "../dist-BVojc0hA.mjs";
2
2
  import { createHmac, timingSafeEqual } from "node:crypto";
3
3
  import "node:zlib";
4
4
  import "node:fs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-provider-homeassistant",
3
- "version": "1.2.631",
3
+ "version": "1.2.633",
4
4
  "description": "Home Assistant device provider addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",
@@ -39,7 +39,7 @@
39
39
  {
40
40
  "id": "provider-homeassistant",
41
41
  "category": "messaging",
42
- "name": "Home Assistant Provider",
42
+ "name": "Home Assistant",
43
43
  "version": "0.1.0",
44
44
  "description": "Adopt Home Assistant devices into CamStack via WebSocket",
45
45
  "entry": "./dist/addon.js",