@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 +10 -1
- package/README.md +31 -11
- package/dist/addon.js +208 -32
- package/dist/addon.mjs +208 -32
- package/dist/{dist-CdqN_VJZ.mjs → dist-BVojc0hA.mjs} +87 -13
- package/dist/{dist-DV8KQcWd.js → dist-DVkAN1CW.js} +87 -13
- package/dist/ha-export/ha-export.addon.js +1 -1
- package/dist/ha-export/ha-export.addon.mjs +1 -1
- package/package.json +2 -2
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
|
-
-
|
|
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
|
-
#
|
|
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
|
-
|
|
5
|
+
<!-- readme:generated:start (derived from the manifest; edit outside this block) -->
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
-
|
|
21
|
-
- A CamStack hub. An agent is used only
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
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-
|
|
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-
|
|
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.
|
|
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
|
|
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",
|