@camstack/addon-provider-homeassistant 1.2.650 → 1.2.651
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 +6 -0
- package/dist/addon.js +1 -1
- package/dist/addon.mjs +1 -1
- package/dist/{ha-broker-source-B2v2dnPe.mjs → ha-broker-source-CCmZ9I6P.mjs} +85 -0
- package/dist/{ha-broker-source-DUj9pUWr.js → ha-broker-source-D4_TtF2V.js} +85 -0
- package/dist/ha-export/ha-export.addon.js +68 -7
- package/dist/ha-export/ha-export.addon.mjs +68 -7
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.2.651] - 2026-11-02
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- A Home Assistant restart no longer parks the link for 10 minutes: a 404 must persist for a minute before the component counts as missing, and a suspended link is now logged and reported on every reconcile
|
|
15
|
+
|
|
10
16
|
## [1.2.650] - 2026-11-02
|
|
11
17
|
|
|
12
18
|
### Changed
|
package/dist/addon.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_ha_broker_source = require("./ha-broker-source-
|
|
2
|
+
const require_ha_broker_source = require("./ha-broker-source-D4_TtF2V.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
|
package/dist/addon.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as humiditySensorCapability, $t as discriminatedUnion, A as brokerCapability, At as tamperCapability, C as alarmPanelCapability, Ct as runningFromMediaPlayer, D as bestLocationMatch, E as batteryCapability, Et as scriptRunnerCapability, F as carbonMonoxideCapability, Ft as vacuumControlCapability, G as deviceAdoptionCapability, Gt as defineEntryKind, H as controlCapability, Ht as baseUnitOf, I as climateControlCapability, It as valveCapability, J as eventEmitterCapability, Kt as defineRecordKinds, L as colorCapability, Lt as vibrationCapability, M as buildDeviceAdminUrl, Mt as tryConvertDelta, N as buttonCapability, Nt as tryConvertUnit, O as binaryCapability, Ot as smokeCapability, P as calendarSourceCapability, Pt as updateCapability, Q as humidifierCapability, Qt as boolean, R as connectivityCapability, Rt as waterHeaterCapability, S as airQualitySensorCapability, St as runningFromLawnMower, T as automationControlCapability, Tt as runningStateCapability, U as coverCapability, Ut as createEvent, V as contactCapability, Vt as RecordSecretUnavailableError, W as deviceAdminLinkCapability, X as floodCapability, Xt as _enum, Y as fanControlCapability, Z as gasCapability, Zt as array, a as BaseDevice, an as union, at as motionCapability, b as accessoriesCapability, bt as resolveAttachmentUrls, cn as DeviceFeature, ct as notifierCapability, dn as errMsg, dt as parseAdminBase, en as literal, et as imageCapability, fn as EventCategory, ft as powerMeterCapability, gt as pressureSensorCapability, ht as presenceCapability, in as string, it as mediaPlayerCapability, j as buildAddonRouteProvider, jt as temperatureSensorCapability, k as brightnessCapability, kt as switchCapability, ln as DeviceRole, lt as numericSensorCapability, mt as prepareNotification, nn as object, nt as lawnMowerControlCapability, o as BaseDeviceProvider, on as unknown, ot as normalizeUnit, p as EnumSensorDateTimeFormatSchema, q as enumSensorCapability, qt as entryBody, rn as record, rt as lockControlCapability, sn as url, st as notificationOutputCapability, t as HA_BROKER_KIND, tn as number, tt as ingestReading, un as DeviceType, v as RUNNING_STATE_COLD, vt as publicEndpointReaderFrom, w as ambientLightSensorCapability, wt as runningFromVacuumState, x as addonRoutesCapability, y as TargetSchema, yt as quantityFromUnit, zt as weatherCapability } from "./ha-broker-source-
|
|
1
|
+
import { $ as humiditySensorCapability, $t as discriminatedUnion, A as brokerCapability, At as tamperCapability, C as alarmPanelCapability, Ct as runningFromMediaPlayer, D as bestLocationMatch, E as batteryCapability, Et as scriptRunnerCapability, F as carbonMonoxideCapability, Ft as vacuumControlCapability, G as deviceAdoptionCapability, Gt as defineEntryKind, H as controlCapability, Ht as baseUnitOf, I as climateControlCapability, It as valveCapability, J as eventEmitterCapability, Kt as defineRecordKinds, L as colorCapability, Lt as vibrationCapability, M as buildDeviceAdminUrl, Mt as tryConvertDelta, N as buttonCapability, Nt as tryConvertUnit, O as binaryCapability, Ot as smokeCapability, P as calendarSourceCapability, Pt as updateCapability, Q as humidifierCapability, Qt as boolean, R as connectivityCapability, Rt as waterHeaterCapability, S as airQualitySensorCapability, St as runningFromLawnMower, T as automationControlCapability, Tt as runningStateCapability, U as coverCapability, Ut as createEvent, V as contactCapability, Vt as RecordSecretUnavailableError, W as deviceAdminLinkCapability, X as floodCapability, Xt as _enum, Y as fanControlCapability, Z as gasCapability, Zt as array, a as BaseDevice, an as union, at as motionCapability, b as accessoriesCapability, bt as resolveAttachmentUrls, cn as DeviceFeature, ct as notifierCapability, dn as errMsg, dt as parseAdminBase, en as literal, et as imageCapability, fn as EventCategory, ft as powerMeterCapability, gt as pressureSensorCapability, ht as presenceCapability, in as string, it as mediaPlayerCapability, j as buildAddonRouteProvider, jt as temperatureSensorCapability, k as brightnessCapability, kt as switchCapability, ln as DeviceRole, lt as numericSensorCapability, mt as prepareNotification, nn as object, nt as lawnMowerControlCapability, o as BaseDeviceProvider, on as unknown, ot as normalizeUnit, p as EnumSensorDateTimeFormatSchema, q as enumSensorCapability, qt as entryBody, rn as record, rt as lockControlCapability, sn as url, st as notificationOutputCapability, t as HA_BROKER_KIND, tn as number, tt as ingestReading, un as DeviceType, v as RUNNING_STATE_COLD, vt as publicEndpointReaderFrom, w as ambientLightSensorCapability, wt as runningFromVacuumState, x as addonRoutesCapability, y as TargetSchema, yt as quantityFromUnit, zt as weatherCapability } from "./ha-broker-source-CCmZ9I6P.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
|
|
@@ -12974,6 +12974,73 @@ var NodeColorIdSchema = _enum([
|
|
|
12974
12974
|
"node-g",
|
|
12975
12975
|
"node-h"
|
|
12976
12976
|
]);
|
|
12977
|
+
//#endregion
|
|
12978
|
+
//#region ../types/dist/index.mjs
|
|
12979
|
+
var SMTP_INBOUND_DEFAULT_PORT = 2525;
|
|
12980
|
+
var SMTP_INBOUND_DEFAULT_DOMAIN = "camstack.local";
|
|
12981
|
+
var SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES = 25 * 1024 * 1024;
|
|
12982
|
+
/** The text excerpt a message carries on the bus. */
|
|
12983
|
+
var SMTP_INBOUND_TEXT_MAX_CHARS = 4096;
|
|
12984
|
+
var PortSchema = number().int().min(1).max(65535);
|
|
12985
|
+
/** A recipient local-part: what stands before the `@`. */
|
|
12986
|
+
var SmtpRecipientSchema = string().min(1).max(64).regex(/^[A-Za-z0-9._+-]+$/);
|
|
12987
|
+
object({
|
|
12988
|
+
port: PortSchema.default(SMTP_INBOUND_DEFAULT_PORT),
|
|
12989
|
+
bindHost: string().min(1).default("0.0.0.0"),
|
|
12990
|
+
domain: string().min(1).default(SMTP_INBOUND_DEFAULT_DOMAIN),
|
|
12991
|
+
requireAuth: boolean().default(true),
|
|
12992
|
+
username: string().default(""),
|
|
12993
|
+
password: string().default("").meta({ secret: true }),
|
|
12994
|
+
starttls: boolean().default(false),
|
|
12995
|
+
maxMessageBytes: number().int().positive().default(SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES)
|
|
12996
|
+
});
|
|
12997
|
+
object({
|
|
12998
|
+
host: string().min(1).nullable(),
|
|
12999
|
+
hostReason: string().nullable(),
|
|
13000
|
+
port: PortSchema,
|
|
13001
|
+
domain: string().min(1),
|
|
13002
|
+
requireAuth: boolean(),
|
|
13003
|
+
username: string(),
|
|
13004
|
+
password: string(),
|
|
13005
|
+
starttls: boolean()
|
|
13006
|
+
});
|
|
13007
|
+
object({
|
|
13008
|
+
recipients: array(SmtpRecipientSchema).min(1),
|
|
13009
|
+
/**
|
|
13010
|
+
* Who is subscribing. A second `subscribe` with the same id REPLACES that subscriber's
|
|
13011
|
+
* recipients (the way a consumer that restarted, or lost a device, declares its set again);
|
|
13012
|
+
* a recipient held by another subscriber is refused with `smtp-inbound/recipient-claimed`.
|
|
13013
|
+
* Absent, the subscriber is anonymous and unique.
|
|
13014
|
+
*/
|
|
13015
|
+
subscriberId: string().min(1).max(64).optional()
|
|
13016
|
+
});
|
|
13017
|
+
var SmtpInboundAttachmentSchema = object({
|
|
13018
|
+
filename: string(),
|
|
13019
|
+
contentType: string(),
|
|
13020
|
+
bytes: number().int().nonnegative()
|
|
13021
|
+
});
|
|
13022
|
+
/** The payload of one `broker.message` (the key is the recipient local-part). */
|
|
13023
|
+
var SmtpInboundMessageSchema = object({
|
|
13024
|
+
/** Monotonic per mailbox; a gap tells a consumer it missed an event. */
|
|
13025
|
+
seq: number().int().positive(),
|
|
13026
|
+
messageId: string(),
|
|
13027
|
+
receivedAt: number().int().nonnegative(),
|
|
13028
|
+
from: string(),
|
|
13029
|
+
to: string(),
|
|
13030
|
+
subject: string(),
|
|
13031
|
+
text: string().max(SMTP_INBOUND_TEXT_MAX_CHARS),
|
|
13032
|
+
attachments: array(SmtpInboundAttachmentSchema)
|
|
13033
|
+
});
|
|
13034
|
+
object({
|
|
13035
|
+
/** The newest seq issued so far (0 before any mail). */
|
|
13036
|
+
lastSeq: number().int().nonnegative(),
|
|
13037
|
+
/** The oldest seq still retained; messages below it are gone. */
|
|
13038
|
+
oldestSeq: number().int().nonnegative(),
|
|
13039
|
+
messages: array(object({
|
|
13040
|
+
key: string(),
|
|
13041
|
+
message: SmtpInboundMessageSchema
|
|
13042
|
+
}))
|
|
13043
|
+
});
|
|
12977
13044
|
new Set([
|
|
12978
13045
|
"track",
|
|
12979
13046
|
"face",
|
|
@@ -55900,6 +55967,11 @@ var IntegrationInstanceSchema = object({
|
|
|
55900
55967
|
name: string(),
|
|
55901
55968
|
/** The addon's own rule (an URL is present, a client id is present...). */
|
|
55902
55969
|
configured: boolean(),
|
|
55970
|
+
/**
|
|
55971
|
+
* The instance's own on/off switch (D996): always present, derived from the record body
|
|
55972
|
+
* (`enabled !== false`, so a row written before the field existed is on).
|
|
55973
|
+
*/
|
|
55974
|
+
enabled: boolean(),
|
|
55903
55975
|
/** Values for the instance form; a secret is a `SecretFieldView`, never text. */
|
|
55904
55976
|
formValues: record(string(), unknown()),
|
|
55905
55977
|
status: IntegrationInstanceStatusSchema.optional()
|
|
@@ -55919,6 +55991,12 @@ method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }), method
|
|
|
55919
55991
|
}), method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
|
|
55920
55992
|
kind: "mutation",
|
|
55921
55993
|
auth: "admin"
|
|
55994
|
+
}), method(object({
|
|
55995
|
+
id: IntegrationInstanceIdSchema,
|
|
55996
|
+
enabled: boolean()
|
|
55997
|
+
}), object({ ok: literal(true) }), {
|
|
55998
|
+
kind: "mutation",
|
|
55999
|
+
auth: "admin"
|
|
55922
56000
|
}), method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
|
|
55923
56001
|
kind: "mutation",
|
|
55924
56002
|
auth: "admin"
|
|
@@ -85446,6 +85524,13 @@ Object.freeze({
|
|
|
85446
85524
|
access: "view",
|
|
85447
85525
|
role: "admin"
|
|
85448
85526
|
},
|
|
85527
|
+
"integrationInstances.setInstanceEnabled": {
|
|
85528
|
+
capName: "integration-instances",
|
|
85529
|
+
capScope: "system",
|
|
85530
|
+
addonId: null,
|
|
85531
|
+
access: "create",
|
|
85532
|
+
role: "admin"
|
|
85533
|
+
},
|
|
85449
85534
|
"integrationInstances.testInstance": {
|
|
85450
85535
|
capName: "integration-instances",
|
|
85451
85536
|
capScope: "system",
|
|
@@ -12975,6 +12975,73 @@ var NodeColorIdSchema = _enum([
|
|
|
12975
12975
|
"node-g",
|
|
12976
12976
|
"node-h"
|
|
12977
12977
|
]);
|
|
12978
|
+
//#endregion
|
|
12979
|
+
//#region ../types/dist/index.mjs
|
|
12980
|
+
var SMTP_INBOUND_DEFAULT_PORT = 2525;
|
|
12981
|
+
var SMTP_INBOUND_DEFAULT_DOMAIN = "camstack.local";
|
|
12982
|
+
var SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES = 25 * 1024 * 1024;
|
|
12983
|
+
/** The text excerpt a message carries on the bus. */
|
|
12984
|
+
var SMTP_INBOUND_TEXT_MAX_CHARS = 4096;
|
|
12985
|
+
var PortSchema = number().int().min(1).max(65535);
|
|
12986
|
+
/** A recipient local-part: what stands before the `@`. */
|
|
12987
|
+
var SmtpRecipientSchema = string().min(1).max(64).regex(/^[A-Za-z0-9._+-]+$/);
|
|
12988
|
+
object({
|
|
12989
|
+
port: PortSchema.default(SMTP_INBOUND_DEFAULT_PORT),
|
|
12990
|
+
bindHost: string().min(1).default("0.0.0.0"),
|
|
12991
|
+
domain: string().min(1).default(SMTP_INBOUND_DEFAULT_DOMAIN),
|
|
12992
|
+
requireAuth: boolean().default(true),
|
|
12993
|
+
username: string().default(""),
|
|
12994
|
+
password: string().default("").meta({ secret: true }),
|
|
12995
|
+
starttls: boolean().default(false),
|
|
12996
|
+
maxMessageBytes: number().int().positive().default(SMTP_INBOUND_DEFAULT_MAX_MESSAGE_BYTES)
|
|
12997
|
+
});
|
|
12998
|
+
object({
|
|
12999
|
+
host: string().min(1).nullable(),
|
|
13000
|
+
hostReason: string().nullable(),
|
|
13001
|
+
port: PortSchema,
|
|
13002
|
+
domain: string().min(1),
|
|
13003
|
+
requireAuth: boolean(),
|
|
13004
|
+
username: string(),
|
|
13005
|
+
password: string(),
|
|
13006
|
+
starttls: boolean()
|
|
13007
|
+
});
|
|
13008
|
+
object({
|
|
13009
|
+
recipients: array(SmtpRecipientSchema).min(1),
|
|
13010
|
+
/**
|
|
13011
|
+
* Who is subscribing. A second `subscribe` with the same id REPLACES that subscriber's
|
|
13012
|
+
* recipients (the way a consumer that restarted, or lost a device, declares its set again);
|
|
13013
|
+
* a recipient held by another subscriber is refused with `smtp-inbound/recipient-claimed`.
|
|
13014
|
+
* Absent, the subscriber is anonymous and unique.
|
|
13015
|
+
*/
|
|
13016
|
+
subscriberId: string().min(1).max(64).optional()
|
|
13017
|
+
});
|
|
13018
|
+
var SmtpInboundAttachmentSchema = object({
|
|
13019
|
+
filename: string(),
|
|
13020
|
+
contentType: string(),
|
|
13021
|
+
bytes: number().int().nonnegative()
|
|
13022
|
+
});
|
|
13023
|
+
/** The payload of one `broker.message` (the key is the recipient local-part). */
|
|
13024
|
+
var SmtpInboundMessageSchema = object({
|
|
13025
|
+
/** Monotonic per mailbox; a gap tells a consumer it missed an event. */
|
|
13026
|
+
seq: number().int().positive(),
|
|
13027
|
+
messageId: string(),
|
|
13028
|
+
receivedAt: number().int().nonnegative(),
|
|
13029
|
+
from: string(),
|
|
13030
|
+
to: string(),
|
|
13031
|
+
subject: string(),
|
|
13032
|
+
text: string().max(SMTP_INBOUND_TEXT_MAX_CHARS),
|
|
13033
|
+
attachments: array(SmtpInboundAttachmentSchema)
|
|
13034
|
+
});
|
|
13035
|
+
object({
|
|
13036
|
+
/** The newest seq issued so far (0 before any mail). */
|
|
13037
|
+
lastSeq: number().int().nonnegative(),
|
|
13038
|
+
/** The oldest seq still retained; messages below it are gone. */
|
|
13039
|
+
oldestSeq: number().int().nonnegative(),
|
|
13040
|
+
messages: array(object({
|
|
13041
|
+
key: string(),
|
|
13042
|
+
message: SmtpInboundMessageSchema
|
|
13043
|
+
}))
|
|
13044
|
+
});
|
|
12978
13045
|
new Set([
|
|
12979
13046
|
"track",
|
|
12980
13047
|
"face",
|
|
@@ -55901,6 +55968,11 @@ var IntegrationInstanceSchema = object({
|
|
|
55901
55968
|
name: string(),
|
|
55902
55969
|
/** The addon's own rule (an URL is present, a client id is present...). */
|
|
55903
55970
|
configured: boolean(),
|
|
55971
|
+
/**
|
|
55972
|
+
* The instance's own on/off switch (D996): always present, derived from the record body
|
|
55973
|
+
* (`enabled !== false`, so a row written before the field existed is on).
|
|
55974
|
+
*/
|
|
55975
|
+
enabled: boolean(),
|
|
55904
55976
|
/** Values for the instance form; a secret is a `SecretFieldView`, never text. */
|
|
55905
55977
|
formValues: record(string(), unknown()),
|
|
55906
55978
|
status: IntegrationInstanceStatusSchema.optional()
|
|
@@ -55920,6 +55992,12 @@ method(_void(), IntegrationInstanceDescriptionSchema, { auth: "admin" }), method
|
|
|
55920
55992
|
}), method(IntegrationInstancePatchSchema, object({ ok: literal(true) }), {
|
|
55921
55993
|
kind: "mutation",
|
|
55922
55994
|
auth: "admin"
|
|
55995
|
+
}), method(object({
|
|
55996
|
+
id: IntegrationInstanceIdSchema,
|
|
55997
|
+
enabled: boolean()
|
|
55998
|
+
}), object({ ok: literal(true) }), {
|
|
55999
|
+
kind: "mutation",
|
|
56000
|
+
auth: "admin"
|
|
55923
56001
|
}), method(object({ id: IntegrationInstanceIdSchema }), object({ ok: literal(true) }), {
|
|
55924
56002
|
kind: "mutation",
|
|
55925
56003
|
auth: "admin"
|
|
@@ -85447,6 +85525,13 @@ Object.freeze({
|
|
|
85447
85525
|
access: "view",
|
|
85448
85526
|
role: "admin"
|
|
85449
85527
|
},
|
|
85528
|
+
"integrationInstances.setInstanceEnabled": {
|
|
85529
|
+
capName: "integration-instances",
|
|
85530
|
+
capScope: "system",
|
|
85531
|
+
addonId: null,
|
|
85532
|
+
access: "create",
|
|
85533
|
+
role: "admin"
|
|
85534
|
+
},
|
|
85450
85535
|
"integrationInstances.testInstance": {
|
|
85451
85536
|
capName: "integration-instances",
|
|
85452
85537
|
capScope: "system",
|
|
@@ -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-D4_TtF2V.js");
|
|
28
28
|
let node_crypto = require("node:crypto");
|
|
29
29
|
require("node:zlib");
|
|
30
30
|
let node_fs = require("node:fs");
|
|
@@ -5491,6 +5491,10 @@ var PushClient = class {
|
|
|
5491
5491
|
disposed = false;
|
|
5492
5492
|
lastFailure = null;
|
|
5493
5493
|
suspendedUntil = 0;
|
|
5494
|
+
/** When the current unbroken streak of 404s began; 0 when there is none. */
|
|
5495
|
+
first404At = 0;
|
|
5496
|
+
/** A suspension was entered and not yet logged as left. */
|
|
5497
|
+
suspensionLogged = false;
|
|
5494
5498
|
constructor(options) {
|
|
5495
5499
|
this.transport = options.transport;
|
|
5496
5500
|
this.logger = options.logger;
|
|
@@ -5509,9 +5513,20 @@ var PushClient = class {
|
|
|
5509
5513
|
get suspended() {
|
|
5510
5514
|
return this.suspendedUntil > Date.now();
|
|
5511
5515
|
}
|
|
5516
|
+
/** Epoch ms of the next probe while suspended; 0 otherwise. */
|
|
5517
|
+
get recheckAt() {
|
|
5518
|
+
return this.suspended ? this.suspendedUntil : 0;
|
|
5519
|
+
}
|
|
5512
5520
|
/** Probe again at once — the operator changed something. */
|
|
5513
5521
|
resetBackoff() {
|
|
5514
5522
|
this.suspendedUntil = 0;
|
|
5523
|
+
this.first404At = 0;
|
|
5524
|
+
this.logSuspensionEnded("reset by a settings change");
|
|
5525
|
+
}
|
|
5526
|
+
logSuspensionEnded(why) {
|
|
5527
|
+
if (!this.suspensionLogged) return;
|
|
5528
|
+
this.suspensionLogged = false;
|
|
5529
|
+
this.logger.info(`ha-export: push link no longer suspended (${why}), probing again`, { meta: { brokerId: this.brokerId } });
|
|
5515
5530
|
}
|
|
5516
5531
|
/** Messages discarded because HA did not answer. Never silent. */
|
|
5517
5532
|
get droppedCount() {
|
|
@@ -5592,8 +5607,13 @@ var PushClient = class {
|
|
|
5592
5607
|
this.dropped += message.type === "batch" ? message.items.length : 1;
|
|
5593
5608
|
return false;
|
|
5594
5609
|
}
|
|
5610
|
+
if (this.suspendedUntil !== 0 && !this.suspended) {
|
|
5611
|
+
this.suspendedUntil = 0;
|
|
5612
|
+
this.logSuspensionEnded("recheck time reached");
|
|
5613
|
+
}
|
|
5595
5614
|
try {
|
|
5596
5615
|
await this.transport(message);
|
|
5616
|
+
this.first404At = 0;
|
|
5597
5617
|
this.onReachable();
|
|
5598
5618
|
return true;
|
|
5599
5619
|
} catch (err) {
|
|
@@ -5620,6 +5640,7 @@ var PushClient = class {
|
|
|
5620
5640
|
this.available = true;
|
|
5621
5641
|
this.lastFailure = null;
|
|
5622
5642
|
this.suspendedUntil = 0;
|
|
5643
|
+
this.logSuspensionEnded("Home Assistant answered");
|
|
5623
5644
|
/**
|
|
5624
5645
|
* The link came back. Drop the dedup cache so the reconcile that
|
|
5625
5646
|
* follows re-pushes every value: HA may have restarted and lost its
|
|
@@ -5657,11 +5678,29 @@ var PushClient = class {
|
|
|
5657
5678
|
*/
|
|
5658
5679
|
onUnreachable(message, err) {
|
|
5659
5680
|
this.available = false;
|
|
5660
|
-
const
|
|
5681
|
+
const now = Date.now();
|
|
5682
|
+
const described = describePushFailure(err);
|
|
5683
|
+
let failure = described;
|
|
5684
|
+
if (described.kind === "component-missing") {
|
|
5685
|
+
if (this.first404At === 0) this.first404At = now;
|
|
5686
|
+
if (now - this.first404At < 6e4) failure = {
|
|
5687
|
+
kind: "other",
|
|
5688
|
+
permanent: false,
|
|
5689
|
+
text: "HTTP 404 — Home Assistant has not registered the CamStack endpoint (yet); retrying"
|
|
5690
|
+
};
|
|
5691
|
+
} else this.first404At = 0;
|
|
5661
5692
|
this.lastFailure = failure;
|
|
5662
|
-
if (failure.permanent) this.suspendedUntil = Date.now() + COMPONENT_RECHECK_MS;
|
|
5663
5693
|
this.dropped += message.type === "batch" ? message.items.length : 1;
|
|
5664
|
-
|
|
5694
|
+
if (failure.permanent) {
|
|
5695
|
+
this.suspendedUntil = now + COMPONENT_RECHECK_MS;
|
|
5696
|
+
this.suspensionLogged = true;
|
|
5697
|
+
this.logger.warn(`ha-export: push link SUSPENDED (${failure.text}); next probe at ${new Date(this.suspendedUntil).toISOString()}`, { meta: {
|
|
5698
|
+
brokerId: this.brokerId,
|
|
5699
|
+
failureKind: failure.kind,
|
|
5700
|
+
recheckAt: this.suspendedUntil,
|
|
5701
|
+
recheckInMs: COMPONENT_RECHECK_MS
|
|
5702
|
+
} });
|
|
5703
|
+
}
|
|
5665
5704
|
if (now - this.droppedLoggedAt < DROP_LOG_INTERVAL_MS) return;
|
|
5666
5705
|
this.droppedLoggedAt = now;
|
|
5667
5706
|
this.logger.warn(`ha-export: push to Home Assistant failed (${failure.text}), messages dropped`, { meta: {
|
|
@@ -5673,6 +5712,19 @@ var PushClient = class {
|
|
|
5673
5712
|
} });
|
|
5674
5713
|
}
|
|
5675
5714
|
};
|
|
5715
|
+
//#endregion
|
|
5716
|
+
//#region src/ha-export/link-gaps.ts
|
|
5717
|
+
/** A parked link is reported, never silently excluded. */
|
|
5718
|
+
function summariseLinkGaps(links) {
|
|
5719
|
+
return {
|
|
5720
|
+
failing: links.filter((l) => !l.healthy && !l.suspended).map((l) => l.brokerId),
|
|
5721
|
+
suspended: links.filter((l) => l.suspended).map((l) => ({
|
|
5722
|
+
brokerId: l.brokerId,
|
|
5723
|
+
recheckAt: l.recheckAt
|
|
5724
|
+
})),
|
|
5725
|
+
anyLive: links.some((l) => !l.suspended)
|
|
5726
|
+
};
|
|
5727
|
+
}
|
|
5676
5728
|
var ReachabilityReconciler = class {
|
|
5677
5729
|
options;
|
|
5678
5730
|
minIntervalMs;
|
|
@@ -8144,13 +8196,21 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
|
|
|
8144
8196
|
const delivered = await this.announce(exported, fullResync);
|
|
8145
8197
|
await this.pushFullState(exported, snapshots);
|
|
8146
8198
|
if (synthetic !== null) await this.pushSynthetic(projectSynthetic(synthetic, Date.now()), [...enabled]);
|
|
8147
|
-
const
|
|
8148
|
-
|
|
8149
|
-
|
|
8199
|
+
const gaps = summariseLinkGaps([...this.links.entries()].map(([brokerId, link]) => ({
|
|
8200
|
+
brokerId,
|
|
8201
|
+
healthy: link.client.healthy,
|
|
8202
|
+
suspended: link.client.suspended,
|
|
8203
|
+
recheckAt: link.client.recheckAt
|
|
8204
|
+
})));
|
|
8205
|
+
if (gaps.failing.length > 0 || !delivered && gaps.anyLive) {
|
|
8150
8206
|
this.resync.markGap("push-not-delivered");
|
|
8151
8207
|
this.lastStructureFingerprint = void 0;
|
|
8152
8208
|
this.ctx.logger.warn("ha-export: a push was not delivered, the next pass is a full resync");
|
|
8153
8209
|
}
|
|
8210
|
+
if (gaps.suspended.length > 0) this.ctx.logger.warn("ha-export: push links suspended, Home Assistant receives nothing", { meta: { suspended: gaps.suspended.map((s) => ({
|
|
8211
|
+
brokerId: s.brokerId,
|
|
8212
|
+
recheckAt: new Date(s.recheckAt).toISOString()
|
|
8213
|
+
})) } });
|
|
8154
8214
|
this.ctx.logger.info("ha-export: reconciled", { meta: {
|
|
8155
8215
|
reason,
|
|
8156
8216
|
fullResync,
|
|
@@ -8158,6 +8218,7 @@ var HaExportAddon = class extends require_ha_broker_source.BaseAddon {
|
|
|
8158
8218
|
devices: exported.size,
|
|
8159
8219
|
entities: entityCount,
|
|
8160
8220
|
unclassifiedCaps: this.unclassified.length,
|
|
8221
|
+
suspendedLinks: gaps.suspended.length,
|
|
8161
8222
|
durationMs: Date.now() - startedAt
|
|
8162
8223
|
} });
|
|
8163
8224
|
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 consumableKindOf, Bt as BaseAddon, Dt as sendToExportedTarget, Ht as baseUnitOf, Jt as isReadingQuantity, K as deviceExportCapability, Wt as defineAddonMigrations, Yt as nodePin, Zt as array, _ as PLUG_APPLIANCE_TYPES, _t as pruneDeletedEntities, c as COCO_TO_MACRO, d as ConsumableItemSchema, f as ConsumableStatusSchema, fn as EventCategory, g as MediaPlayerRepeatSchema, h as HvacModeSchema, i as AlarmArmModeSchema, in as string, j as buildAddonRouteProvider, l as CameraSwitchIdSchema, m as FanDirectionSchema, nn as object, on as unknown, pt as preconditionOf, r as listHomeAssistantBrokers, s as CAMERA_SWITCH_ORDER, u as ConsumableFormSchema, ut as oauthIntegrationCapability, x as addonRoutesCapability, xt as runExportedAction, z as consumableFormOf } from "../ha-broker-source-
|
|
1
|
+
import { B as consumableKindOf, Bt as BaseAddon, Dt as sendToExportedTarget, Ht as baseUnitOf, Jt as isReadingQuantity, K as deviceExportCapability, Wt as defineAddonMigrations, Yt as nodePin, Zt as array, _ as PLUG_APPLIANCE_TYPES, _t as pruneDeletedEntities, c as COCO_TO_MACRO, d as ConsumableItemSchema, f as ConsumableStatusSchema, fn as EventCategory, g as MediaPlayerRepeatSchema, h as HvacModeSchema, i as AlarmArmModeSchema, in as string, j as buildAddonRouteProvider, l as CameraSwitchIdSchema, m as FanDirectionSchema, nn as object, on as unknown, pt as preconditionOf, r as listHomeAssistantBrokers, s as CAMERA_SWITCH_ORDER, u as ConsumableFormSchema, ut as oauthIntegrationCapability, x as addonRoutesCapability, xt as runExportedAction, z as consumableFormOf } from "../ha-broker-source-CCmZ9I6P.mjs";
|
|
2
2
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
3
3
|
import "node:zlib";
|
|
4
4
|
import "node:fs";
|
|
@@ -5456,6 +5456,10 @@ var PushClient = class {
|
|
|
5456
5456
|
disposed = false;
|
|
5457
5457
|
lastFailure = null;
|
|
5458
5458
|
suspendedUntil = 0;
|
|
5459
|
+
/** When the current unbroken streak of 404s began; 0 when there is none. */
|
|
5460
|
+
first404At = 0;
|
|
5461
|
+
/** A suspension was entered and not yet logged as left. */
|
|
5462
|
+
suspensionLogged = false;
|
|
5459
5463
|
constructor(options) {
|
|
5460
5464
|
this.transport = options.transport;
|
|
5461
5465
|
this.logger = options.logger;
|
|
@@ -5474,9 +5478,20 @@ var PushClient = class {
|
|
|
5474
5478
|
get suspended() {
|
|
5475
5479
|
return this.suspendedUntil > Date.now();
|
|
5476
5480
|
}
|
|
5481
|
+
/** Epoch ms of the next probe while suspended; 0 otherwise. */
|
|
5482
|
+
get recheckAt() {
|
|
5483
|
+
return this.suspended ? this.suspendedUntil : 0;
|
|
5484
|
+
}
|
|
5477
5485
|
/** Probe again at once — the operator changed something. */
|
|
5478
5486
|
resetBackoff() {
|
|
5479
5487
|
this.suspendedUntil = 0;
|
|
5488
|
+
this.first404At = 0;
|
|
5489
|
+
this.logSuspensionEnded("reset by a settings change");
|
|
5490
|
+
}
|
|
5491
|
+
logSuspensionEnded(why) {
|
|
5492
|
+
if (!this.suspensionLogged) return;
|
|
5493
|
+
this.suspensionLogged = false;
|
|
5494
|
+
this.logger.info(`ha-export: push link no longer suspended (${why}), probing again`, { meta: { brokerId: this.brokerId } });
|
|
5480
5495
|
}
|
|
5481
5496
|
/** Messages discarded because HA did not answer. Never silent. */
|
|
5482
5497
|
get droppedCount() {
|
|
@@ -5557,8 +5572,13 @@ var PushClient = class {
|
|
|
5557
5572
|
this.dropped += message.type === "batch" ? message.items.length : 1;
|
|
5558
5573
|
return false;
|
|
5559
5574
|
}
|
|
5575
|
+
if (this.suspendedUntil !== 0 && !this.suspended) {
|
|
5576
|
+
this.suspendedUntil = 0;
|
|
5577
|
+
this.logSuspensionEnded("recheck time reached");
|
|
5578
|
+
}
|
|
5560
5579
|
try {
|
|
5561
5580
|
await this.transport(message);
|
|
5581
|
+
this.first404At = 0;
|
|
5562
5582
|
this.onReachable();
|
|
5563
5583
|
return true;
|
|
5564
5584
|
} catch (err) {
|
|
@@ -5585,6 +5605,7 @@ var PushClient = class {
|
|
|
5585
5605
|
this.available = true;
|
|
5586
5606
|
this.lastFailure = null;
|
|
5587
5607
|
this.suspendedUntil = 0;
|
|
5608
|
+
this.logSuspensionEnded("Home Assistant answered");
|
|
5588
5609
|
/**
|
|
5589
5610
|
* The link came back. Drop the dedup cache so the reconcile that
|
|
5590
5611
|
* follows re-pushes every value: HA may have restarted and lost its
|
|
@@ -5622,11 +5643,29 @@ var PushClient = class {
|
|
|
5622
5643
|
*/
|
|
5623
5644
|
onUnreachable(message, err) {
|
|
5624
5645
|
this.available = false;
|
|
5625
|
-
const
|
|
5646
|
+
const now = Date.now();
|
|
5647
|
+
const described = describePushFailure(err);
|
|
5648
|
+
let failure = described;
|
|
5649
|
+
if (described.kind === "component-missing") {
|
|
5650
|
+
if (this.first404At === 0) this.first404At = now;
|
|
5651
|
+
if (now - this.first404At < 6e4) failure = {
|
|
5652
|
+
kind: "other",
|
|
5653
|
+
permanent: false,
|
|
5654
|
+
text: "HTTP 404 — Home Assistant has not registered the CamStack endpoint (yet); retrying"
|
|
5655
|
+
};
|
|
5656
|
+
} else this.first404At = 0;
|
|
5626
5657
|
this.lastFailure = failure;
|
|
5627
|
-
if (failure.permanent) this.suspendedUntil = Date.now() + COMPONENT_RECHECK_MS;
|
|
5628
5658
|
this.dropped += message.type === "batch" ? message.items.length : 1;
|
|
5629
|
-
|
|
5659
|
+
if (failure.permanent) {
|
|
5660
|
+
this.suspendedUntil = now + COMPONENT_RECHECK_MS;
|
|
5661
|
+
this.suspensionLogged = true;
|
|
5662
|
+
this.logger.warn(`ha-export: push link SUSPENDED (${failure.text}); next probe at ${new Date(this.suspendedUntil).toISOString()}`, { meta: {
|
|
5663
|
+
brokerId: this.brokerId,
|
|
5664
|
+
failureKind: failure.kind,
|
|
5665
|
+
recheckAt: this.suspendedUntil,
|
|
5666
|
+
recheckInMs: COMPONENT_RECHECK_MS
|
|
5667
|
+
} });
|
|
5668
|
+
}
|
|
5630
5669
|
if (now - this.droppedLoggedAt < DROP_LOG_INTERVAL_MS) return;
|
|
5631
5670
|
this.droppedLoggedAt = now;
|
|
5632
5671
|
this.logger.warn(`ha-export: push to Home Assistant failed (${failure.text}), messages dropped`, { meta: {
|
|
@@ -5638,6 +5677,19 @@ var PushClient = class {
|
|
|
5638
5677
|
} });
|
|
5639
5678
|
}
|
|
5640
5679
|
};
|
|
5680
|
+
//#endregion
|
|
5681
|
+
//#region src/ha-export/link-gaps.ts
|
|
5682
|
+
/** A parked link is reported, never silently excluded. */
|
|
5683
|
+
function summariseLinkGaps(links) {
|
|
5684
|
+
return {
|
|
5685
|
+
failing: links.filter((l) => !l.healthy && !l.suspended).map((l) => l.brokerId),
|
|
5686
|
+
suspended: links.filter((l) => l.suspended).map((l) => ({
|
|
5687
|
+
brokerId: l.brokerId,
|
|
5688
|
+
recheckAt: l.recheckAt
|
|
5689
|
+
})),
|
|
5690
|
+
anyLive: links.some((l) => !l.suspended)
|
|
5691
|
+
};
|
|
5692
|
+
}
|
|
5641
5693
|
var ReachabilityReconciler = class {
|
|
5642
5694
|
options;
|
|
5643
5695
|
minIntervalMs;
|
|
@@ -8109,13 +8161,21 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
8109
8161
|
const delivered = await this.announce(exported, fullResync);
|
|
8110
8162
|
await this.pushFullState(exported, snapshots);
|
|
8111
8163
|
if (synthetic !== null) await this.pushSynthetic(projectSynthetic(synthetic, Date.now()), [...enabled]);
|
|
8112
|
-
const
|
|
8113
|
-
|
|
8114
|
-
|
|
8164
|
+
const gaps = summariseLinkGaps([...this.links.entries()].map(([brokerId, link]) => ({
|
|
8165
|
+
brokerId,
|
|
8166
|
+
healthy: link.client.healthy,
|
|
8167
|
+
suspended: link.client.suspended,
|
|
8168
|
+
recheckAt: link.client.recheckAt
|
|
8169
|
+
})));
|
|
8170
|
+
if (gaps.failing.length > 0 || !delivered && gaps.anyLive) {
|
|
8115
8171
|
this.resync.markGap("push-not-delivered");
|
|
8116
8172
|
this.lastStructureFingerprint = void 0;
|
|
8117
8173
|
this.ctx.logger.warn("ha-export: a push was not delivered, the next pass is a full resync");
|
|
8118
8174
|
}
|
|
8175
|
+
if (gaps.suspended.length > 0) this.ctx.logger.warn("ha-export: push links suspended, Home Assistant receives nothing", { meta: { suspended: gaps.suspended.map((s) => ({
|
|
8176
|
+
brokerId: s.brokerId,
|
|
8177
|
+
recheckAt: new Date(s.recheckAt).toISOString()
|
|
8178
|
+
})) } });
|
|
8119
8179
|
this.ctx.logger.info("ha-export: reconciled", { meta: {
|
|
8120
8180
|
reason,
|
|
8121
8181
|
fullResync,
|
|
@@ -8123,6 +8183,7 @@ var HaExportAddon = class extends BaseAddon {
|
|
|
8123
8183
|
devices: exported.size,
|
|
8124
8184
|
entities: entityCount,
|
|
8125
8185
|
unclassifiedCaps: this.unclassified.length,
|
|
8186
|
+
suspendedLinks: gaps.suspended.length,
|
|
8126
8187
|
durationMs: Date.now() - startedAt
|
|
8127
8188
|
} });
|
|
8128
8189
|
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.651",
|
|
4
4
|
"description": "Home Assistant device provider addon for CamStack",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
"description": "Device provider (import) + native entity export (push). Two addons, one bundle, separate processes."
|
|
128
128
|
},
|
|
129
129
|
"requires": {
|
|
130
|
-
"server": ">=1.2.
|
|
130
|
+
"server": ">=1.2.741"
|
|
131
131
|
}
|
|
132
132
|
},
|
|
133
133
|
"files": [
|