@camstack/addon-provider-homeassistant 1.2.631 → 1.2.632

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/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-D0ZiJUPy.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-CPkvghX8.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({
@@ -65272,9 +65287,9 @@ var PowerMeterStatusSchema = object({
65272
65287
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
65273
65288
  kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
65274
65289
  /** Voltage in volts. */
65275
- volts: number().optional().meta(readingMeta("voltage")),
65290
+ volts: number().optional().meta(readingMeta("voltage", { label: "Voltage" })),
65276
65291
  /** Current in amperes. */
65277
- amps: number().optional().meta(readingMeta("current")),
65292
+ amps: number().optional().meta(readingMeta("current", { label: "Current" })),
65278
65293
  /** Apparent power in volt-amperes. */
65279
65294
  va: number().optional().meta(readingMeta("apparent-power")),
65280
65295
  /** Power factor (0..1) if reported. */
@@ -65282,17 +65297,74 @@ var PowerMeterStatusSchema = object({
65282
65297
  /** Ms epoch when the slice was last updated. */
65283
65298
  lastFetchedAt: number()
65284
65299
  });
65300
+ /**
65301
+ * Loads that draw metered mains power (D951). The reading is a feature of the
65302
+ * device itself — a plug, a lamp, a fan, an air conditioner, a boiler, a
65303
+ * vacuum dock — not a separate sensor beside it. A `Sensor` stays: a clamp
65304
+ * meter or a standalone energy monitor IS the device. Cover, lock, valve,
65305
+ * camera and the like are left out: their draw is not what anyone meters.
65306
+ */
65307
+ var POWER_METER_DEVICE_TYPES = [
65308
+ DeviceType.Sensor,
65309
+ DeviceType.Switch,
65310
+ DeviceType.Light,
65311
+ DeviceType.Fan,
65312
+ DeviceType.Climate,
65313
+ DeviceType.Thermostat,
65314
+ DeviceType.Humidifier,
65315
+ DeviceType.WaterHeater,
65316
+ DeviceType.MediaPlayer,
65317
+ DeviceType.Washer,
65318
+ DeviceType.Dryer,
65319
+ DeviceType.Dishwasher,
65320
+ DeviceType.Vacuum,
65321
+ DeviceType.LawnMower,
65322
+ DeviceType.PetFeeder
65323
+ ];
65324
+ /** Everything but a bare `Sensor`: a sensor that takes its reading from another sensor adds nothing. */
65325
+ var POWER_METER_CUSTOMIZE_DEVICE_TYPES = POWER_METER_DEVICE_TYPES.filter((type) => type !== DeviceType.Sensor);
65326
+ /** One scalar target per reading; each is sourced from a device of its own (D951). */
65327
+ function powerTarget(id, unit) {
65328
+ return {
65329
+ id,
65330
+ cardinality: "scalar",
65331
+ vocabulary: {
65332
+ cap: "power-meter",
65333
+ stateField: id
65334
+ },
65335
+ deviceTypes: POWER_METER_CUSTOMIZE_DEVICE_TYPES,
65336
+ apply: "add",
65337
+ path: id,
65338
+ sources: [{
65339
+ kind: "numeric",
65340
+ requiredUnit: unit,
65341
+ transforms: [{ kind: "identity" }]
65342
+ }],
65343
+ companions: [{
65344
+ kind: "clock",
65345
+ path: "lastFetchedAt",
65346
+ when: "always"
65347
+ }],
65348
+ sourceReport: { status: "assignment-status" },
65349
+ advancedExpression: true
65350
+ };
65351
+ }
65352
+ /** "Power and energy" extension: watts, energy, voltage and current from other devices (D951). */
65353
+ var powerMeterCustomize = {
65354
+ version: 1,
65355
+ targets: [
65356
+ powerTarget("watts", "W"),
65357
+ powerTarget("kwhTotal", "kWh"),
65358
+ powerTarget("volts", "V"),
65359
+ powerTarget("amps", "A")
65360
+ ]
65361
+ };
65285
65362
  var powerMeterCapability = {
65286
65363
  name: "power-meter",
65287
65364
  scope: "device",
65288
65365
  deviceNative: true,
65289
65366
  mode: "singleton",
65290
- deviceTypes: [
65291
- DeviceType.Sensor,
65292
- DeviceType.Washer,
65293
- DeviceType.Dryer,
65294
- DeviceType.Dishwasher
65295
- ],
65367
+ deviceTypes: POWER_METER_DEVICE_TYPES,
65296
65368
  methods: {},
65297
65369
  status: {
65298
65370
  schema: PowerMeterStatusSchema,
@@ -65349,7 +65421,8 @@ var powerMeterCapability = {
65349
65421
  } },
65350
65422
  /** Clock fields: written, but excluded from the compare that decides
65351
65423
  * whether persisting is worth a SQLite commit. */
65352
- volatileStateFields: ["lastFetchedAt"]
65424
+ volatileStateFields: ["lastFetchedAt"],
65425
+ customize: powerMeterCustomize
65353
65426
  };
65354
65427
  /**
65355
65428
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -71180,7 +71253,7 @@ function tryConvertDelta(value, from, to) {
71180
71253
  return shifted - origin;
71181
71254
  }
71182
71255
  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;
71256
+ 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.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
71257
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
71185
71258
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
71186
71259
  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({
@@ -65273,9 +65288,9 @@ var PowerMeterStatusSchema = object({
65273
65288
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
65274
65289
  kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
65275
65290
  /** Voltage in volts. */
65276
- volts: number().optional().meta(readingMeta("voltage")),
65291
+ volts: number().optional().meta(readingMeta("voltage", { label: "Voltage" })),
65277
65292
  /** Current in amperes. */
65278
- amps: number().optional().meta(readingMeta("current")),
65293
+ amps: number().optional().meta(readingMeta("current", { label: "Current" })),
65279
65294
  /** Apparent power in volt-amperes. */
65280
65295
  va: number().optional().meta(readingMeta("apparent-power")),
65281
65296
  /** Power factor (0..1) if reported. */
@@ -65283,17 +65298,74 @@ var PowerMeterStatusSchema = object({
65283
65298
  /** Ms epoch when the slice was last updated. */
65284
65299
  lastFetchedAt: number()
65285
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
+ };
65286
65363
  var powerMeterCapability = {
65287
65364
  name: "power-meter",
65288
65365
  scope: "device",
65289
65366
  deviceNative: true,
65290
65367
  mode: "singleton",
65291
- deviceTypes: [
65292
- DeviceType.Sensor,
65293
- DeviceType.Washer,
65294
- DeviceType.Dryer,
65295
- DeviceType.Dishwasher
65296
- ],
65368
+ deviceTypes: POWER_METER_DEVICE_TYPES,
65297
65369
  methods: {},
65298
65370
  status: {
65299
65371
  schema: PowerMeterStatusSchema,
@@ -65350,7 +65422,8 @@ var powerMeterCapability = {
65350
65422
  } },
65351
65423
  /** Clock fields: written, but excluded from the compare that decides
65352
65424
  * whether persisting is worth a SQLite commit. */
65353
- volatileStateFields: ["lastFetchedAt"]
65425
+ volatileStateFields: ["lastFetchedAt"],
65426
+ customize: powerMeterCustomize
65354
65427
  };
65355
65428
  /**
65356
65429
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -71181,7 +71254,7 @@ function tryConvertDelta(value, from, to) {
71181
71254
  return shifted - origin;
71182
71255
  }
71183
71256
  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;
71257
+ 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.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
71258
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
71186
71259
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
71187
71260
  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-D0ZiJUPy.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-CPkvghX8.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.632",
4
4
  "description": "Home Assistant device provider addon for CamStack",
5
5
  "keywords": [
6
6
  "camstack",