@camstack/addon-provider-homeassistant 1.2.650 → 1.2.652
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 +13 -0
- package/dist/addon.js +199 -1
- package/dist/addon.mjs +199 -1
- package/dist/{ha-broker-source-B2v2dnPe.mjs → ha-broker-source-CBWl6Wat.mjs} +369 -130
- package/dist/{ha-broker-source-DUj9pUWr.js → ha-broker-source-D3ScsO0T.js} +374 -129
- package/dist/ha-export/ha-export.addon.js +73 -8
- package/dist/ha-export/ha-export.addon.mjs +73 -8
- package/package.json +5 -2
|
@@ -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_ha_broker_source = require("../ha-broker-source-
|
|
27
|
+
const require_ha_broker_source = require("../ha-broker-source-D3ScsO0T.js");
|
|
28
28
|
let node_crypto = require("node:crypto");
|
|
29
29
|
require("node:zlib");
|
|
30
30
|
let node_fs = require("node:fs");
|
|
@@ -3522,7 +3522,11 @@ var CAPS_NOT_EXPORTED = [
|
|
|
3522
3522
|
},
|
|
3523
3523
|
{
|
|
3524
3524
|
cap: "pet-fountain",
|
|
3525
|
-
reason: "A fountain is BUILT here from the loose entities Home Assistant already holds (a pump switch,
|
|
3525
|
+
reason: "A fountain is BUILT here from the loose entities Home Assistant already holds (a pump switch, a button, sensors), so exporting it back would publish a second copy of entities that already exist there. A dedicated mapping can follow when an ecosystem asks for the built device."
|
|
3526
|
+
},
|
|
3527
|
+
{
|
|
3528
|
+
cap: "pump-cycle",
|
|
3529
|
+
reason: "The pump duty cycle of a fountain BUILT here from a cycle switch and two number boxes Home Assistant already holds, so exporting it back would publish a second copy of entities that already exist there."
|
|
3526
3530
|
},
|
|
3527
3531
|
{
|
|
3528
3532
|
cap: "ir-transmitter",
|
|
@@ -5491,6 +5495,10 @@ var PushClient = class {
|
|
|
5491
5495
|
disposed = false;
|
|
5492
5496
|
lastFailure = null;
|
|
5493
5497
|
suspendedUntil = 0;
|
|
5498
|
+
/** When the current unbroken streak of 404s began; 0 when there is none. */
|
|
5499
|
+
first404At = 0;
|
|
5500
|
+
/** A suspension was entered and not yet logged as left. */
|
|
5501
|
+
suspensionLogged = false;
|
|
5494
5502
|
constructor(options) {
|
|
5495
5503
|
this.transport = options.transport;
|
|
5496
5504
|
this.logger = options.logger;
|
|
@@ -5509,9 +5517,20 @@ var PushClient = class {
|
|
|
5509
5517
|
get suspended() {
|
|
5510
5518
|
return this.suspendedUntil > Date.now();
|
|
5511
5519
|
}
|
|
5520
|
+
/** Epoch ms of the next probe while suspended; 0 otherwise. */
|
|
5521
|
+
get recheckAt() {
|
|
5522
|
+
return this.suspended ? this.suspendedUntil : 0;
|
|
5523
|
+
}
|
|
5512
5524
|
/** Probe again at once — the operator changed something. */
|
|
5513
5525
|
resetBackoff() {
|
|
5514
5526
|
this.suspendedUntil = 0;
|
|
5527
|
+
this.first404At = 0;
|
|
5528
|
+
this.logSuspensionEnded("reset by a settings change");
|
|
5529
|
+
}
|
|
5530
|
+
logSuspensionEnded(why) {
|
|
5531
|
+
if (!this.suspensionLogged) return;
|
|
5532
|
+
this.suspensionLogged = false;
|
|
5533
|
+
this.logger.info(`ha-export: push link no longer suspended (${why}), probing again`, { meta: { brokerId: this.brokerId } });
|
|
5515
5534
|
}
|
|
5516
5535
|
/** Messages discarded because HA did not answer. Never silent. */
|
|
5517
5536
|
get droppedCount() {
|
|
@@ -5592,8 +5611,13 @@ var PushClient = class {
|
|
|
5592
5611
|
this.dropped += message.type === "batch" ? message.items.length : 1;
|
|
5593
5612
|
return false;
|
|
5594
5613
|
}
|
|
5614
|
+
if (this.suspendedUntil !== 0 && !this.suspended) {
|
|
5615
|
+
this.suspendedUntil = 0;
|
|
5616
|
+
this.logSuspensionEnded("recheck time reached");
|
|
5617
|
+
}
|
|
5595
5618
|
try {
|
|
5596
5619
|
await this.transport(message);
|
|
5620
|
+
this.first404At = 0;
|
|
5597
5621
|
this.onReachable();
|
|
5598
5622
|
return true;
|
|
5599
5623
|
} catch (err) {
|
|
@@ -5620,6 +5644,7 @@ var PushClient = class {
|
|
|
5620
5644
|
this.available = true;
|
|
5621
5645
|
this.lastFailure = null;
|
|
5622
5646
|
this.suspendedUntil = 0;
|
|
5647
|
+
this.logSuspensionEnded("Home Assistant answered");
|
|
5623
5648
|
/**
|
|
5624
5649
|
* The link came back. Drop the dedup cache so the reconcile that
|
|
5625
5650
|
* follows re-pushes every value: HA may have restarted and lost its
|
|
@@ -5657,11 +5682,29 @@ var PushClient = class {
|
|
|
5657
5682
|
*/
|
|
5658
5683
|
onUnreachable(message, err) {
|
|
5659
5684
|
this.available = false;
|
|
5660
|
-
const
|
|
5685
|
+
const now = Date.now();
|
|
5686
|
+
const described = describePushFailure(err);
|
|
5687
|
+
let failure = described;
|
|
5688
|
+
if (described.kind === "component-missing") {
|
|
5689
|
+
if (this.first404At === 0) this.first404At = now;
|
|
5690
|
+
if (now - this.first404At < 6e4) failure = {
|
|
5691
|
+
kind: "other",
|
|
5692
|
+
permanent: false,
|
|
5693
|
+
text: "HTTP 404 — Home Assistant has not registered the CamStack endpoint (yet); retrying"
|
|
5694
|
+
};
|
|
5695
|
+
} else this.first404At = 0;
|
|
5661
5696
|
this.lastFailure = failure;
|
|
5662
|
-
if (failure.permanent) this.suspendedUntil = Date.now() + COMPONENT_RECHECK_MS;
|
|
5663
5697
|
this.dropped += message.type === "batch" ? message.items.length : 1;
|
|
5664
|
-
|
|
5698
|
+
if (failure.permanent) {
|
|
5699
|
+
this.suspendedUntil = now + COMPONENT_RECHECK_MS;
|
|
5700
|
+
this.suspensionLogged = true;
|
|
5701
|
+
this.logger.warn(`ha-export: push link SUSPENDED (${failure.text}); next probe at ${new Date(this.suspendedUntil).toISOString()}`, { meta: {
|
|
5702
|
+
brokerId: this.brokerId,
|
|
5703
|
+
failureKind: failure.kind,
|
|
5704
|
+
recheckAt: this.suspendedUntil,
|
|
5705
|
+
recheckInMs: COMPONENT_RECHECK_MS
|
|
5706
|
+
} });
|
|
5707
|
+
}
|
|
5665
5708
|
if (now - this.droppedLoggedAt < DROP_LOG_INTERVAL_MS) return;
|
|
5666
5709
|
this.droppedLoggedAt = now;
|
|
5667
5710
|
this.logger.warn(`ha-export: push to Home Assistant failed (${failure.text}), messages dropped`, { meta: {
|
|
@@ -5673,6 +5716,19 @@ var PushClient = class {
|
|
|
5673
5716
|
} });
|
|
5674
5717
|
}
|
|
5675
5718
|
};
|
|
5719
|
+
//#endregion
|
|
5720
|
+
//#region src/ha-export/link-gaps.ts
|
|
5721
|
+
/** A parked link is reported, never silently excluded. */
|
|
5722
|
+
function summariseLinkGaps(links) {
|
|
5723
|
+
return {
|
|
5724
|
+
failing: links.filter((l) => !l.healthy && !l.suspended).map((l) => l.brokerId),
|
|
5725
|
+
suspended: links.filter((l) => l.suspended).map((l) => ({
|
|
5726
|
+
brokerId: l.brokerId,
|
|
5727
|
+
recheckAt: l.recheckAt
|
|
5728
|
+
})),
|
|
5729
|
+
anyLive: links.some((l) => !l.suspended)
|
|
5730
|
+
};
|
|
5731
|
+
}
|
|
5676
5732
|
var ReachabilityReconciler = class {
|
|
5677
5733
|
options;
|
|
5678
5734
|
minIntervalMs;
|
|
@@ -8144,13 +8200,21 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
|
|
|
8144
8200
|
const delivered = await this.announce(exported, fullResync);
|
|
8145
8201
|
await this.pushFullState(exported, snapshots);
|
|
8146
8202
|
if (synthetic !== null) await this.pushSynthetic(projectSynthetic(synthetic, Date.now()), [...enabled]);
|
|
8147
|
-
const
|
|
8148
|
-
|
|
8149
|
-
|
|
8203
|
+
const gaps = summariseLinkGaps([...this.links.entries()].map(([brokerId, link]) => ({
|
|
8204
|
+
brokerId,
|
|
8205
|
+
healthy: link.client.healthy,
|
|
8206
|
+
suspended: link.client.suspended,
|
|
8207
|
+
recheckAt: link.client.recheckAt
|
|
8208
|
+
})));
|
|
8209
|
+
if (gaps.failing.length > 0 || !delivered && gaps.anyLive) {
|
|
8150
8210
|
this.resync.markGap("push-not-delivered");
|
|
8151
8211
|
this.lastStructureFingerprint = void 0;
|
|
8152
8212
|
this.ctx.logger.warn("ha-export: a push was not delivered, the next pass is a full resync");
|
|
8153
8213
|
}
|
|
8214
|
+
if (gaps.suspended.length > 0) this.ctx.logger.warn("ha-export: push links suspended, Home Assistant receives nothing", { meta: { suspended: gaps.suspended.map((s) => ({
|
|
8215
|
+
brokerId: s.brokerId,
|
|
8216
|
+
recheckAt: new Date(s.recheckAt).toISOString()
|
|
8217
|
+
})) } });
|
|
8154
8218
|
this.ctx.logger.info("ha-export: reconciled", { meta: {
|
|
8155
8219
|
reason,
|
|
8156
8220
|
fullResync,
|
|
@@ -8158,6 +8222,7 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
|
|
|
8158
8222
|
devices: exported.size,
|
|
8159
8223
|
entities: entityCount,
|
|
8160
8224
|
unclassifiedCaps: this.unclassified.length,
|
|
8225
|
+
suspendedLinks: gaps.suspended.length,
|
|
8161
8226
|
durationMs: Date.now() - startedAt
|
|
8162
8227
|
} });
|
|
8163
8228
|
if (this.unclassified.length > 0) this.ctx.logger.warn("ha-export: capabilities in neither the map nor the exclusion list", { meta: { caps: this.unclassified } });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { B as
|
|
1
|
+
import { B as consumableFormOf, Gt as defineAddonMigrations, M as buildAddonRouteProvider, Ot as sendToExportedTarget, Qt as array, St as runExportedAction, Ut as baseUnitOf, V as consumableKindOf, Vt as BaseAddon, Xt as nodePin, Yt as isReadingQuantity, _ as PLUG_APPLIANCE_TYPES, an as string, c as COCO_TO_MACRO, d as ConsumableItemSchema, dt as oauthIntegrationCapability, f as ConsumableStatusSchema, g as MediaPlayerRepeatSchema, h as HvacModeSchema, i as AlarmArmModeSchema, l as CameraSwitchIdSchema, m as FanDirectionSchema, mt as preconditionOf, pn as EventCategory, q as deviceExportCapability, r as listHomeAssistantBrokers, rn as object, s as CAMERA_SWITCH_ORDER, sn as unknown, u as ConsumableFormSchema, vt as pruneDeletedEntities, x as addonRoutesCapability } from "../ha-broker-source-CBWl6Wat.mjs";
|
|
2
2
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
3
3
|
import "node:zlib";
|
|
4
4
|
import "node:fs";
|
|
@@ -3487,7 +3487,11 @@ var CAPS_NOT_EXPORTED = [
|
|
|
3487
3487
|
},
|
|
3488
3488
|
{
|
|
3489
3489
|
cap: "pet-fountain",
|
|
3490
|
-
reason: "A fountain is BUILT here from the loose entities Home Assistant already holds (a pump switch,
|
|
3490
|
+
reason: "A fountain is BUILT here from the loose entities Home Assistant already holds (a pump switch, a button, sensors), so exporting it back would publish a second copy of entities that already exist there. A dedicated mapping can follow when an ecosystem asks for the built device."
|
|
3491
|
+
},
|
|
3492
|
+
{
|
|
3493
|
+
cap: "pump-cycle",
|
|
3494
|
+
reason: "The pump duty cycle of a fountain BUILT here from a cycle switch and two number boxes Home Assistant already holds, so exporting it back would publish a second copy of entities that already exist there."
|
|
3491
3495
|
},
|
|
3492
3496
|
{
|
|
3493
3497
|
cap: "ir-transmitter",
|
|
@@ -5456,6 +5460,10 @@ var PushClient = class {
|
|
|
5456
5460
|
disposed = false;
|
|
5457
5461
|
lastFailure = null;
|
|
5458
5462
|
suspendedUntil = 0;
|
|
5463
|
+
/** When the current unbroken streak of 404s began; 0 when there is none. */
|
|
5464
|
+
first404At = 0;
|
|
5465
|
+
/** A suspension was entered and not yet logged as left. */
|
|
5466
|
+
suspensionLogged = false;
|
|
5459
5467
|
constructor(options) {
|
|
5460
5468
|
this.transport = options.transport;
|
|
5461
5469
|
this.logger = options.logger;
|
|
@@ -5474,9 +5482,20 @@ var PushClient = class {
|
|
|
5474
5482
|
get suspended() {
|
|
5475
5483
|
return this.suspendedUntil > Date.now();
|
|
5476
5484
|
}
|
|
5485
|
+
/** Epoch ms of the next probe while suspended; 0 otherwise. */
|
|
5486
|
+
get recheckAt() {
|
|
5487
|
+
return this.suspended ? this.suspendedUntil : 0;
|
|
5488
|
+
}
|
|
5477
5489
|
/** Probe again at once — the operator changed something. */
|
|
5478
5490
|
resetBackoff() {
|
|
5479
5491
|
this.suspendedUntil = 0;
|
|
5492
|
+
this.first404At = 0;
|
|
5493
|
+
this.logSuspensionEnded("reset by a settings change");
|
|
5494
|
+
}
|
|
5495
|
+
logSuspensionEnded(why) {
|
|
5496
|
+
if (!this.suspensionLogged) return;
|
|
5497
|
+
this.suspensionLogged = false;
|
|
5498
|
+
this.logger.info(`ha-export: push link no longer suspended (${why}), probing again`, { meta: { brokerId: this.brokerId } });
|
|
5480
5499
|
}
|
|
5481
5500
|
/** Messages discarded because HA did not answer. Never silent. */
|
|
5482
5501
|
get droppedCount() {
|
|
@@ -5557,8 +5576,13 @@ var PushClient = class {
|
|
|
5557
5576
|
this.dropped += message.type === "batch" ? message.items.length : 1;
|
|
5558
5577
|
return false;
|
|
5559
5578
|
}
|
|
5579
|
+
if (this.suspendedUntil !== 0 && !this.suspended) {
|
|
5580
|
+
this.suspendedUntil = 0;
|
|
5581
|
+
this.logSuspensionEnded("recheck time reached");
|
|
5582
|
+
}
|
|
5560
5583
|
try {
|
|
5561
5584
|
await this.transport(message);
|
|
5585
|
+
this.first404At = 0;
|
|
5562
5586
|
this.onReachable();
|
|
5563
5587
|
return true;
|
|
5564
5588
|
} catch (err) {
|
|
@@ -5585,6 +5609,7 @@ var PushClient = class {
|
|
|
5585
5609
|
this.available = true;
|
|
5586
5610
|
this.lastFailure = null;
|
|
5587
5611
|
this.suspendedUntil = 0;
|
|
5612
|
+
this.logSuspensionEnded("Home Assistant answered");
|
|
5588
5613
|
/**
|
|
5589
5614
|
* The link came back. Drop the dedup cache so the reconcile that
|
|
5590
5615
|
* follows re-pushes every value: HA may have restarted and lost its
|
|
@@ -5622,11 +5647,29 @@ var PushClient = class {
|
|
|
5622
5647
|
*/
|
|
5623
5648
|
onUnreachable(message, err) {
|
|
5624
5649
|
this.available = false;
|
|
5625
|
-
const
|
|
5650
|
+
const now = Date.now();
|
|
5651
|
+
const described = describePushFailure(err);
|
|
5652
|
+
let failure = described;
|
|
5653
|
+
if (described.kind === "component-missing") {
|
|
5654
|
+
if (this.first404At === 0) this.first404At = now;
|
|
5655
|
+
if (now - this.first404At < 6e4) failure = {
|
|
5656
|
+
kind: "other",
|
|
5657
|
+
permanent: false,
|
|
5658
|
+
text: "HTTP 404 — Home Assistant has not registered the CamStack endpoint (yet); retrying"
|
|
5659
|
+
};
|
|
5660
|
+
} else this.first404At = 0;
|
|
5626
5661
|
this.lastFailure = failure;
|
|
5627
|
-
if (failure.permanent) this.suspendedUntil = Date.now() + COMPONENT_RECHECK_MS;
|
|
5628
5662
|
this.dropped += message.type === "batch" ? message.items.length : 1;
|
|
5629
|
-
|
|
5663
|
+
if (failure.permanent) {
|
|
5664
|
+
this.suspendedUntil = now + COMPONENT_RECHECK_MS;
|
|
5665
|
+
this.suspensionLogged = true;
|
|
5666
|
+
this.logger.warn(`ha-export: push link SUSPENDED (${failure.text}); next probe at ${new Date(this.suspendedUntil).toISOString()}`, { meta: {
|
|
5667
|
+
brokerId: this.brokerId,
|
|
5668
|
+
failureKind: failure.kind,
|
|
5669
|
+
recheckAt: this.suspendedUntil,
|
|
5670
|
+
recheckInMs: COMPONENT_RECHECK_MS
|
|
5671
|
+
} });
|
|
5672
|
+
}
|
|
5630
5673
|
if (now - this.droppedLoggedAt < DROP_LOG_INTERVAL_MS) return;
|
|
5631
5674
|
this.droppedLoggedAt = now;
|
|
5632
5675
|
this.logger.warn(`ha-export: push to Home Assistant failed (${failure.text}), messages dropped`, { meta: {
|
|
@@ -5638,6 +5681,19 @@ var PushClient = class {
|
|
|
5638
5681
|
} });
|
|
5639
5682
|
}
|
|
5640
5683
|
};
|
|
5684
|
+
//#endregion
|
|
5685
|
+
//#region src/ha-export/link-gaps.ts
|
|
5686
|
+
/** A parked link is reported, never silently excluded. */
|
|
5687
|
+
function summariseLinkGaps(links) {
|
|
5688
|
+
return {
|
|
5689
|
+
failing: links.filter((l) => !l.healthy && !l.suspended).map((l) => l.brokerId),
|
|
5690
|
+
suspended: links.filter((l) => l.suspended).map((l) => ({
|
|
5691
|
+
brokerId: l.brokerId,
|
|
5692
|
+
recheckAt: l.recheckAt
|
|
5693
|
+
})),
|
|
5694
|
+
anyLive: links.some((l) => !l.suspended)
|
|
5695
|
+
};
|
|
5696
|
+
}
|
|
5641
5697
|
var ReachabilityReconciler = class {
|
|
5642
5698
|
options;
|
|
5643
5699
|
minIntervalMs;
|
|
@@ -8109,13 +8165,21 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
8109
8165
|
const delivered = await this.announce(exported, fullResync);
|
|
8110
8166
|
await this.pushFullState(exported, snapshots);
|
|
8111
8167
|
if (synthetic !== null) await this.pushSynthetic(projectSynthetic(synthetic, Date.now()), [...enabled]);
|
|
8112
|
-
const
|
|
8113
|
-
|
|
8114
|
-
|
|
8168
|
+
const gaps = summariseLinkGaps([...this.links.entries()].map(([brokerId, link]) => ({
|
|
8169
|
+
brokerId,
|
|
8170
|
+
healthy: link.client.healthy,
|
|
8171
|
+
suspended: link.client.suspended,
|
|
8172
|
+
recheckAt: link.client.recheckAt
|
|
8173
|
+
})));
|
|
8174
|
+
if (gaps.failing.length > 0 || !delivered && gaps.anyLive) {
|
|
8115
8175
|
this.resync.markGap("push-not-delivered");
|
|
8116
8176
|
this.lastStructureFingerprint = void 0;
|
|
8117
8177
|
this.ctx.logger.warn("ha-export: a push was not delivered, the next pass is a full resync");
|
|
8118
8178
|
}
|
|
8179
|
+
if (gaps.suspended.length > 0) this.ctx.logger.warn("ha-export: push links suspended, Home Assistant receives nothing", { meta: { suspended: gaps.suspended.map((s) => ({
|
|
8180
|
+
brokerId: s.brokerId,
|
|
8181
|
+
recheckAt: new Date(s.recheckAt).toISOString()
|
|
8182
|
+
})) } });
|
|
8119
8183
|
this.ctx.logger.info("ha-export: reconciled", { meta: {
|
|
8120
8184
|
reason,
|
|
8121
8185
|
fullResync,
|
|
@@ -8123,6 +8187,7 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
8123
8187
|
devices: exported.size,
|
|
8124
8188
|
entities: entityCount,
|
|
8125
8189
|
unclassifiedCaps: this.unclassified.length,
|
|
8190
|
+
suspendedLinks: gaps.suspended.length,
|
|
8126
8191
|
durationMs: Date.now() - startedAt
|
|
8127
8192
|
} });
|
|
8128
8193
|
if (this.unclassified.length > 0) this.ctx.logger.warn("ha-export: capabilities in neither the map nor the exclusion list", { meta: { caps: this.unclassified } });
|
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.652",
|
|
4
4
|
"description": "Home Assistant device provider addon for CamStack",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
|
@@ -55,6 +55,9 @@
|
|
|
55
55
|
{
|
|
56
56
|
"name": "broker"
|
|
57
57
|
},
|
|
58
|
+
{
|
|
59
|
+
"name": "board-key-source"
|
|
60
|
+
},
|
|
58
61
|
{
|
|
59
62
|
"name": "device-adoption"
|
|
60
63
|
},
|
|
@@ -127,7 +130,7 @@
|
|
|
127
130
|
"description": "Device provider (import) + native entity export (push). Two addons, one bundle, separate processes."
|
|
128
131
|
},
|
|
129
132
|
"requires": {
|
|
130
|
-
"server": ">=1.2.
|
|
133
|
+
"server": ">=1.2.742"
|
|
131
134
|
}
|
|
132
135
|
},
|
|
133
136
|
"files": [
|