@camstack/addon-provider-dreame 0.2.162 → 0.2.163
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 +137 -1
- package/dist/addon.mjs +137 -1
- 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
|
+
## [0.2.163] - 2026-11-01
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- An integration deleted while its provider was down now has its orphan devices removed after two concordant reads of the integration list
|
|
15
|
+
|
|
10
16
|
## [0.2.162] - 2026-11-01
|
|
11
17
|
|
|
12
18
|
### Changed
|
package/dist/addon.js
CHANGED
|
@@ -147951,6 +147951,88 @@ function ingestReading(input) {
|
|
|
147951
147951
|
value: converted
|
|
147952
147952
|
};
|
|
147953
147953
|
}
|
|
147954
|
+
/**
|
|
147955
|
+
* Closes the gap D49 leaves: an integration deleted while its provider was down never emits an
|
|
147956
|
+
* event the provider sees. An integration id absent from an ANSWERED list is only a suspect; its
|
|
147957
|
+
* devices go (through the provider's own removeDevices) when a SECOND answered read, at least
|
|
147958
|
+
* `minIntervalMs` later, still lacks it. A read that threw changes nothing; reappearing clears.
|
|
147959
|
+
*/
|
|
147960
|
+
var OrphanIntegrationSweep = class {
|
|
147961
|
+
#logger;
|
|
147962
|
+
#minIntervalMs;
|
|
147963
|
+
#now;
|
|
147964
|
+
#suspects = /* @__PURE__ */ new Map();
|
|
147965
|
+
constructor(options) {
|
|
147966
|
+
this.#logger = options.logger;
|
|
147967
|
+
this.#minIntervalMs = options.minIntervalMs ?? 6e4;
|
|
147968
|
+
this.#now = options.now ?? Date.now;
|
|
147969
|
+
}
|
|
147970
|
+
get minIntervalMs() {
|
|
147971
|
+
return this.#minIntervalMs;
|
|
147972
|
+
}
|
|
147973
|
+
async sweep(input) {
|
|
147974
|
+
if (input.listedIds === null) return {
|
|
147975
|
+
newSuspects: 0,
|
|
147976
|
+
removed: []
|
|
147977
|
+
};
|
|
147978
|
+
const listed = new Set(input.listedIds);
|
|
147979
|
+
const owners = new Set(input.ownerIds);
|
|
147980
|
+
const now = this.#now();
|
|
147981
|
+
let newSuspects = 0;
|
|
147982
|
+
const removed = [];
|
|
147983
|
+
for (const id of [...this.#suspects.keys()]) if (listed.has(id) || !owners.has(id)) {
|
|
147984
|
+
this.#suspects.delete(id);
|
|
147985
|
+
this.#logger.info("Orphan integration suspect cleared", { meta: {
|
|
147986
|
+
integrationId: id,
|
|
147987
|
+
reason: listed.has(id) ? "listed again" : "owns no devices"
|
|
147988
|
+
} });
|
|
147989
|
+
}
|
|
147990
|
+
for (const id of owners) {
|
|
147991
|
+
if (listed.has(id)) continue;
|
|
147992
|
+
const since = this.#suspects.get(id);
|
|
147993
|
+
if (since === void 0) {
|
|
147994
|
+
this.#suspects.set(id, now);
|
|
147995
|
+
newSuspects += 1;
|
|
147996
|
+
this.#logger.info("Integration owns devices but is absent from the list — suspect", { meta: {
|
|
147997
|
+
integrationId: id,
|
|
147998
|
+
minIntervalMs: this.#minIntervalMs
|
|
147999
|
+
} });
|
|
148000
|
+
continue;
|
|
148001
|
+
}
|
|
148002
|
+
if (now - since < this.#minIntervalMs) continue;
|
|
148003
|
+
try {
|
|
148004
|
+
const count = await input.removeDevices(id);
|
|
148005
|
+
this.#suspects.delete(id);
|
|
148006
|
+
removed.push(id);
|
|
148007
|
+
this.#logger.warn("Orphan integration devices removed — absent from two answered lists", { meta: {
|
|
148008
|
+
integrationId: id,
|
|
148009
|
+
removed: count,
|
|
148010
|
+
firstMissingAt: since
|
|
148011
|
+
} });
|
|
148012
|
+
} catch (err) {
|
|
148013
|
+
this.#logger.warn("Orphan integration device removal failed — kept as suspect", { meta: {
|
|
148014
|
+
integrationId: id,
|
|
148015
|
+
error: errMsg(err)
|
|
148016
|
+
} });
|
|
148017
|
+
}
|
|
148018
|
+
}
|
|
148019
|
+
return {
|
|
148020
|
+
newSuspects,
|
|
148021
|
+
removed
|
|
148022
|
+
};
|
|
148023
|
+
}
|
|
148024
|
+
};
|
|
148025
|
+
/** The integration ids that own top-level devices of one provider (a config that cannot be read is skipped). */
|
|
148026
|
+
async function collectOwnerIntegrationIds(input) {
|
|
148027
|
+
const ids = /* @__PURE__ */ new Set();
|
|
148028
|
+
for (const d of input.reg.getAllForAddon(input.addonId)) {
|
|
148029
|
+
if (d.parentDeviceId !== null) continue;
|
|
148030
|
+
const cfg = await input.devices.loadConfig(d.id).catch(() => ({}));
|
|
148031
|
+
const owner = input.ownerOf(cfg);
|
|
148032
|
+
if (owner !== null) ids.add(owner);
|
|
148033
|
+
}
|
|
148034
|
+
return [...ids];
|
|
148035
|
+
}
|
|
147954
148036
|
String.raw`(?<=^|["'(\s>,\[{])`;
|
|
147955
148037
|
var MB = 1024 * 1024;
|
|
147956
148038
|
1024 * MB, 3072 * MB;
|
|
@@ -147971,8 +148053,14 @@ object({
|
|
|
147971
148053
|
*/
|
|
147972
148054
|
var DreameReconciler = class {
|
|
147973
148055
|
#deps;
|
|
148056
|
+
#sweep;
|
|
148057
|
+
#rerun = null;
|
|
147974
148058
|
constructor(deps) {
|
|
147975
148059
|
this.#deps = deps;
|
|
148060
|
+
this.#sweep = new OrphanIntegrationSweep({
|
|
148061
|
+
logger: deps.logger,
|
|
148062
|
+
...deps.sweepOptions
|
|
148063
|
+
});
|
|
147976
148064
|
}
|
|
147977
148065
|
/** Upsert a client per usable enabled integration; drop the CLIENT (never devices) of one the list answered is gone. */
|
|
147978
148066
|
async reconcile() {
|
|
@@ -147995,6 +148083,37 @@ var DreameReconciler = class {
|
|
|
147995
148083
|
}
|
|
147996
148084
|
const enabledIds = new Set(enabled.map((i) => i.id));
|
|
147997
148085
|
for (const id of clients.list()) if (!enabledIds.has(id)) await clients.remove(id);
|
|
148086
|
+
await this.#sweepOrphans(all.map((i) => i.id));
|
|
148087
|
+
}
|
|
148088
|
+
/** Stop the scheduled second read (addon stop). */
|
|
148089
|
+
dispose() {
|
|
148090
|
+
if (this.#rerun !== null) clearTimeout(this.#rerun);
|
|
148091
|
+
this.#rerun = null;
|
|
148092
|
+
}
|
|
148093
|
+
/**
|
|
148094
|
+
* An integration deleted while this addon was down emits nothing it sees: its id owns devices
|
|
148095
|
+
* yet is absent from the ANSWERED list. Removal needs a second answered read later, so one
|
|
148096
|
+
* re-run is scheduled when a suspect appears (bounded: one timer, no retry loop).
|
|
148097
|
+
*/
|
|
148098
|
+
async #sweepOrphans(listedIds) {
|
|
148099
|
+
const { ownerIntegrationIds, removeDevices, logger } = this.#deps;
|
|
148100
|
+
if (ownerIntegrationIds === void 0) return;
|
|
148101
|
+
try {
|
|
148102
|
+
const ownerIds = await ownerIntegrationIds();
|
|
148103
|
+
if ((await this.#sweep.sweep({
|
|
148104
|
+
ownerIds,
|
|
148105
|
+
listedIds,
|
|
148106
|
+
removeDevices
|
|
148107
|
+
})).newSuspects > 0 && this.#rerun === null) {
|
|
148108
|
+
this.#rerun = setTimeout(() => {
|
|
148109
|
+
this.#rerun = null;
|
|
148110
|
+
this.reconcile();
|
|
148111
|
+
}, this.#sweep.minIntervalMs + 1e3);
|
|
148112
|
+
this.#rerun.unref();
|
|
148113
|
+
}
|
|
148114
|
+
} catch (err) {
|
|
148115
|
+
logger.warn("Orphan integration sweep failed — devices left as they are", { meta: { error: errMsg(err) } });
|
|
148116
|
+
}
|
|
147998
148117
|
}
|
|
147999
148118
|
/** An explicit lifecycle event: only `deleted` / `disabled` of a named integration remove devices. */
|
|
148000
148119
|
async onLifecycle(kind, integrationId) {
|
|
@@ -201120,6 +201239,7 @@ var DreameProviderAddon = class extends BaseDeviceProvider {
|
|
|
201120
201239
|
this.ctx.logger.warn("Dreame: provider shutdown error", { meta: { error: errMsg(err) } });
|
|
201121
201240
|
}
|
|
201122
201241
|
this.clients = null;
|
|
201242
|
+
this.integrationReconciler?.dispose();
|
|
201123
201243
|
this.integrationReconciler = null;
|
|
201124
201244
|
dreameSession.unbind();
|
|
201125
201245
|
await super.onShutdown();
|
|
@@ -201192,7 +201312,8 @@ var DreameProviderAddon = class extends BaseDeviceProvider {
|
|
|
201192
201312
|
await this.requireClients().upsert(integration.id, integration.name, connection);
|
|
201193
201313
|
return true;
|
|
201194
201314
|
},
|
|
201195
|
-
removeDevices: (integrationId) => this.cascadeRemoveDevicesForIntegration(integrationId)
|
|
201315
|
+
removeDevices: (integrationId) => this.cascadeRemoveDevicesForIntegration(integrationId),
|
|
201316
|
+
ownerIntegrationIds: () => this.ownerIntegrationIds()
|
|
201196
201317
|
});
|
|
201197
201318
|
return this.integrationReconciler;
|
|
201198
201319
|
}
|
|
@@ -201212,6 +201333,21 @@ var DreameProviderAddon = class extends BaseDeviceProvider {
|
|
|
201212
201333
|
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, on("disabled"));
|
|
201213
201334
|
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, on("deleted"));
|
|
201214
201335
|
}
|
|
201336
|
+
/** The integrations that own devices here, for the orphan sweep. */
|
|
201337
|
+
async ownerIntegrationIds() {
|
|
201338
|
+
const reg = this.ctx.kernel.deviceRegistry;
|
|
201339
|
+
const devices = this.ctx.kernel.devices;
|
|
201340
|
+
if (!reg || !devices) return [];
|
|
201341
|
+
return await collectOwnerIntegrationIds({
|
|
201342
|
+
reg,
|
|
201343
|
+
devices,
|
|
201344
|
+
addonId: this.addonId,
|
|
201345
|
+
ownerOf: (cfg) => {
|
|
201346
|
+
const owner = cfg["brokerId"];
|
|
201347
|
+
return cfg["system"] === "dreame" && typeof owner === "string" ? owner : null;
|
|
201348
|
+
}
|
|
201349
|
+
});
|
|
201350
|
+
}
|
|
201215
201351
|
async cascadeRemoveDevicesForIntegration(integrationId) {
|
|
201216
201352
|
const reg = this.ctx.kernel.deviceRegistry;
|
|
201217
201353
|
const devices = this.ctx.kernel.devices;
|
package/dist/addon.mjs
CHANGED
|
@@ -147948,6 +147948,88 @@ function ingestReading(input) {
|
|
|
147948
147948
|
value: converted
|
|
147949
147949
|
};
|
|
147950
147950
|
}
|
|
147951
|
+
/**
|
|
147952
|
+
* Closes the gap D49 leaves: an integration deleted while its provider was down never emits an
|
|
147953
|
+
* event the provider sees. An integration id absent from an ANSWERED list is only a suspect; its
|
|
147954
|
+
* devices go (through the provider's own removeDevices) when a SECOND answered read, at least
|
|
147955
|
+
* `minIntervalMs` later, still lacks it. A read that threw changes nothing; reappearing clears.
|
|
147956
|
+
*/
|
|
147957
|
+
var OrphanIntegrationSweep = class {
|
|
147958
|
+
#logger;
|
|
147959
|
+
#minIntervalMs;
|
|
147960
|
+
#now;
|
|
147961
|
+
#suspects = /* @__PURE__ */ new Map();
|
|
147962
|
+
constructor(options) {
|
|
147963
|
+
this.#logger = options.logger;
|
|
147964
|
+
this.#minIntervalMs = options.minIntervalMs ?? 6e4;
|
|
147965
|
+
this.#now = options.now ?? Date.now;
|
|
147966
|
+
}
|
|
147967
|
+
get minIntervalMs() {
|
|
147968
|
+
return this.#minIntervalMs;
|
|
147969
|
+
}
|
|
147970
|
+
async sweep(input) {
|
|
147971
|
+
if (input.listedIds === null) return {
|
|
147972
|
+
newSuspects: 0,
|
|
147973
|
+
removed: []
|
|
147974
|
+
};
|
|
147975
|
+
const listed = new Set(input.listedIds);
|
|
147976
|
+
const owners = new Set(input.ownerIds);
|
|
147977
|
+
const now = this.#now();
|
|
147978
|
+
let newSuspects = 0;
|
|
147979
|
+
const removed = [];
|
|
147980
|
+
for (const id of [...this.#suspects.keys()]) if (listed.has(id) || !owners.has(id)) {
|
|
147981
|
+
this.#suspects.delete(id);
|
|
147982
|
+
this.#logger.info("Orphan integration suspect cleared", { meta: {
|
|
147983
|
+
integrationId: id,
|
|
147984
|
+
reason: listed.has(id) ? "listed again" : "owns no devices"
|
|
147985
|
+
} });
|
|
147986
|
+
}
|
|
147987
|
+
for (const id of owners) {
|
|
147988
|
+
if (listed.has(id)) continue;
|
|
147989
|
+
const since = this.#suspects.get(id);
|
|
147990
|
+
if (since === void 0) {
|
|
147991
|
+
this.#suspects.set(id, now);
|
|
147992
|
+
newSuspects += 1;
|
|
147993
|
+
this.#logger.info("Integration owns devices but is absent from the list — suspect", { meta: {
|
|
147994
|
+
integrationId: id,
|
|
147995
|
+
minIntervalMs: this.#minIntervalMs
|
|
147996
|
+
} });
|
|
147997
|
+
continue;
|
|
147998
|
+
}
|
|
147999
|
+
if (now - since < this.#minIntervalMs) continue;
|
|
148000
|
+
try {
|
|
148001
|
+
const count = await input.removeDevices(id);
|
|
148002
|
+
this.#suspects.delete(id);
|
|
148003
|
+
removed.push(id);
|
|
148004
|
+
this.#logger.warn("Orphan integration devices removed — absent from two answered lists", { meta: {
|
|
148005
|
+
integrationId: id,
|
|
148006
|
+
removed: count,
|
|
148007
|
+
firstMissingAt: since
|
|
148008
|
+
} });
|
|
148009
|
+
} catch (err) {
|
|
148010
|
+
this.#logger.warn("Orphan integration device removal failed — kept as suspect", { meta: {
|
|
148011
|
+
integrationId: id,
|
|
148012
|
+
error: errMsg(err)
|
|
148013
|
+
} });
|
|
148014
|
+
}
|
|
148015
|
+
}
|
|
148016
|
+
return {
|
|
148017
|
+
newSuspects,
|
|
148018
|
+
removed
|
|
148019
|
+
};
|
|
148020
|
+
}
|
|
148021
|
+
};
|
|
148022
|
+
/** The integration ids that own top-level devices of one provider (a config that cannot be read is skipped). */
|
|
148023
|
+
async function collectOwnerIntegrationIds(input) {
|
|
148024
|
+
const ids = /* @__PURE__ */ new Set();
|
|
148025
|
+
for (const d of input.reg.getAllForAddon(input.addonId)) {
|
|
148026
|
+
if (d.parentDeviceId !== null) continue;
|
|
148027
|
+
const cfg = await input.devices.loadConfig(d.id).catch(() => ({}));
|
|
148028
|
+
const owner = input.ownerOf(cfg);
|
|
148029
|
+
if (owner !== null) ids.add(owner);
|
|
148030
|
+
}
|
|
148031
|
+
return [...ids];
|
|
148032
|
+
}
|
|
147951
148033
|
String.raw`(?<=^|["'(\s>,\[{])`;
|
|
147952
148034
|
var MB = 1024 * 1024;
|
|
147953
148035
|
1024 * MB, 3072 * MB;
|
|
@@ -147968,8 +148050,14 @@ object({
|
|
|
147968
148050
|
*/
|
|
147969
148051
|
var DreameReconciler = class {
|
|
147970
148052
|
#deps;
|
|
148053
|
+
#sweep;
|
|
148054
|
+
#rerun = null;
|
|
147971
148055
|
constructor(deps) {
|
|
147972
148056
|
this.#deps = deps;
|
|
148057
|
+
this.#sweep = new OrphanIntegrationSweep({
|
|
148058
|
+
logger: deps.logger,
|
|
148059
|
+
...deps.sweepOptions
|
|
148060
|
+
});
|
|
147973
148061
|
}
|
|
147974
148062
|
/** Upsert a client per usable enabled integration; drop the CLIENT (never devices) of one the list answered is gone. */
|
|
147975
148063
|
async reconcile() {
|
|
@@ -147992,6 +148080,37 @@ var DreameReconciler = class {
|
|
|
147992
148080
|
}
|
|
147993
148081
|
const enabledIds = new Set(enabled.map((i) => i.id));
|
|
147994
148082
|
for (const id of clients.list()) if (!enabledIds.has(id)) await clients.remove(id);
|
|
148083
|
+
await this.#sweepOrphans(all.map((i) => i.id));
|
|
148084
|
+
}
|
|
148085
|
+
/** Stop the scheduled second read (addon stop). */
|
|
148086
|
+
dispose() {
|
|
148087
|
+
if (this.#rerun !== null) clearTimeout(this.#rerun);
|
|
148088
|
+
this.#rerun = null;
|
|
148089
|
+
}
|
|
148090
|
+
/**
|
|
148091
|
+
* An integration deleted while this addon was down emits nothing it sees: its id owns devices
|
|
148092
|
+
* yet is absent from the ANSWERED list. Removal needs a second answered read later, so one
|
|
148093
|
+
* re-run is scheduled when a suspect appears (bounded: one timer, no retry loop).
|
|
148094
|
+
*/
|
|
148095
|
+
async #sweepOrphans(listedIds) {
|
|
148096
|
+
const { ownerIntegrationIds, removeDevices, logger } = this.#deps;
|
|
148097
|
+
if (ownerIntegrationIds === void 0) return;
|
|
148098
|
+
try {
|
|
148099
|
+
const ownerIds = await ownerIntegrationIds();
|
|
148100
|
+
if ((await this.#sweep.sweep({
|
|
148101
|
+
ownerIds,
|
|
148102
|
+
listedIds,
|
|
148103
|
+
removeDevices
|
|
148104
|
+
})).newSuspects > 0 && this.#rerun === null) {
|
|
148105
|
+
this.#rerun = setTimeout(() => {
|
|
148106
|
+
this.#rerun = null;
|
|
148107
|
+
this.reconcile();
|
|
148108
|
+
}, this.#sweep.minIntervalMs + 1e3);
|
|
148109
|
+
this.#rerun.unref();
|
|
148110
|
+
}
|
|
148111
|
+
} catch (err) {
|
|
148112
|
+
logger.warn("Orphan integration sweep failed — devices left as they are", { meta: { error: errMsg(err) } });
|
|
148113
|
+
}
|
|
147995
148114
|
}
|
|
147996
148115
|
/** An explicit lifecycle event: only `deleted` / `disabled` of a named integration remove devices. */
|
|
147997
148116
|
async onLifecycle(kind, integrationId) {
|
|
@@ -201117,6 +201236,7 @@ var DreameProviderAddon = class extends BaseDeviceProvider {
|
|
|
201117
201236
|
this.ctx.logger.warn("Dreame: provider shutdown error", { meta: { error: errMsg(err) } });
|
|
201118
201237
|
}
|
|
201119
201238
|
this.clients = null;
|
|
201239
|
+
this.integrationReconciler?.dispose();
|
|
201120
201240
|
this.integrationReconciler = null;
|
|
201121
201241
|
dreameSession.unbind();
|
|
201122
201242
|
await super.onShutdown();
|
|
@@ -201189,7 +201309,8 @@ var DreameProviderAddon = class extends BaseDeviceProvider {
|
|
|
201189
201309
|
await this.requireClients().upsert(integration.id, integration.name, connection);
|
|
201190
201310
|
return true;
|
|
201191
201311
|
},
|
|
201192
|
-
removeDevices: (integrationId) => this.cascadeRemoveDevicesForIntegration(integrationId)
|
|
201312
|
+
removeDevices: (integrationId) => this.cascadeRemoveDevicesForIntegration(integrationId),
|
|
201313
|
+
ownerIntegrationIds: () => this.ownerIntegrationIds()
|
|
201193
201314
|
});
|
|
201194
201315
|
return this.integrationReconciler;
|
|
201195
201316
|
}
|
|
@@ -201209,6 +201330,21 @@ var DreameProviderAddon = class extends BaseDeviceProvider {
|
|
|
201209
201330
|
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, on("disabled"));
|
|
201210
201331
|
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, on("deleted"));
|
|
201211
201332
|
}
|
|
201333
|
+
/** The integrations that own devices here, for the orphan sweep. */
|
|
201334
|
+
async ownerIntegrationIds() {
|
|
201335
|
+
const reg = this.ctx.kernel.deviceRegistry;
|
|
201336
|
+
const devices = this.ctx.kernel.devices;
|
|
201337
|
+
if (!reg || !devices) return [];
|
|
201338
|
+
return await collectOwnerIntegrationIds({
|
|
201339
|
+
reg,
|
|
201340
|
+
devices,
|
|
201341
|
+
addonId: this.addonId,
|
|
201342
|
+
ownerOf: (cfg) => {
|
|
201343
|
+
const owner = cfg["brokerId"];
|
|
201344
|
+
return cfg["system"] === "dreame" && typeof owner === "string" ? owner : null;
|
|
201345
|
+
}
|
|
201346
|
+
});
|
|
201347
|
+
}
|
|
201212
201348
|
async cascadeRemoveDevicesForIntegration(integrationId) {
|
|
201213
201349
|
const reg = this.ctx.kernel.deviceRegistry;
|
|
201214
201350
|
const devices = this.ctx.kernel.devices;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-provider-dreame",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.163",
|
|
4
4
|
"description": "Dreame robot-vacuum / lawn-mower device-provider addon for CamStack — wraps the @apocaliss92/nodedreame Dreamehome cloud client",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
}
|
|
75
75
|
],
|
|
76
76
|
"requires": {
|
|
77
|
-
"server": ">=1.2.
|
|
77
|
+
"server": ">=1.2.722"
|
|
78
78
|
}
|
|
79
79
|
},
|
|
80
80
|
"files": [
|