@camstack/addon-provider-dreame 0.1.14 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/addon.js +407 -660
- package/dist/addon.mjs +407 -660
- package/dist/index.js +1 -1
- package/dist/index.mjs +2 -2
- package/package.json +2 -5
package/dist/addon.mjs
CHANGED
|
@@ -14818,83 +14818,34 @@ var GetStateInputSchema = object({
|
|
|
14818
14818
|
* HA: entity_id (returns the cached entity state). */
|
|
14819
14819
|
key: string()
|
|
14820
14820
|
});
|
|
14821
|
-
|
|
14822
|
-
|
|
14823
|
-
|
|
14824
|
-
|
|
14825
|
-
|
|
14826
|
-
|
|
14827
|
-
|
|
14828
|
-
|
|
14829
|
-
|
|
14830
|
-
|
|
14831
|
-
|
|
14832
|
-
|
|
14833
|
-
|
|
14834
|
-
|
|
14835
|
-
|
|
14836
|
-
|
|
14837
|
-
|
|
14838
|
-
|
|
14839
|
-
|
|
14840
|
-
|
|
14841
|
-
|
|
14842
|
-
|
|
14843
|
-
|
|
14844
|
-
|
|
14845
|
-
|
|
14846
|
-
|
|
14847
|
-
|
|
14848
|
-
|
|
14849
|
-
}),
|
|
14850
|
-
/** Read the persisted settings record for a broker (kind-specific
|
|
14851
|
-
* shape). Admin-only — settings may contain secrets. Returns `null`
|
|
14852
|
-
* when the broker id is unknown to the provider (the collection
|
|
14853
|
-
* fallback may route a foreign id to the first provider). */
|
|
14854
|
-
getSettings: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
|
|
14855
|
-
/** Overwrite the persisted settings record. The kind-specific
|
|
14856
|
-
* provider validates the shape and applies the change (reconnects
|
|
14857
|
-
* if credentials changed). */
|
|
14858
|
-
setSettings: method(object({
|
|
14859
|
-
id: string(),
|
|
14860
|
-
settings: SettingsRecordSchema$1
|
|
14861
|
-
}), _void(), {
|
|
14862
|
-
kind: "mutation",
|
|
14863
|
-
auth: "admin"
|
|
14864
|
-
}),
|
|
14865
|
-
/** Returns the kind-specific connection config the consumer needs
|
|
14866
|
-
* to open its own client (MQTT pattern: `{url, username, password,
|
|
14867
|
-
* clientIdPrefix}`). HA providers MAY return the auth envelope
|
|
14868
|
-
* but typical HA consumers use `publish` / `subscribe` instead.
|
|
14869
|
-
* Returns `null` when the broker id is unknown to the provider. */
|
|
14870
|
-
getBrokerConfig: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
|
|
14871
|
-
getSettingsSchema: method(SettingsSchemaInputSchema, SettingsSchemaResultSchema, { auth: "admin" }),
|
|
14872
|
-
testSettings: method(TestSettingsInputSchema, TestSettingsResultSchema, {
|
|
14873
|
-
kind: "mutation",
|
|
14874
|
-
auth: "admin"
|
|
14875
|
-
}),
|
|
14876
|
-
publish: method(PublishInputSchema, unknown(), {
|
|
14877
|
-
kind: "mutation",
|
|
14878
|
-
auth: "admin"
|
|
14879
|
-
}),
|
|
14880
|
-
subscribe: method(SubscribeInputSchema, SubscribeResultSchema, {
|
|
14881
|
-
kind: "mutation",
|
|
14882
|
-
auth: "admin"
|
|
14883
|
-
}),
|
|
14884
|
-
unsubscribe: method(UnsubscribeInputSchema, _void(), {
|
|
14885
|
-
kind: "mutation",
|
|
14886
|
-
auth: "admin"
|
|
14887
|
-
}),
|
|
14888
|
-
/** Read the broker's cached state for a key. Returns `null` when
|
|
14889
|
-
* unknown to the broker (never published / unknown entity). */
|
|
14890
|
-
getState: method(GetStateInputSchema, unknown().nullable()),
|
|
14891
|
-
/** Status method — explicit registration with a `z.void()` input so
|
|
14892
|
-
* the codegen-generated tRPC router types its input as
|
|
14893
|
-
* `{addonId?: string, nodeId?: string}` (system-scoped collection
|
|
14894
|
-
* shape) instead of the device-scoped `{deviceId}` fallback. */
|
|
14895
|
-
getStatus: method(_void(), RegistryStatusSchema)
|
|
14896
|
-
}
|
|
14897
|
-
};
|
|
14821
|
+
method(ListInputSchema, array(BrokerInfoSchema$1)), method(GetInputSchema, BrokerInfoSchema$1.nullable()), method(_void(), array(BrokerProviderInfoSchema), { auth: "admin" }), method(AddInputSchema, AddResultSchema, {
|
|
14822
|
+
kind: "mutation",
|
|
14823
|
+
auth: "admin"
|
|
14824
|
+
}), method(RemoveInputSchema, _void(), {
|
|
14825
|
+
kind: "mutation",
|
|
14826
|
+
auth: "admin"
|
|
14827
|
+
}), method(GetInputSchema, TestConnectionResultSchema, {
|
|
14828
|
+
kind: "mutation",
|
|
14829
|
+
auth: "admin"
|
|
14830
|
+
}), method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }), method(object({
|
|
14831
|
+
id: string(),
|
|
14832
|
+
settings: SettingsRecordSchema$1
|
|
14833
|
+
}), _void(), {
|
|
14834
|
+
kind: "mutation",
|
|
14835
|
+
auth: "admin"
|
|
14836
|
+
}), method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }), method(SettingsSchemaInputSchema, SettingsSchemaResultSchema, { auth: "admin" }), method(TestSettingsInputSchema, TestSettingsResultSchema, {
|
|
14837
|
+
kind: "mutation",
|
|
14838
|
+
auth: "admin"
|
|
14839
|
+
}), method(PublishInputSchema, unknown(), {
|
|
14840
|
+
kind: "mutation",
|
|
14841
|
+
auth: "admin"
|
|
14842
|
+
}), method(SubscribeInputSchema, SubscribeResultSchema, {
|
|
14843
|
+
kind: "mutation",
|
|
14844
|
+
auth: "admin"
|
|
14845
|
+
}), method(UnsubscribeInputSchema, _void(), {
|
|
14846
|
+
kind: "mutation",
|
|
14847
|
+
auth: "admin"
|
|
14848
|
+
}), method(GetStateInputSchema, unknown().nullable()), method(_void(), RegistryStatusSchema);
|
|
14898
14849
|
DeviceType.Camera;
|
|
14899
14850
|
/**
|
|
14900
14851
|
* `custom-model-registry` — collection cap exposing operator-registered
|
|
@@ -18366,6 +18317,17 @@ var AvailableIntegrationTypeSchema = object({
|
|
|
18366
18317
|
iconUrl: string().nullable(),
|
|
18367
18318
|
color: string(),
|
|
18368
18319
|
instanceMode: string(),
|
|
18320
|
+
/**
|
|
18321
|
+
* Integration wizard `mode` (LOCKED MODEL): `standalone` (create
|
|
18322
|
+
* immediately then add devices, no config step/button), `account` (config
|
|
18323
|
+
* step), or `broker` (broker step). Derived server-side by
|
|
18324
|
+
* `getAvailableTypes` when the addon manifest omits an explicit `mode`.
|
|
18325
|
+
*/
|
|
18326
|
+
mode: _enum([
|
|
18327
|
+
"standalone",
|
|
18328
|
+
"account",
|
|
18329
|
+
"broker"
|
|
18330
|
+
]),
|
|
18369
18331
|
discoveryMode: string(),
|
|
18370
18332
|
/**
|
|
18371
18333
|
* Which integration-marker cap the addon declared, so the wizard can
|
|
@@ -24084,6 +24046,124 @@ object({
|
|
|
24084
24046
|
schemaVersion: literal(1)
|
|
24085
24047
|
});
|
|
24086
24048
|
//#endregion
|
|
24049
|
+
//#region src/config.ts
|
|
24050
|
+
/**
|
|
24051
|
+
* The Dreame cloud regions the wrapped `@apocaliss92/nodedreame` client
|
|
24052
|
+
* supports. Mirrors the library's `DreameRegion` union — kept local so the
|
|
24053
|
+
* addon validates the operator-supplied value at the system boundary without
|
|
24054
|
+
* importing a runtime value the library does not export.
|
|
24055
|
+
*/
|
|
24056
|
+
var DREAME_REGIONS = [
|
|
24057
|
+
"eu",
|
|
24058
|
+
"us",
|
|
24059
|
+
"cn",
|
|
24060
|
+
"ru",
|
|
24061
|
+
"sg",
|
|
24062
|
+
"in",
|
|
24063
|
+
"de",
|
|
24064
|
+
"tw"
|
|
24065
|
+
];
|
|
24066
|
+
var DreameRegionSchema = _enum(DREAME_REGIONS);
|
|
24067
|
+
/**
|
|
24068
|
+
* Operator-supplied Dreamehome account settings for ONE broker (= one cloud
|
|
24069
|
+
* account). The Dreame integration is cloud-only (outbound HTTPS + per-device
|
|
24070
|
+
* MQTT push), so the connection is just account credentials plus a region.
|
|
24071
|
+
*
|
|
24072
|
+
* `pollIntervalMs` is the fallback poll cadence the library uses while a
|
|
24073
|
+
* device's MQTT push is down; `0` disables poll fallback.
|
|
24074
|
+
*
|
|
24075
|
+
* The diagnostic catalog (enum/numeric sensors, switches, selects) is seeded
|
|
24076
|
+
* from the cloud SHADOW once at broker start and then tracked by the library's
|
|
24077
|
+
* MQTT push. Live `get_properties` is NOT polled: a docked/asleep robot times it
|
|
24078
|
+
* out (Home Assistant shows the same diagnostics as `unavailable` then), and
|
|
24079
|
+
* hammering it only adds cloud load — so we rely on the shadow seed + push.
|
|
24080
|
+
*/
|
|
24081
|
+
var dreameConfigSchema = object({
|
|
24082
|
+
username: string().min(1).describe("Dreamehome account email / username"),
|
|
24083
|
+
password: string().min(1).describe("Dreamehome account password"),
|
|
24084
|
+
region: DreameRegionSchema.default("eu").describe("Dreame cloud region"),
|
|
24085
|
+
pollIntervalMs: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(0).max(36e5).default(3e4)).describe("Poll fallback interval (ms) while MQTT push is down — 0 disables")
|
|
24086
|
+
});
|
|
24087
|
+
/**
|
|
24088
|
+
* Build the `NodreameOptions` the wrapped client constructor expects from the
|
|
24089
|
+
* validated addon config. Pure: same config in → same options out.
|
|
24090
|
+
*/
|
|
24091
|
+
function toNodreameOptions(config) {
|
|
24092
|
+
return {
|
|
24093
|
+
username: config.username,
|
|
24094
|
+
password: config.password,
|
|
24095
|
+
region: config.region,
|
|
24096
|
+
fetchInitialValues: true,
|
|
24097
|
+
pollIntervalMs: config.pollIntervalMs
|
|
24098
|
+
};
|
|
24099
|
+
}
|
|
24100
|
+
/**
|
|
24101
|
+
* Parse an integration's settings into a connection, or null when the mandatory
|
|
24102
|
+
* credentials (username + password) are missing. Used by the boot/lifecycle
|
|
24103
|
+
* reconcile to skip an integration whose account form was not completed.
|
|
24104
|
+
*/
|
|
24105
|
+
function connectionFromSettings(settings) {
|
|
24106
|
+
const parsed = dreameConfigSchema.safeParse(settings);
|
|
24107
|
+
return parsed.success ? parsed.data : null;
|
|
24108
|
+
}
|
|
24109
|
+
/**
|
|
24110
|
+
* Hand-written connection form for the account/integration creation UI — the
|
|
24111
|
+
* wizard's `account` config step renders this via `getGlobalSettings`. A flat
|
|
24112
|
+
* set of sections the admin UI renders into the "Add Dreame account" modal.
|
|
24113
|
+
*/
|
|
24114
|
+
function buildConnectionFormSchema() {
|
|
24115
|
+
return { sections: [
|
|
24116
|
+
{
|
|
24117
|
+
id: "credentials",
|
|
24118
|
+
title: "Dreamehome account",
|
|
24119
|
+
description: "Sign in with your Dreamehome (Dreame app) account credentials.",
|
|
24120
|
+
columns: 1,
|
|
24121
|
+
fields: [{
|
|
24122
|
+
type: "text",
|
|
24123
|
+
key: "username",
|
|
24124
|
+
label: "Email / username",
|
|
24125
|
+
required: true,
|
|
24126
|
+
placeholder: "you@example.com"
|
|
24127
|
+
}, {
|
|
24128
|
+
type: "password",
|
|
24129
|
+
key: "password",
|
|
24130
|
+
label: "Password",
|
|
24131
|
+
required: true,
|
|
24132
|
+
showToggle: true
|
|
24133
|
+
}]
|
|
24134
|
+
},
|
|
24135
|
+
{
|
|
24136
|
+
id: "region",
|
|
24137
|
+
title: "Region",
|
|
24138
|
+
description: "Select the Dreame cloud region your account is registered in.",
|
|
24139
|
+
columns: 1,
|
|
24140
|
+
fields: [{
|
|
24141
|
+
type: "select",
|
|
24142
|
+
key: "region",
|
|
24143
|
+
label: "Region",
|
|
24144
|
+
default: "eu",
|
|
24145
|
+
options: DREAME_REGIONS.map((r) => ({
|
|
24146
|
+
value: r,
|
|
24147
|
+
label: r.toUpperCase()
|
|
24148
|
+
}))
|
|
24149
|
+
}]
|
|
24150
|
+
},
|
|
24151
|
+
{
|
|
24152
|
+
id: "advanced",
|
|
24153
|
+
title: "Advanced",
|
|
24154
|
+
columns: 1,
|
|
24155
|
+
fields: [{
|
|
24156
|
+
type: "number",
|
|
24157
|
+
key: "pollIntervalMs",
|
|
24158
|
+
label: "Poll fallback interval (ms, 0 = disabled)",
|
|
24159
|
+
min: 0,
|
|
24160
|
+
max: 36e5,
|
|
24161
|
+
default: 3e4
|
|
24162
|
+
}]
|
|
24163
|
+
}
|
|
24164
|
+
] };
|
|
24165
|
+
}
|
|
24166
|
+
//#endregion
|
|
24087
24167
|
//#region node_modules/undici/lib/core/symbols.js
|
|
24088
24168
|
var require_symbols$4 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
24089
24169
|
module.exports = {
|
|
@@ -74386,134 +74466,6 @@ objectType({
|
|
|
74386
74466
|
})
|
|
74387
74467
|
});
|
|
74388
74468
|
//#endregion
|
|
74389
|
-
//#region src/config.ts
|
|
74390
|
-
/**
|
|
74391
|
-
* The Dreame cloud regions the wrapped `@apocaliss92/nodedreame` client
|
|
74392
|
-
* supports. Mirrors the library's `DreameRegion` union — kept local so the
|
|
74393
|
-
* addon validates the operator-supplied value at the system boundary without
|
|
74394
|
-
* importing a runtime value the library does not export.
|
|
74395
|
-
*/
|
|
74396
|
-
var DREAME_REGIONS = [
|
|
74397
|
-
"eu",
|
|
74398
|
-
"us",
|
|
74399
|
-
"cn",
|
|
74400
|
-
"ru",
|
|
74401
|
-
"sg",
|
|
74402
|
-
"in",
|
|
74403
|
-
"de",
|
|
74404
|
-
"tw"
|
|
74405
|
-
];
|
|
74406
|
-
var DreameRegionSchema = _enum(DREAME_REGIONS);
|
|
74407
|
-
/**
|
|
74408
|
-
* Operator-supplied Dreamehome account settings for ONE broker (= one cloud
|
|
74409
|
-
* account). The Dreame integration is cloud-only (outbound HTTPS + per-device
|
|
74410
|
-
* MQTT push), so the connection is just account credentials plus a region.
|
|
74411
|
-
*
|
|
74412
|
-
* `pollIntervalMs` is the fallback poll cadence the library uses while a
|
|
74413
|
-
* device's MQTT push is down; `0` disables poll fallback.
|
|
74414
|
-
*
|
|
74415
|
-
* The diagnostic catalog (enum/numeric sensors, switches, selects) is seeded
|
|
74416
|
-
* from the cloud SHADOW once at broker start and then tracked by the library's
|
|
74417
|
-
* MQTT push. Live `get_properties` is NOT polled: a docked/asleep robot times it
|
|
74418
|
-
* out (Home Assistant shows the same diagnostics as `unavailable` then), and
|
|
74419
|
-
* hammering it only adds cloud load — so we rely on the shadow seed + push.
|
|
74420
|
-
*/
|
|
74421
|
-
var dreameConfigSchema = object({
|
|
74422
|
-
username: string().min(1).describe("Dreamehome account email / username"),
|
|
74423
|
-
password: string().min(1).describe("Dreamehome account password"),
|
|
74424
|
-
region: DreameRegionSchema.default("eu").describe("Dreame cloud region"),
|
|
74425
|
-
pollIntervalMs: preprocess((v) => v === "" || v === null ? void 0 : v, number().int().min(0).max(36e5).default(3e4)).describe("Poll fallback interval (ms) while MQTT push is down — 0 disables")
|
|
74426
|
-
});
|
|
74427
|
-
/**
|
|
74428
|
-
* Build the `NodreameOptions` the wrapped client constructor expects from the
|
|
74429
|
-
* validated addon config. Pure: same config in → same options out.
|
|
74430
|
-
*/
|
|
74431
|
-
function toNodreameOptions(config) {
|
|
74432
|
-
return {
|
|
74433
|
-
username: config.username,
|
|
74434
|
-
password: config.password,
|
|
74435
|
-
region: config.region,
|
|
74436
|
-
fetchInitialValues: true,
|
|
74437
|
-
pollIntervalMs: config.pollIntervalMs
|
|
74438
|
-
};
|
|
74439
|
-
}
|
|
74440
|
-
/** Top-level addon config — an ordered list of broker entries (default empty). */
|
|
74441
|
-
var dreameAddonConfigSchema = object({ brokers: array(object({
|
|
74442
|
-
/** Stable opaque identifier — e.g. 'dreame_001', 'dreame_002'. */
|
|
74443
|
-
id: string().min(1),
|
|
74444
|
-
/** Human-readable label shown in the admin UI. */
|
|
74445
|
-
name: string().min(1),
|
|
74446
|
-
/** Validated account connection settings. */
|
|
74447
|
-
connection: dreameConfigSchema,
|
|
74448
|
-
/** FK to the spawning integration — auto-cleanup on integration delete. */
|
|
74449
|
-
integrationId: string().optional()
|
|
74450
|
-
})).default([]) });
|
|
74451
|
-
/**
|
|
74452
|
-
* Coerce a loose settings blob (from the broker `add`/`setSettings` cap) through
|
|
74453
|
-
* the connection schema, applying all defaults. Throws a `ZodError` on invalid
|
|
74454
|
-
* input so the caller surfaces a clear error at the system boundary.
|
|
74455
|
-
*/
|
|
74456
|
-
function settingsToDreameConfig(settings) {
|
|
74457
|
-
return dreameConfigSchema.parse(settings ?? {});
|
|
74458
|
-
}
|
|
74459
|
-
/**
|
|
74460
|
-
* Hand-written connection form for the broker/integration creation UI. Mirrors
|
|
74461
|
-
* the Homematic / Home Assistant broker-settings form shape — a flat set of
|
|
74462
|
-
* sections the admin UI renders into the "Add Dreame account" modal.
|
|
74463
|
-
*/
|
|
74464
|
-
function buildConnectionFormSchema() {
|
|
74465
|
-
return { sections: [
|
|
74466
|
-
{
|
|
74467
|
-
id: "credentials",
|
|
74468
|
-
title: "Dreamehome account",
|
|
74469
|
-
description: "Sign in with your Dreamehome (Dreame app) account credentials.",
|
|
74470
|
-
columns: 1,
|
|
74471
|
-
fields: [{
|
|
74472
|
-
type: "text",
|
|
74473
|
-
key: "username",
|
|
74474
|
-
label: "Email / username",
|
|
74475
|
-
required: true,
|
|
74476
|
-
placeholder: "you@example.com"
|
|
74477
|
-
}, {
|
|
74478
|
-
type: "password",
|
|
74479
|
-
key: "password",
|
|
74480
|
-
label: "Password",
|
|
74481
|
-
required: true,
|
|
74482
|
-
showToggle: true
|
|
74483
|
-
}]
|
|
74484
|
-
},
|
|
74485
|
-
{
|
|
74486
|
-
id: "region",
|
|
74487
|
-
title: "Region",
|
|
74488
|
-
description: "Select the Dreame cloud region your account is registered in.",
|
|
74489
|
-
columns: 1,
|
|
74490
|
-
fields: [{
|
|
74491
|
-
type: "select",
|
|
74492
|
-
key: "region",
|
|
74493
|
-
label: "Region",
|
|
74494
|
-
default: "eu",
|
|
74495
|
-
options: DREAME_REGIONS.map((r) => ({
|
|
74496
|
-
value: r,
|
|
74497
|
-
label: r.toUpperCase()
|
|
74498
|
-
}))
|
|
74499
|
-
}]
|
|
74500
|
-
},
|
|
74501
|
-
{
|
|
74502
|
-
id: "advanced",
|
|
74503
|
-
title: "Advanced",
|
|
74504
|
-
columns: 1,
|
|
74505
|
-
fields: [{
|
|
74506
|
-
type: "number",
|
|
74507
|
-
key: "pollIntervalMs",
|
|
74508
|
-
label: "Poll fallback interval (ms, 0 = disabled)",
|
|
74509
|
-
min: 0,
|
|
74510
|
-
max: 36e5,
|
|
74511
|
-
default: 3e4
|
|
74512
|
-
}]
|
|
74513
|
-
}
|
|
74514
|
-
] };
|
|
74515
|
-
}
|
|
74516
|
-
//#endregion
|
|
74517
74469
|
//#region src/dreame-entity-catalog.ts
|
|
74518
74470
|
var STATS = "Statistics";
|
|
74519
74471
|
var MAINT = "Maintenance";
|
|
@@ -76100,35 +76052,51 @@ var DreameFacadeResolver = class {
|
|
|
76100
76052
|
* and device instances. */
|
|
76101
76053
|
var dreameFacades = new DreameFacadeResolver();
|
|
76102
76054
|
//#endregion
|
|
76103
|
-
//#region src/dreame-
|
|
76055
|
+
//#region src/dreame-client-registry.ts
|
|
76056
|
+
/**
|
|
76057
|
+
* Per-integration Dreame connection registry (account mode).
|
|
76058
|
+
*
|
|
76059
|
+
* Under the LOCKED integration/adoption model (design §7.1), Dreame is a
|
|
76060
|
+
* `mode: account` addon: each Dreame integration carries its own Dreamehome
|
|
76061
|
+
* account credentials in its `integration.settings`, and this registry holds one
|
|
76062
|
+
* {@link DreameIntegrationManager} per `integrationId`
|
|
76063
|
+
* (`Map<integrationId, DreameIntegrationManager>`). Multi-account = multiple
|
|
76064
|
+
* integrations; there is NO shared broker.
|
|
76065
|
+
*
|
|
76066
|
+
* The live `Nodreame` facade each manager owns is published on the in-process
|
|
76067
|
+
* {@link dreameFacades} resolver KEYED BY `integrationId`, so the device classes
|
|
76068
|
+
* resolve their live handle by the `brokerId` field they persist — which now
|
|
76069
|
+
* holds the integrationId (unchanged device-side code, see the addon migration
|
|
76070
|
+
* note). MQTT session/socket lifecycle stays entirely inside the manager +
|
|
76071
|
+
* library (no reintroduced socket-leak): the registry only starts/stops managers.
|
|
76072
|
+
*/
|
|
76104
76073
|
/**
|
|
76105
|
-
*
|
|
76106
|
-
*
|
|
76107
|
-
*
|
|
76108
|
-
*
|
|
76074
|
+
* Owns the `Map<integrationId, DreameIntegrationManager>`. Callers reconcile the
|
|
76075
|
+
* map against the live integration list on boot + on every integration lifecycle
|
|
76076
|
+
* event; device classes + the adoption provider resolve their facade by
|
|
76077
|
+
* `integrationId` through {@link dreameFacades}.
|
|
76078
|
+
*
|
|
76079
|
+
* Structural twin of `WyzeClientRegistry` — `upsert` is session-preserving when
|
|
76080
|
+
* credentials are unchanged, `retain` drops vanished integrations.
|
|
76109
76081
|
*/
|
|
76110
|
-
var
|
|
76082
|
+
var DreameClientRegistry = class {
|
|
76111
76083
|
#logger;
|
|
76112
|
-
#
|
|
76113
|
-
#
|
|
76084
|
+
#onConnected;
|
|
76085
|
+
#onDisconnected;
|
|
76114
76086
|
#makeManager;
|
|
76115
|
-
#managers = /* @__PURE__ */ new Map();
|
|
76116
|
-
#facades = /* @__PURE__ */ new Map();
|
|
76117
|
-
#integrationToBroker = /* @__PURE__ */ new Map();
|
|
76118
|
-
#nextId = 1;
|
|
76119
76087
|
#useDefaultFactory;
|
|
76088
|
+
#entries = /* @__PURE__ */ new Map();
|
|
76120
76089
|
constructor(logger, deps = {}) {
|
|
76121
76090
|
this.#logger = logger;
|
|
76122
|
-
this.#
|
|
76123
|
-
this.#
|
|
76091
|
+
this.#onConnected = deps.onConnected ?? (() => void 0);
|
|
76092
|
+
this.#onDisconnected = deps.onDisconnected ?? (() => void 0);
|
|
76124
76093
|
this.#useDefaultFactory = deps.makeManager === void 0;
|
|
76125
76094
|
this.#makeManager = deps.makeManager ?? ((opts) => {
|
|
76126
76095
|
const raw = new Nodreame(toNodreameOptions(opts.connection));
|
|
76127
|
-
|
|
76128
|
-
dreameFacades.set(opts.id, raw);
|
|
76096
|
+
dreameFacades.set(opts.integrationId, raw);
|
|
76129
76097
|
const wrapped = raw;
|
|
76130
76098
|
return new DreameIntegrationManager({
|
|
76131
|
-
id: opts.
|
|
76099
|
+
id: opts.integrationId,
|
|
76132
76100
|
name: opts.name,
|
|
76133
76101
|
connection: opts.connection,
|
|
76134
76102
|
logger: opts.logger,
|
|
@@ -76138,270 +76106,124 @@ var DreameBrokerRegistry = class {
|
|
|
76138
76106
|
});
|
|
76139
76107
|
});
|
|
76140
76108
|
}
|
|
76141
|
-
|
|
76142
|
-
|
|
76143
|
-
for (const entry of entries) this.#seedCounter(entry.id);
|
|
76144
|
-
for (const entry of entries) try {
|
|
76145
|
-
await this.#startManager(entry);
|
|
76146
|
-
} catch (err) {
|
|
76147
|
-
this.#logger.warn("DreameBrokerRegistry: failed to restore manager", {
|
|
76148
|
-
tags: { brokerId: entry.id },
|
|
76149
|
-
meta: { error: errMsg(err) }
|
|
76150
|
-
});
|
|
76151
|
-
}
|
|
76109
|
+
setOnConnected(cb) {
|
|
76110
|
+
this.#onConnected = cb;
|
|
76152
76111
|
}
|
|
76153
|
-
|
|
76154
|
-
|
|
76155
|
-
|
|
76156
|
-
|
|
76112
|
+
setOnDisconnected(cb) {
|
|
76113
|
+
this.#onDisconnected = cb;
|
|
76114
|
+
}
|
|
76115
|
+
/**
|
|
76116
|
+
* Ensure a manager exists for `integrationId` with the given credentials.
|
|
76117
|
+
* Idempotent: an existing entry with identical credentials is preserved
|
|
76118
|
+
* (keeps its live MQTT session); a credentials change re-applies the
|
|
76119
|
+
* connection atomically (stop + start). Best-effort start — a failed login
|
|
76120
|
+
* leaves the entry registered so a later reconcile / lifecycle event retries.
|
|
76121
|
+
*/
|
|
76122
|
+
async upsert(integrationId, name, connection) {
|
|
76123
|
+
const existing = this.#entries.get(integrationId);
|
|
76124
|
+
if (existing) {
|
|
76125
|
+
if (sameConnection(existing.connection, connection)) return;
|
|
76157
76126
|
try {
|
|
76158
|
-
|
|
76127
|
+
if (this.#useDefaultFactory) {
|
|
76128
|
+
const raw = new Nodreame(toNodreameOptions(connection));
|
|
76129
|
+
dreameFacades.set(integrationId, raw);
|
|
76130
|
+
}
|
|
76131
|
+
await existing.manager.applyConnection(connection);
|
|
76132
|
+
this.#entries.set(integrationId, {
|
|
76133
|
+
manager: existing.manager,
|
|
76134
|
+
connection
|
|
76135
|
+
});
|
|
76159
76136
|
} catch (err) {
|
|
76160
|
-
this.#logger.warn("
|
|
76161
|
-
tags: {
|
|
76137
|
+
this.#logger.warn("DreameClientRegistry: applyConnection failed", {
|
|
76138
|
+
tags: { integrationId },
|
|
76162
76139
|
meta: { error: errMsg(err) }
|
|
76163
76140
|
});
|
|
76164
76141
|
}
|
|
76165
|
-
|
|
76166
|
-
|
|
76167
|
-
this.#
|
|
76168
|
-
|
|
76169
|
-
dreameFacades.clear();
|
|
76170
|
-
}
|
|
76171
|
-
setOnBrokerConnected(cb) {
|
|
76172
|
-
this.#onBrokerConnected = cb;
|
|
76173
|
-
}
|
|
76174
|
-
setOnBrokerDisconnected(cb) {
|
|
76175
|
-
this.#onBrokerDisconnected = cb;
|
|
76176
|
-
}
|
|
76177
|
-
async createEntry(name, connection, opts = {}) {
|
|
76178
|
-
const id = this.#allocateId();
|
|
76179
|
-
const entry = {
|
|
76180
|
-
id,
|
|
76142
|
+
return;
|
|
76143
|
+
}
|
|
76144
|
+
const manager = this.#makeManager({
|
|
76145
|
+
integrationId,
|
|
76181
76146
|
name,
|
|
76182
76147
|
connection,
|
|
76183
|
-
|
|
76184
|
-
|
|
76185
|
-
|
|
76186
|
-
|
|
76187
|
-
|
|
76188
|
-
|
|
76189
|
-
|
|
76190
|
-
|
|
76191
|
-
if (!mgr) throw new Error(`DreameBrokerRegistry: unknown broker id "${id}"`);
|
|
76192
|
-
for (const [integrationId, brokerId] of this.#integrationToBroker.entries()) if (brokerId === id) this.#integrationToBroker.delete(integrationId);
|
|
76193
|
-
this.#managers.delete(id);
|
|
76194
|
-
this.#facades.delete(id);
|
|
76195
|
-
dreameFacades.set(id, null);
|
|
76148
|
+
logger: this.#logger,
|
|
76149
|
+
onConnected: (id) => this.#onConnected(id),
|
|
76150
|
+
onDisconnected: (id) => this.#onDisconnected(id)
|
|
76151
|
+
});
|
|
76152
|
+
this.#entries.set(integrationId, {
|
|
76153
|
+
manager,
|
|
76154
|
+
connection
|
|
76155
|
+
});
|
|
76196
76156
|
try {
|
|
76197
|
-
await
|
|
76157
|
+
await manager.start();
|
|
76198
76158
|
} catch (err) {
|
|
76199
|
-
this.#logger.warn("
|
|
76200
|
-
tags: {
|
|
76159
|
+
this.#logger.warn("DreameClientRegistry: manager start failed", {
|
|
76160
|
+
tags: { integrationId },
|
|
76201
76161
|
meta: { error: errMsg(err) }
|
|
76202
76162
|
});
|
|
76203
76163
|
}
|
|
76204
76164
|
}
|
|
76205
|
-
|
|
76206
|
-
|
|
76207
|
-
|
|
76208
|
-
if (
|
|
76209
|
-
|
|
76210
|
-
|
|
76211
|
-
|
|
76165
|
+
/** Stop + drop the manager for an integration that no longer exists. */
|
|
76166
|
+
async remove(integrationId) {
|
|
76167
|
+
const entry = this.#entries.get(integrationId);
|
|
76168
|
+
if (!entry) return;
|
|
76169
|
+
this.#entries.delete(integrationId);
|
|
76170
|
+
dreameFacades.set(integrationId, null);
|
|
76171
|
+
try {
|
|
76172
|
+
await entry.manager.stop();
|
|
76173
|
+
} catch (err) {
|
|
76174
|
+
this.#logger.warn("DreameClientRegistry: remove stop failed", {
|
|
76175
|
+
tags: { integrationId },
|
|
76176
|
+
meta: { error: errMsg(err) }
|
|
76177
|
+
});
|
|
76212
76178
|
}
|
|
76213
|
-
await mgr.applyConnection(connection);
|
|
76214
|
-
return {
|
|
76215
|
-
id,
|
|
76216
|
-
name: mgr.getInfo().name,
|
|
76217
|
-
connection
|
|
76218
|
-
};
|
|
76219
76179
|
}
|
|
76220
|
-
|
|
76221
|
-
|
|
76180
|
+
/** Stop + drop every manager whose integrationId is not in `keep`. */
|
|
76181
|
+
async retain(keep) {
|
|
76182
|
+
const stale = [...this.#entries.keys()].filter((id) => !keep.has(id));
|
|
76183
|
+
for (const id of stale) await this.remove(id);
|
|
76222
76184
|
}
|
|
76223
|
-
|
|
76224
|
-
|
|
76225
|
-
|
|
76226
|
-
|
|
76185
|
+
/** Stop every manager and clear all state (full shutdown). */
|
|
76186
|
+
async shutdown() {
|
|
76187
|
+
const ids = [...this.#entries.keys()];
|
|
76188
|
+
await Promise.all(ids.map(async (id) => {
|
|
76189
|
+
try {
|
|
76190
|
+
await this.#entries.get(id)?.manager.stop();
|
|
76191
|
+
} catch (err) {
|
|
76192
|
+
this.#logger.warn("DreameClientRegistry: shutdown stop failed", {
|
|
76193
|
+
tags: { integrationId: id },
|
|
76194
|
+
meta: { error: errMsg(err) }
|
|
76195
|
+
});
|
|
76196
|
+
}
|
|
76197
|
+
}));
|
|
76198
|
+
this.#entries.clear();
|
|
76199
|
+
dreameFacades.clear();
|
|
76227
76200
|
}
|
|
76228
|
-
|
|
76229
|
-
|
|
76201
|
+
/** True when a manager is registered for the integration. */
|
|
76202
|
+
has(integrationId) {
|
|
76203
|
+
return this.#entries.has(integrationId);
|
|
76230
76204
|
}
|
|
76205
|
+
/** The registered integration ids (one per account). */
|
|
76231
76206
|
list() {
|
|
76232
|
-
return
|
|
76207
|
+
return [...this.#entries.keys()];
|
|
76233
76208
|
}
|
|
76234
|
-
|
|
76235
|
-
|
|
76209
|
+
/** The current connection an integration's manager is configured with. */
|
|
76210
|
+
getConnection(integrationId) {
|
|
76211
|
+
return this.#entries.get(integrationId)?.connection ?? null;
|
|
76236
76212
|
}
|
|
76237
|
-
|
|
76238
|
-
|
|
76213
|
+
/** Immutable status snapshot for an integration's manager. */
|
|
76214
|
+
getInfo(integrationId) {
|
|
76215
|
+
return this.#entries.get(integrationId)?.manager.getInfo() ?? null;
|
|
76239
76216
|
}
|
|
76240
76217
|
size() {
|
|
76241
|
-
return this.#
|
|
76218
|
+
return this.#entries.size;
|
|
76242
76219
|
}
|
|
76243
76220
|
connectedCount() {
|
|
76244
76221
|
let count = 0;
|
|
76245
|
-
for (const
|
|
76222
|
+
for (const entry of this.#entries.values()) if (entry.manager.getInfo().status === "connected") count++;
|
|
76246
76223
|
return count;
|
|
76247
76224
|
}
|
|
76248
|
-
async #startManager(entry) {
|
|
76249
|
-
if (this.#managers.has(entry.id)) {
|
|
76250
|
-
if (entry.integrationId !== void 0) this.#integrationToBroker.set(entry.integrationId, entry.id);
|
|
76251
|
-
return;
|
|
76252
|
-
}
|
|
76253
|
-
const mgr = this.#makeManager({
|
|
76254
|
-
id: entry.id,
|
|
76255
|
-
name: entry.name,
|
|
76256
|
-
connection: entry.connection,
|
|
76257
|
-
logger: this.#logger,
|
|
76258
|
-
onConnected: (id) => this.#onBrokerConnected(id),
|
|
76259
|
-
onDisconnected: (id) => this.#onBrokerDisconnected(id)
|
|
76260
|
-
});
|
|
76261
|
-
this.#managers.set(entry.id, mgr);
|
|
76262
|
-
if (entry.integrationId !== void 0) this.#integrationToBroker.set(entry.integrationId, entry.id);
|
|
76263
|
-
await mgr.start();
|
|
76264
|
-
}
|
|
76265
|
-
#allocateId() {
|
|
76266
|
-
const id = `dreame_${String(this.#nextId).padStart(3, "0")}`;
|
|
76267
|
-
this.#nextId++;
|
|
76268
|
-
return id;
|
|
76269
|
-
}
|
|
76270
|
-
#seedCounter(id) {
|
|
76271
|
-
const match = /^dreame_(\d+)$/.exec(id);
|
|
76272
|
-
if (match === null || match[1] === void 0) return;
|
|
76273
|
-
const n = parseInt(match[1], 10);
|
|
76274
|
-
if (!isNaN(n)) this.#nextId = Math.max(this.#nextId, n + 1);
|
|
76275
|
-
}
|
|
76276
76225
|
};
|
|
76277
76226
|
//#endregion
|
|
76278
|
-
//#region src/dreame-broker-provider.ts
|
|
76279
|
-
/** Kind tag registered by this provider — matches the `listProviders` entry. */
|
|
76280
|
-
var DREAME_KIND = "dreame";
|
|
76281
|
-
/**
|
|
76282
|
-
* Construct the `broker` cap provider for the Dreame addon. Pure builder: all
|
|
76283
|
-
* side-effecting deps are injected so the returned object is unit-testable.
|
|
76284
|
-
* Mirrors `buildHmBrokerProvider`.
|
|
76285
|
-
*/
|
|
76286
|
-
function buildDreameBrokerProvider(deps) {
|
|
76287
|
-
const { ownerAddonId, registry, getBrokers, persistBrokers, cascadeRemoveDevices, logger } = deps;
|
|
76288
|
-
const owns = (id) => getBrokers().some((b) => b.id === id);
|
|
76289
|
-
return {
|
|
76290
|
-
list: async ({ kind }) => {
|
|
76291
|
-
if (kind && kind !== DREAME_KIND) return [];
|
|
76292
|
-
return registry.list().map((info) => ({
|
|
76293
|
-
...info,
|
|
76294
|
-
addonId: ownerAddonId
|
|
76295
|
-
}));
|
|
76296
|
-
},
|
|
76297
|
-
get: async ({ id }) => {
|
|
76298
|
-
const info = registry.get(id);
|
|
76299
|
-
return info ? {
|
|
76300
|
-
...info,
|
|
76301
|
-
addonId: ownerAddonId
|
|
76302
|
-
} : null;
|
|
76303
|
-
},
|
|
76304
|
-
listProviders: async () => [{
|
|
76305
|
-
addonId: ownerAddonId,
|
|
76306
|
-
kinds: [{
|
|
76307
|
-
kind: DREAME_KIND,
|
|
76308
|
-
label: "Dreame"
|
|
76309
|
-
}]
|
|
76310
|
-
}],
|
|
76311
|
-
add: async ({ kind, name, settings }) => {
|
|
76312
|
-
if (kind !== DREAME_KIND) throw new Error(`provider-dreame: only kind '${DREAME_KIND}' is handled here (got '${kind}')`);
|
|
76313
|
-
const entry = await registry.createEntry(name, settingsToDreameConfig(settings));
|
|
76314
|
-
await persistBrokers([...getBrokers(), entry]);
|
|
76315
|
-
return { id: entry.id };
|
|
76316
|
-
},
|
|
76317
|
-
remove: async ({ id }) => {
|
|
76318
|
-
if (!owns(id)) return;
|
|
76319
|
-
await registry.removeEntry(id);
|
|
76320
|
-
await cascadeRemoveDevices(id).catch((err) => {
|
|
76321
|
-
logger.warn("dreame: broker cascade-remove threw", {
|
|
76322
|
-
tags: { brokerId: id },
|
|
76323
|
-
meta: { error: errMsg(err) }
|
|
76324
|
-
});
|
|
76325
|
-
});
|
|
76326
|
-
await persistBrokers(getBrokers().filter((b) => b.id !== id));
|
|
76327
|
-
},
|
|
76328
|
-
testConnection: async ({ id }) => {
|
|
76329
|
-
if (!owns(id)) return {
|
|
76330
|
-
ok: false,
|
|
76331
|
-
error: "unknown broker"
|
|
76332
|
-
};
|
|
76333
|
-
const info = registry.get(id);
|
|
76334
|
-
if (!info) return {
|
|
76335
|
-
ok: false,
|
|
76336
|
-
error: "unknown broker"
|
|
76337
|
-
};
|
|
76338
|
-
if (info.status === "connected") return {
|
|
76339
|
-
ok: true,
|
|
76340
|
-
latencyMs: info.lastCheckedAt ? Math.max(0, Date.now() - info.lastCheckedAt) : 0
|
|
76341
|
-
};
|
|
76342
|
-
return {
|
|
76343
|
-
ok: false,
|
|
76344
|
-
error: info.error ?? `status: ${info.status}`
|
|
76345
|
-
};
|
|
76346
|
-
},
|
|
76347
|
-
getSettings: async ({ id }) => {
|
|
76348
|
-
const entry = getBrokers().find((b) => b.id === id);
|
|
76349
|
-
if (!entry) return null;
|
|
76350
|
-
return {
|
|
76351
|
-
...entry.connection,
|
|
76352
|
-
password: ""
|
|
76353
|
-
};
|
|
76354
|
-
},
|
|
76355
|
-
setSettings: async ({ id, settings }) => {
|
|
76356
|
-
if (!owns(id)) return;
|
|
76357
|
-
const existing = getBrokers().find((b) => b.id === id);
|
|
76358
|
-
if (!existing) return;
|
|
76359
|
-
const parsed = settingsToDreameConfig(settings);
|
|
76360
|
-
const merged = {
|
|
76361
|
-
...parsed,
|
|
76362
|
-
password: parsed.password.length > 0 ? parsed.password : existing.connection.password
|
|
76363
|
-
};
|
|
76364
|
-
const updated = await registry.updateEntry(id, merged);
|
|
76365
|
-
await persistBrokers(getBrokers().map((b) => b.id === id ? updated : b));
|
|
76366
|
-
},
|
|
76367
|
-
getBrokerConfig: async ({ id }) => {
|
|
76368
|
-
const entry = getBrokers().find((b) => b.id === id);
|
|
76369
|
-
if (!entry) return null;
|
|
76370
|
-
return {
|
|
76371
|
-
...entry.connection,
|
|
76372
|
-
password: ""
|
|
76373
|
-
};
|
|
76374
|
-
},
|
|
76375
|
-
getSettingsSchema: async ({ kind }) => {
|
|
76376
|
-
if (kind !== DREAME_KIND) return null;
|
|
76377
|
-
return buildConnectionFormSchema();
|
|
76378
|
-
},
|
|
76379
|
-
testSettings: async ({ kind, settings }) => {
|
|
76380
|
-
if (kind !== DREAME_KIND) return {
|
|
76381
|
-
ok: false,
|
|
76382
|
-
error: `unsupported kind: ${kind}`
|
|
76383
|
-
};
|
|
76384
|
-
try {
|
|
76385
|
-
settingsToDreameConfig(settings);
|
|
76386
|
-
return { ok: true };
|
|
76387
|
-
} catch (err) {
|
|
76388
|
-
return {
|
|
76389
|
-
ok: false,
|
|
76390
|
-
error: errMsg(err)
|
|
76391
|
-
};
|
|
76392
|
-
}
|
|
76393
|
-
},
|
|
76394
|
-
publish: async () => null,
|
|
76395
|
-
subscribe: async () => ({ subscriptionId: "" }),
|
|
76396
|
-
unsubscribe: async () => void 0,
|
|
76397
|
-
getState: async () => null,
|
|
76398
|
-
getStatus: async () => ({
|
|
76399
|
-
brokerCount: registry.size(),
|
|
76400
|
-
connectedCount: registry.connectedCount()
|
|
76401
|
-
})
|
|
76402
|
-
};
|
|
76403
|
-
}
|
|
76404
|
-
//#endregion
|
|
76405
76227
|
//#region src/dreame-mapping.ts
|
|
76406
76228
|
/** Classify a Dreame cloud `model` string into a supported device kind. Pure. */
|
|
76407
76229
|
function classifyDreameModel(model) {
|
|
@@ -76454,11 +76276,11 @@ var DEVICES_FILTER = {
|
|
|
76454
76276
|
label: "Devices",
|
|
76455
76277
|
isDefault: true
|
|
76456
76278
|
};
|
|
76457
|
-
/** Build a `dreameDeviceId → CamStack deviceId` map for a single
|
|
76458
|
-
async function
|
|
76279
|
+
/** Build a `dreameDeviceId → CamStack deviceId` map for a single integration. */
|
|
76280
|
+
async function adoptedMapForIntegration(integrationId, listAdoptedDreame) {
|
|
76459
76281
|
const all = await listAdoptedDreame();
|
|
76460
76282
|
const map = /* @__PURE__ */ new Map();
|
|
76461
|
-
for (const device of all) if (device.config["system"] === "dreame" && device.config["brokerId"] ===
|
|
76283
|
+
for (const device of all) if (device.config["system"] === "dreame" && device.config["brokerId"] === integrationId) {
|
|
76462
76284
|
const dreameId = device.config["dreameDeviceId"];
|
|
76463
76285
|
if (typeof dreameId === "string") map.set(dreameId, device.id);
|
|
76464
76286
|
}
|
|
@@ -76466,15 +76288,15 @@ async function adoptedMapForBroker(brokerId, listAdoptedDreame) {
|
|
|
76466
76288
|
}
|
|
76467
76289
|
/**
|
|
76468
76290
|
* Construct the `device-adoption` cap provider for the Dreame addon. Pure
|
|
76469
|
-
* builder: all side-effecting deps are injected.
|
|
76470
|
-
*
|
|
76291
|
+
* builder: all side-effecting deps are injected. Single `devices` granularity
|
|
76292
|
+
* (one Container per cloud device); resolves each account by `integrationId`.
|
|
76471
76293
|
*/
|
|
76472
76294
|
function buildDreameAdoptionProvider(deps) {
|
|
76473
|
-
const {
|
|
76474
|
-
async function
|
|
76295
|
+
const { getFacade, hasIntegration, listIntegrations, listAdoptedDreame, adoptDevice, removeDevice, findDeviceConfig, logger } = deps;
|
|
76296
|
+
async function allCandidatesForIntegration(integrationId) {
|
|
76475
76297
|
return buildDreameCandidates({
|
|
76476
|
-
devices:
|
|
76477
|
-
adopted: await
|
|
76298
|
+
devices: getFacade(integrationId)?.devices ?? [],
|
|
76299
|
+
adopted: await adoptedMapForIntegration(integrationId, listAdoptedDreame)
|
|
76478
76300
|
});
|
|
76479
76301
|
}
|
|
76480
76302
|
function applyCandidateTextFilter(cands, filterText) {
|
|
@@ -76492,7 +76314,7 @@ function buildDreameAdoptionProvider(deps) {
|
|
|
76492
76314
|
return {
|
|
76493
76315
|
listCandidateFilters: async () => ({ filters: [DEVICES_FILTER] }),
|
|
76494
76316
|
listCandidates: async ({ integrationId, page, pageSize, filterText }) => {
|
|
76495
|
-
const filtered = applyCandidateTextFilter(await
|
|
76317
|
+
const filtered = applyCandidateTextFilter(await allCandidatesForIntegration(integrationId), filterText);
|
|
76496
76318
|
const start = (page - 1) * pageSize;
|
|
76497
76319
|
return {
|
|
76498
76320
|
candidates: filtered.slice(start, start + pageSize),
|
|
@@ -76502,13 +76324,12 @@ function buildDreameAdoptionProvider(deps) {
|
|
|
76502
76324
|
};
|
|
76503
76325
|
},
|
|
76504
76326
|
getCandidate: async ({ integrationId, childNativeId }) => {
|
|
76505
|
-
return (await
|
|
76327
|
+
return (await allCandidatesForIntegration(integrationId)).find((c) => c.childNativeId === childNativeId) ?? null;
|
|
76506
76328
|
},
|
|
76507
76329
|
getStatus: async () => {
|
|
76508
76330
|
try {
|
|
76509
|
-
const brokers = registry.list();
|
|
76510
76331
|
let candidateCount = 0;
|
|
76511
|
-
for (const
|
|
76332
|
+
for (const integrationId of listIntegrations()) candidateCount += (await allCandidatesForIntegration(integrationId)).length;
|
|
76512
76333
|
const adoptedCount = (await listAdoptedDreame()).filter((d) => d.config["system"] === "dreame").length;
|
|
76513
76334
|
return {
|
|
76514
76335
|
lastDiscoveryAt: Date.now(),
|
|
@@ -76527,9 +76348,8 @@ function buildDreameAdoptionProvider(deps) {
|
|
|
76527
76348
|
}
|
|
76528
76349
|
},
|
|
76529
76350
|
refresh: async ({ integrationId }) => {
|
|
76530
|
-
const
|
|
76531
|
-
const
|
|
76532
|
-
const adoptedCount = (await listAdoptedDreame()).filter((d) => d.config["system"] === "dreame" && d.config["brokerId"] === brokerId).length;
|
|
76351
|
+
const candidateCount = (await allCandidatesForIntegration(integrationId)).length;
|
|
76352
|
+
const adoptedCount = (await adoptedMapForIntegration(integrationId, listAdoptedDreame)).size;
|
|
76533
76353
|
return {
|
|
76534
76354
|
lastDiscoveryAt: Date.now(),
|
|
76535
76355
|
candidateCount,
|
|
@@ -76538,18 +76358,17 @@ function buildDreameAdoptionProvider(deps) {
|
|
|
76538
76358
|
};
|
|
76539
76359
|
},
|
|
76540
76360
|
adopt: async ({ integrationId, childNativeIds, perCandidate }) => {
|
|
76541
|
-
const
|
|
76542
|
-
|
|
76543
|
-
if (facade === null) throw new Error(`dreame adopt: broker ${brokerId} not connected`);
|
|
76361
|
+
const facade = getFacade(integrationId);
|
|
76362
|
+
if (facade === null) throw new Error(`dreame adopt: integration ${integrationId} not connected`);
|
|
76544
76363
|
const devices = facade.devices;
|
|
76545
76364
|
const adopted = [];
|
|
76546
76365
|
let failures = 0;
|
|
76547
76366
|
for (const dreameDeviceId of childNativeIds) try {
|
|
76548
76367
|
const dev = devices.find((d) => d.deviceId === dreameDeviceId);
|
|
76549
76368
|
if (dev === void 0) {
|
|
76550
|
-
logger.warn("dreame adopt: device not found on
|
|
76369
|
+
logger.warn("dreame adopt: device not found on integration — skipping", { meta: {
|
|
76551
76370
|
dreameDeviceId,
|
|
76552
|
-
|
|
76371
|
+
integrationId
|
|
76553
76372
|
} });
|
|
76554
76373
|
failures++;
|
|
76555
76374
|
continue;
|
|
@@ -76561,7 +76380,7 @@ function buildDreameAdoptionProvider(deps) {
|
|
|
76561
76380
|
if (candidate === void 0) {
|
|
76562
76381
|
logger.warn("dreame adopt: device model unsupported — skipping", { meta: {
|
|
76563
76382
|
dreameDeviceId,
|
|
76564
|
-
|
|
76383
|
+
integrationId,
|
|
76565
76384
|
model: dev.model
|
|
76566
76385
|
} });
|
|
76567
76386
|
failures++;
|
|
@@ -76570,7 +76389,6 @@ function buildDreameAdoptionProvider(deps) {
|
|
|
76570
76389
|
const name = perCandidate?.[dreameDeviceId]?.name ?? dev.name ?? dreameDeviceId;
|
|
76571
76390
|
const { deviceId, accessoryDeviceIds } = await adoptDevice({
|
|
76572
76391
|
dreameDeviceId,
|
|
76573
|
-
brokerId,
|
|
76574
76392
|
integrationId,
|
|
76575
76393
|
type: candidate.type,
|
|
76576
76394
|
name,
|
|
@@ -76584,7 +76402,7 @@ function buildDreameAdoptionProvider(deps) {
|
|
|
76584
76402
|
} catch (err) {
|
|
76585
76403
|
logger.warn("dreame adopt: failed to adopt device", { meta: {
|
|
76586
76404
|
dreameDeviceId,
|
|
76587
|
-
|
|
76405
|
+
integrationId,
|
|
76588
76406
|
error: errMsg(err)
|
|
76589
76407
|
} });
|
|
76590
76408
|
failures++;
|
|
@@ -76599,9 +76417,10 @@ function buildDreameAdoptionProvider(deps) {
|
|
|
76599
76417
|
const cfg = await findDeviceConfig(camDeviceId);
|
|
76600
76418
|
if (cfg === null) throw new Error(`dreame resync: device ${camDeviceId} not found`);
|
|
76601
76419
|
if (cfg["system"] !== "dreame") throw new Error(`dreame resync: device ${camDeviceId} is not a Dreame device`);
|
|
76602
|
-
const
|
|
76420
|
+
const integrationId = String(cfg["brokerId"]);
|
|
76603
76421
|
const dreameDeviceId = String(cfg["dreameDeviceId"]);
|
|
76604
|
-
if (!(
|
|
76422
|
+
if (!hasIntegration(integrationId)) throw new Error(`dreame resync: integration ${integrationId} not connected`);
|
|
76423
|
+
if (!(getFacade(integrationId)?.devices.some((d) => d.deviceId === dreameDeviceId) ?? false)) throw new Error(`dreame resync: device ${dreameDeviceId} no longer present on integration ${integrationId}`);
|
|
76605
76424
|
return {
|
|
76606
76425
|
changed: false,
|
|
76607
76426
|
rebuiltChildren: 0
|
|
@@ -76610,29 +76429,31 @@ function buildDreameAdoptionProvider(deps) {
|
|
|
76610
76429
|
};
|
|
76611
76430
|
}
|
|
76612
76431
|
//#endregion
|
|
76613
|
-
//#region src/dreame-
|
|
76432
|
+
//#region src/dreame-integration-device-cascade.ts
|
|
76614
76433
|
/**
|
|
76615
76434
|
* Remove every adopted Dreame PARENT device (children cascade via the kernel)
|
|
76616
|
-
* whose persisted config carries `{ system: 'dreame', brokerId }`.
|
|
76617
|
-
*
|
|
76618
|
-
*
|
|
76619
|
-
*
|
|
76435
|
+
* whose persisted config carries `{ system: 'dreame', brokerId: integrationId }`.
|
|
76436
|
+
* Used when an integration is deleted. Best-effort per device; returns the count
|
|
76437
|
+
* removed.
|
|
76438
|
+
*
|
|
76439
|
+
* NOTE: the persisted config field is still named `brokerId` (unchanged
|
|
76440
|
+
* device-side schema) but under the account model it holds the `integrationId`.
|
|
76620
76441
|
*/
|
|
76621
|
-
async function
|
|
76622
|
-
const { reg, devices, addonId,
|
|
76442
|
+
async function cascadeRemoveDevicesForIntegration(input) {
|
|
76443
|
+
const { reg, devices, addonId, integrationId, logger } = input;
|
|
76623
76444
|
let removed = 0;
|
|
76624
76445
|
for (const d of reg.getAllForAddon(addonId)) {
|
|
76625
76446
|
if (d.parentDeviceId !== null) continue;
|
|
76626
76447
|
const cfg = await devices.loadConfig(d.id).catch(() => ({}));
|
|
76627
|
-
if (cfg["system"] !== "dreame" || cfg["brokerId"] !==
|
|
76448
|
+
if (cfg["system"] !== "dreame" || cfg["brokerId"] !== integrationId) continue;
|
|
76628
76449
|
try {
|
|
76629
76450
|
await devices.remove(d.id);
|
|
76630
76451
|
removed += 1;
|
|
76631
76452
|
} catch (err) {
|
|
76632
|
-
logger.warn("dreame:
|
|
76453
|
+
logger.warn("dreame: integration cascade-remove failed", {
|
|
76633
76454
|
tags: { deviceId: d.id },
|
|
76634
76455
|
meta: {
|
|
76635
|
-
|
|
76456
|
+
integrationId,
|
|
76636
76457
|
error: errMsg(err)
|
|
76637
76458
|
}
|
|
76638
76459
|
});
|
|
@@ -76641,20 +76462,22 @@ async function cascadeRemoveDevicesForBroker(input) {
|
|
|
76641
76462
|
return removed;
|
|
76642
76463
|
}
|
|
76643
76464
|
//#endregion
|
|
76644
|
-
//#region src/dreame-
|
|
76465
|
+
//#region src/dreame-integration-offline.ts
|
|
76645
76466
|
/**
|
|
76646
|
-
* Set every Dreame device belonging to `
|
|
76467
|
+
* Set every Dreame device belonging to `integrationId` to the requested online
|
|
76647
76468
|
* state. `devices` is the addon-scoped device list; every Dreame top-level
|
|
76648
|
-
* Container carries `{ system: 'dreame', brokerId }` in its
|
|
76469
|
+
* Container carries `{ system: 'dreame', brokerId: integrationId }` in its
|
|
76470
|
+
* config blob (the `brokerId` field name is unchanged; it now holds the
|
|
76471
|
+
* integrationId).
|
|
76649
76472
|
*
|
|
76650
76473
|
* Churn-free: skips any device already in the target state. Returns the count
|
|
76651
|
-
* of devices actually transitioned.
|
|
76474
|
+
* of devices actually transitioned.
|
|
76652
76475
|
*/
|
|
76653
|
-
function
|
|
76476
|
+
function setIntegrationDevicesOnline(devices, integrationId, online) {
|
|
76654
76477
|
let count = 0;
|
|
76655
76478
|
for (const dev of devices) {
|
|
76656
76479
|
if (dev.config.get("system") !== "dreame") continue;
|
|
76657
|
-
if (dev.config.get("brokerId") !==
|
|
76480
|
+
if (dev.config.get("brokerId") !== integrationId) continue;
|
|
76658
76481
|
if (dev.online === online) continue;
|
|
76659
76482
|
dev.markOnline(online);
|
|
76660
76483
|
count += 1;
|
|
@@ -79433,101 +79256,53 @@ var DreameContainerDevice = class extends BaseDevice$1 {
|
|
|
79433
79256
|
};
|
|
79434
79257
|
//#endregion
|
|
79435
79258
|
//#region src/addon.ts
|
|
79436
|
-
/** Default multi-broker config — a fresh install starts with no accounts. */
|
|
79437
|
-
var DEFAULTS = { brokers: [] };
|
|
79438
79259
|
/**
|
|
79439
|
-
* Dreame device-provider addon (multi-account).
|
|
79260
|
+
* Dreame device-provider addon — `mode: account` (multi-account, broker-less).
|
|
79261
|
+
*
|
|
79262
|
+
* Wraps the `@apocaliss92/nodedreame` Dreamehome cloud client. Each Dreame
|
|
79263
|
+
* integration carries its own account credentials in its `integration.settings`;
|
|
79264
|
+
* the addon holds one {@link DreameIntegrationManager} per `integrationId` in
|
|
79265
|
+
* {@link DreameClientRegistry} (`Map<integrationId, manager>`). There is NO
|
|
79266
|
+
* shared broker. The live `Nodreame` facade each manager owns is published on
|
|
79267
|
+
* the in-process `dreameFacades` resolver KEYED BY `integrationId`.
|
|
79268
|
+
*
|
|
79269
|
+
* A `device-adoption` cap provider enumerates each account's cloud devices and
|
|
79270
|
+
* adopts them; each adopted cloud device becomes a {@link DeviceType.Container}
|
|
79271
|
+
* parent that fans out a single typed accessory child (vacuum or mower) and its
|
|
79272
|
+
* entity children. The device classes resolve their live handle by the
|
|
79273
|
+
* `brokerId` config field — UNCHANGED device-side code; that field now holds the
|
|
79274
|
+
* integrationId (see the migration note in the addon docs).
|
|
79440
79275
|
*
|
|
79441
|
-
*
|
|
79442
|
-
* account = one "broker"; each adopted cloud device becomes a {@link DeviceType.Container}
|
|
79443
|
-
* parent that fans out a single typed accessory child (vacuum or mower). Modelled
|
|
79444
|
-
* directly on the Homematic / Home Assistant device-provider template:
|
|
79445
|
-
* `broker` + `device-adoption` cap providers for connection + adoption, the
|
|
79446
|
-
* `device-provider` cap from the base class, and an in-process facade resolver
|
|
79447
|
-
* (`dreameFacades`) the device classes read.
|
|
79276
|
+
* `hub-only`: the map data-plane binds 127.0.0.1.
|
|
79448
79277
|
*/
|
|
79449
79278
|
var DreameProviderAddon = class extends BaseDeviceProvider {
|
|
79450
79279
|
addonId = "provider-dreame";
|
|
79451
79280
|
providerName = "Dreame";
|
|
79452
79281
|
deviceClasses = { [DeviceType.Container]: DreameContainerDevice };
|
|
79453
|
-
|
|
79282
|
+
clients = null;
|
|
79454
79283
|
/** Teardown for the `/map` data-plane listener (vacuum-map PNG serving). */
|
|
79455
79284
|
mapDataPlaneDispose = null;
|
|
79456
79285
|
constructor() {
|
|
79457
|
-
super({
|
|
79286
|
+
super({});
|
|
79458
79287
|
}
|
|
79459
79288
|
async onInitialize() {
|
|
79460
79289
|
const regs = await super.onInitialize();
|
|
79461
|
-
this.
|
|
79462
|
-
this.
|
|
79463
|
-
this.ctx.logger.info("Dreame:
|
|
79464
|
-
this.
|
|
79290
|
+
this.clients = new DreameClientRegistry(this.ctx.logger);
|
|
79291
|
+
this.clients.setOnConnected((integrationId) => {
|
|
79292
|
+
this.ctx.logger.info("Dreame: integration connected", { meta: { integrationId } });
|
|
79293
|
+
this.setIntegrationDevicesOnline(integrationId, true);
|
|
79465
79294
|
});
|
|
79466
|
-
this.
|
|
79467
|
-
this.
|
|
79295
|
+
this.clients.setOnDisconnected((integrationId) => {
|
|
79296
|
+
this.setIntegrationDevicesOnline(integrationId, false);
|
|
79468
79297
|
});
|
|
79469
|
-
await this.
|
|
79470
|
-
this.ctx.logger.info("Dreame: provider initialised", { meta: { brokerCount: this.config.brokers.length } });
|
|
79471
|
-
await this.reconcileIntegrationsToBrokers();
|
|
79298
|
+
await this.reconcileIntegrations();
|
|
79472
79299
|
this.subscribeIntegrationLifecycle();
|
|
79473
79300
|
await this.setupMapDataPlane();
|
|
79474
|
-
|
|
79475
|
-
|
|
79476
|
-
|
|
79477
|
-
|
|
79478
|
-
|
|
79479
|
-
},
|
|
79480
|
-
{
|
|
79481
|
-
capability: deviceAdoptionCapability,
|
|
79482
|
-
provider: this.buildAdoptionProvider()
|
|
79483
|
-
}
|
|
79484
|
-
];
|
|
79485
|
-
}
|
|
79486
|
-
/** Reconcile the live registry against the persisted `brokers` array after a
|
|
79487
|
-
* settings write (UI save or `broker.*` cap). Mirrors the HA / Homematic addon. */
|
|
79488
|
-
async onConfigChanged() {
|
|
79489
|
-
const reg = this.registry;
|
|
79490
|
-
if (!reg) return;
|
|
79491
|
-
const persisted = this.config.brokers;
|
|
79492
|
-
const liveIds = new Set(reg.list().map((b) => b.id));
|
|
79493
|
-
const persistedIds = new Set(persisted.map((e) => e.id));
|
|
79494
|
-
for (const liveId of liveIds) if (!persistedIds.has(liveId)) try {
|
|
79495
|
-
await reg.removeEntry(liveId);
|
|
79496
|
-
} catch (err) {
|
|
79497
|
-
this.ctx.logger.warn("Dreame onConfigChanged: removeEntry failed", { meta: {
|
|
79498
|
-
brokerId: liveId,
|
|
79499
|
-
error: errMsg(err)
|
|
79500
|
-
} });
|
|
79501
|
-
}
|
|
79502
|
-
for (const entry of persisted) {
|
|
79503
|
-
if (entry.id && liveIds.has(entry.id)) {
|
|
79504
|
-
if (sameConnection(entry.connection, reg.getConnection(entry.id))) continue;
|
|
79505
|
-
try {
|
|
79506
|
-
await reg.updateEntry(entry.id, entry.connection);
|
|
79507
|
-
} catch (err) {
|
|
79508
|
-
this.ctx.logger.warn("Dreame onConfigChanged: updateEntry failed", { meta: {
|
|
79509
|
-
brokerId: entry.id,
|
|
79510
|
-
error: errMsg(err)
|
|
79511
|
-
} });
|
|
79512
|
-
}
|
|
79513
|
-
continue;
|
|
79514
|
-
}
|
|
79515
|
-
try {
|
|
79516
|
-
const created = await reg.createEntry(entry.name, entry.connection);
|
|
79517
|
-
if (created.id !== entry.id) {
|
|
79518
|
-
const next = persisted.map((b) => b === entry ? {
|
|
79519
|
-
...b,
|
|
79520
|
-
id: created.id
|
|
79521
|
-
} : b);
|
|
79522
|
-
await this.updateGlobalSettings({ brokers: next });
|
|
79523
|
-
}
|
|
79524
|
-
} catch (err) {
|
|
79525
|
-
this.ctx.logger.warn("Dreame onConfigChanged: failed to start new broker", { meta: {
|
|
79526
|
-
brokerName: entry.name,
|
|
79527
|
-
error: errMsg(err)
|
|
79528
|
-
} });
|
|
79529
|
-
}
|
|
79530
|
-
}
|
|
79301
|
+
this.ctx.logger.info("Dreame: provider initialised", { meta: { integrationCount: this.requireClients().size() } });
|
|
79302
|
+
return [...regs, {
|
|
79303
|
+
capability: deviceAdoptionCapability,
|
|
79304
|
+
provider: this.buildAdoptionProvider()
|
|
79305
|
+
}];
|
|
79531
79306
|
}
|
|
79532
79307
|
async onShutdown() {
|
|
79533
79308
|
try {
|
|
@@ -79538,13 +79313,17 @@ var DreameProviderAddon = class extends BaseDeviceProvider {
|
|
|
79538
79313
|
this.mapDataPlaneDispose = null;
|
|
79539
79314
|
setMapBaseUrl(null);
|
|
79540
79315
|
try {
|
|
79541
|
-
await this.
|
|
79316
|
+
await this.clients?.shutdown();
|
|
79542
79317
|
} catch (err) {
|
|
79543
79318
|
this.ctx.logger.warn("Dreame: provider shutdown error", { meta: { error: errMsg(err) } });
|
|
79544
79319
|
}
|
|
79545
|
-
this.
|
|
79320
|
+
this.clients = null;
|
|
79546
79321
|
await super.onShutdown();
|
|
79547
79322
|
}
|
|
79323
|
+
requireClients() {
|
|
79324
|
+
if (!this.clients) throw new Error("Dreame provider not initialised");
|
|
79325
|
+
return this.clients;
|
|
79326
|
+
}
|
|
79548
79327
|
/**
|
|
79549
79328
|
* Register the `/map` data-plane route that streams rendered vacuum-map PNGs
|
|
79550
79329
|
* to the browser (`<img>` loads `/addon/<addonId>/map?did=…&t=…`). The map
|
|
@@ -79575,93 +79354,76 @@ var DreameProviderAddon = class extends BaseDeviceProvider {
|
|
|
79575
79354
|
this.ctx.logger.warn("Dreame: map data-plane setup failed", { meta: { error: errMsg(err) } });
|
|
79576
79355
|
}
|
|
79577
79356
|
}
|
|
79578
|
-
/**
|
|
79579
|
-
*
|
|
79357
|
+
/**
|
|
79358
|
+
* Stable id, integration-scoped so two accounts exposing the same cloud device
|
|
79359
|
+
* id don't collide. `brokerId` in the config is the integrationId (unchanged
|
|
79360
|
+
* field name).
|
|
79361
|
+
*/
|
|
79580
79362
|
generateStableId(_type, config) {
|
|
79581
79363
|
return `dreame:${String(config?.["brokerId"] ?? "unknown")}:${String(config?.["dreameDeviceId"] ?? Date.now())}`;
|
|
79582
79364
|
}
|
|
79583
|
-
buildBrokerProvider() {
|
|
79584
|
-
return buildDreameBrokerProvider({
|
|
79585
|
-
ownerAddonId: this.ctx.id,
|
|
79586
|
-
registry: this.requireRegistry(),
|
|
79587
|
-
getBrokers: () => this.config.brokers,
|
|
79588
|
-
persistBrokers: (brokers) => this.updateGlobalSettings({ brokers: [...brokers] }),
|
|
79589
|
-
cascadeRemoveDevices: (brokerId) => this.cascadeRemoveDevicesForBroker(brokerId),
|
|
79590
|
-
logger: this.ctx.logger
|
|
79591
|
-
});
|
|
79592
|
-
}
|
|
79593
|
-
async cascadeRemoveDevicesForBroker(brokerId) {
|
|
79594
|
-
const reg = this.ctx.kernel.deviceRegistry;
|
|
79595
|
-
const devices = this.ctx.kernel.devices;
|
|
79596
|
-
if (!reg || !devices) return;
|
|
79597
|
-
await cascadeRemoveDevicesForBroker({
|
|
79598
|
-
reg,
|
|
79599
|
-
devices,
|
|
79600
|
-
addonId: this.addonId,
|
|
79601
|
-
brokerId,
|
|
79602
|
-
logger: this.ctx.logger
|
|
79603
|
-
});
|
|
79604
|
-
}
|
|
79605
79365
|
/**
|
|
79606
|
-
*
|
|
79607
|
-
*
|
|
79608
|
-
*
|
|
79609
|
-
* was deleted
|
|
79610
|
-
*
|
|
79366
|
+
* Rebuild the `Map<integrationId, manager>` from the live integration list:
|
|
79367
|
+
* for each surviving Dreame integration read its settings and upsert a manager
|
|
79368
|
+
* (session-preserving if credentials are unchanged); drop managers whose
|
|
79369
|
+
* integration was deleted/disabled — cascade-removing their adopted devices.
|
|
79370
|
+
* Idempotent; runs on boot + on every integration lifecycle event. Guarded so
|
|
79371
|
+
* a failure never fails init.
|
|
79611
79372
|
*/
|
|
79612
|
-
async
|
|
79373
|
+
async reconcileIntegrations() {
|
|
79374
|
+
const reg = this.requireClients();
|
|
79613
79375
|
try {
|
|
79614
|
-
const mine = (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id);
|
|
79615
|
-
const surviving = new Set(
|
|
79616
|
-
for (const integration of mine) {
|
|
79617
|
-
const
|
|
79618
|
-
|
|
79619
|
-
|
|
79620
|
-
if (!this.config.brokers.some((b) => b.id === brokerId)) {
|
|
79621
|
-
this.ctx.logger.warn("Dreame integration→broker: linked broker not found", { meta: {
|
|
79622
|
-
integrationId: integration.id,
|
|
79623
|
-
brokerId
|
|
79624
|
-
} });
|
|
79376
|
+
const mine = (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id && i.enabled);
|
|
79377
|
+
const surviving = /* @__PURE__ */ new Set();
|
|
79378
|
+
for (const integration of mine) try {
|
|
79379
|
+
const connection = connectionFromSettings(await this.ctx.api.integrations.getSettings.query({ id: integration.id }));
|
|
79380
|
+
if (!connection) {
|
|
79381
|
+
this.ctx.logger.warn("Dreame integration has no complete credentials — skipping", { meta: { integrationId: integration.id } });
|
|
79625
79382
|
continue;
|
|
79626
79383
|
}
|
|
79627
|
-
|
|
79628
|
-
|
|
79629
|
-
const toRemove = this.config.brokers.filter((b) => b.integrationId !== void 0 && !surviving.has(b.integrationId)).map((b) => b.id);
|
|
79630
|
-
if (toRemove.length === 0) return;
|
|
79631
|
-
for (const id of toRemove) try {
|
|
79632
|
-
await this.requireRegistry().removeEntry(id);
|
|
79633
|
-
await this.cascadeRemoveDevicesForBroker(id);
|
|
79384
|
+
await reg.upsert(integration.id, integration.name, connection);
|
|
79385
|
+
surviving.add(integration.id);
|
|
79634
79386
|
} catch (err) {
|
|
79635
|
-
this.ctx.logger.warn("Dreame
|
|
79636
|
-
|
|
79387
|
+
this.ctx.logger.warn("Dreame reconcile: failed to read integration settings", { meta: {
|
|
79388
|
+
integrationId: integration.id,
|
|
79637
79389
|
error: errMsg(err)
|
|
79638
79390
|
} });
|
|
79639
79391
|
}
|
|
79640
|
-
const
|
|
79641
|
-
await
|
|
79392
|
+
const vanished = reg.list().filter((id) => !surviving.has(id));
|
|
79393
|
+
await reg.retain(surviving);
|
|
79394
|
+
for (const integrationId of vanished) await this.cascadeRemoveDevicesForIntegration(integrationId);
|
|
79642
79395
|
} catch (err) {
|
|
79643
|
-
this.ctx.logger.warn("Dreame integration
|
|
79396
|
+
this.ctx.logger.warn("Dreame integration reconcile failed", { meta: { error: errMsg(err) } });
|
|
79644
79397
|
}
|
|
79645
79398
|
}
|
|
79646
79399
|
subscribeIntegrationLifecycle() {
|
|
79647
79400
|
const handler = (event) => {
|
|
79648
79401
|
const addonId = event.data["addonId"];
|
|
79649
79402
|
if (typeof addonId === "string" && addonId !== this.ctx.id) return;
|
|
79650
|
-
this.
|
|
79403
|
+
this.reconcileIntegrations();
|
|
79651
79404
|
};
|
|
79652
79405
|
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationEnabled }, handler);
|
|
79653
79406
|
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, handler);
|
|
79654
79407
|
this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, handler);
|
|
79655
79408
|
}
|
|
79409
|
+
async cascadeRemoveDevicesForIntegration(integrationId) {
|
|
79410
|
+
const reg = this.ctx.kernel.deviceRegistry;
|
|
79411
|
+
const devices = this.ctx.kernel.devices;
|
|
79412
|
+
if (!reg || !devices) return;
|
|
79413
|
+
await cascadeRemoveDevicesForIntegration({
|
|
79414
|
+
reg,
|
|
79415
|
+
devices,
|
|
79416
|
+
addonId: this.addonId,
|
|
79417
|
+
integrationId,
|
|
79418
|
+
logger: this.ctx.logger
|
|
79419
|
+
});
|
|
79420
|
+
}
|
|
79656
79421
|
buildAdoptionProvider() {
|
|
79657
79422
|
return buildDreameAdoptionProvider({
|
|
79658
|
-
registry: this.requireRegistry(),
|
|
79659
79423
|
logger: this.ctx.logger,
|
|
79660
|
-
|
|
79661
|
-
|
|
79662
|
-
|
|
79663
|
-
return brokerId;
|
|
79664
|
-
},
|
|
79424
|
+
getFacade: (integrationId) => dreameFacades.get(integrationId),
|
|
79425
|
+
hasIntegration: (integrationId) => this.requireClients().has(integrationId),
|
|
79426
|
+
listIntegrations: () => this.requireClients().list(),
|
|
79665
79427
|
listAdoptedDreame: async () => {
|
|
79666
79428
|
const reg = this.ctx.kernel.deviceRegistry;
|
|
79667
79429
|
const devices = this.ctx.kernel.devices;
|
|
@@ -79677,12 +79439,12 @@ var DreameProviderAddon = class extends BaseDeviceProvider {
|
|
|
79677
79439
|
}
|
|
79678
79440
|
return out;
|
|
79679
79441
|
},
|
|
79680
|
-
adoptDevice: async ({ dreameDeviceId,
|
|
79442
|
+
adoptDevice: async ({ dreameDeviceId, integrationId, name, model }) => {
|
|
79681
79443
|
const devices = this.ctx.kernel.devices;
|
|
79682
79444
|
if (!devices) throw new Error("dreame adopt: kernel.devices unavailable");
|
|
79683
79445
|
const config = {
|
|
79684
79446
|
dreameDeviceId,
|
|
79685
|
-
brokerId,
|
|
79447
|
+
brokerId: integrationId,
|
|
79686
79448
|
model,
|
|
79687
79449
|
system: "dreame",
|
|
79688
79450
|
integrationId,
|
|
@@ -79711,18 +79473,7 @@ var DreameProviderAddon = class extends BaseDeviceProvider {
|
|
|
79711
79473
|
});
|
|
79712
79474
|
}
|
|
79713
79475
|
globalSettingsSchema() {
|
|
79714
|
-
return
|
|
79715
|
-
id: "dreame-broker",
|
|
79716
|
-
title: "Dreame account",
|
|
79717
|
-
description: "Dreame accounts are managed in the External systems → Brokers tab.",
|
|
79718
|
-
columns: 1,
|
|
79719
|
-
fields: [{
|
|
79720
|
-
type: "info",
|
|
79721
|
-
key: "brokerHelp",
|
|
79722
|
-
label: "Accounts are managed separately",
|
|
79723
|
-
content: "This integration links to a Dreamehome account broker. Add, edit, or remove accounts from External systems → Brokers. The integration only stores a reference to its broker — credentials live on the broker."
|
|
79724
|
-
}]
|
|
79725
|
-
}] });
|
|
79476
|
+
return buildConnectionFormSchema();
|
|
79726
79477
|
}
|
|
79727
79478
|
async supportsManualCreation() {
|
|
79728
79479
|
return false;
|
|
@@ -79733,19 +79484,15 @@ var DreameProviderAddon = class extends BaseDeviceProvider {
|
|
|
79733
79484
|
async onCreateDevice(_type, _config) {
|
|
79734
79485
|
throw new Error("Dreame devices are adopted from the cloud, not created manually");
|
|
79735
79486
|
}
|
|
79736
|
-
|
|
79487
|
+
setIntegrationDevicesOnline(integrationId, online) {
|
|
79737
79488
|
const reg = this.ctx.kernel.deviceRegistry;
|
|
79738
79489
|
if (!reg) return;
|
|
79739
|
-
const n =
|
|
79740
|
-
if (n > 0) this.ctx.logger.info("Dreame:
|
|
79741
|
-
|
|
79490
|
+
const n = setIntegrationDevicesOnline(reg.getAllForAddon(this.addonId), integrationId, online);
|
|
79491
|
+
if (n > 0) this.ctx.logger.info("Dreame: integration devices " + (online ? "online" : "offline"), { meta: {
|
|
79492
|
+
integrationId,
|
|
79742
79493
|
count: n
|
|
79743
79494
|
} });
|
|
79744
79495
|
}
|
|
79745
|
-
requireRegistry() {
|
|
79746
|
-
if (!this.registry) throw new Error("Dreame provider not initialised");
|
|
79747
|
-
return this.registry;
|
|
79748
|
-
}
|
|
79749
79496
|
};
|
|
79750
79497
|
//#endregion
|
|
79751
|
-
export { DreameProviderAddon, mapMowerStatusToActivity as a, deviceTypeForKind as c, dreameConfigSchema as d, toNodreameOptions as f, tokenToSuctionLevel as i, buildConnectionFormSchema as l, suctionLevelToken as n, classifyDreameModel as o, supportedFanSpeedTokens as r, controlCapForKind as s, mapMiotStateToVacuumState as t,
|
|
79498
|
+
export { DreameProviderAddon, mapMowerStatusToActivity as a, deviceTypeForKind as c, dreameConfigSchema as d, toNodreameOptions as f, tokenToSuctionLevel as i, buildConnectionFormSchema as l, suctionLevelToken as n, classifyDreameModel as o, supportedFanSpeedTokens as r, controlCapForKind as s, mapMiotStateToVacuumState as t, connectionFromSettings as u };
|