@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.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
- var brokerCapability = {
14822
- name: "broker",
14823
- scope: "system",
14824
- mode: "collection",
14825
- providerKind: "broker",
14826
- status: {
14827
- schema: RegistryStatusSchema,
14828
- kind: "poll"
14829
- },
14830
- methods: {
14831
- list: method(ListInputSchema, array(BrokerInfoSchema$1)),
14832
- get: method(GetInputSchema, BrokerInfoSchema$1.nullable()),
14833
- /** Enumerate which addon provides which broker kind(s) for the
14834
- * unified create picker. The auto-mount fans this array across
14835
- * every registered `broker` provider (array-output method), so the
14836
- * picker sees every kind from every provider in one call. */
14837
- listProviders: method(_void(), array(BrokerProviderInfoSchema), { auth: "admin" }),
14838
- add: method(AddInputSchema, AddResultSchema, {
14839
- kind: "mutation",
14840
- auth: "admin"
14841
- }),
14842
- remove: method(RemoveInputSchema, _void(), {
14843
- kind: "mutation",
14844
- auth: "admin"
14845
- }),
14846
- testConnection: method(GetInputSchema, TestConnectionResultSchema, {
14847
- kind: "mutation",
14848
- auth: "admin"
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-broker-registry.ts
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
- * Manages N live Dreamehome account connections ("brokers"), each backed by a
76106
- * {@link DreameIntegrationManager}. Allocates stable ids (`dreame_001`, …),
76107
- * supports CRUD, an integration FK index for cascade-delete, and lifecycle
76108
- * helpers. Mirrors `HmBrokerRegistry`.
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 DreameBrokerRegistry = class {
76082
+ var DreameClientRegistry = class {
76111
76083
  #logger;
76112
- #onBrokerConnected;
76113
- #onBrokerDisconnected;
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.#onBrokerConnected = deps.onBrokerConnected ?? (() => void 0);
76123
- this.#onBrokerDisconnected = deps.onBrokerDisconnected ?? (() => void 0);
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
- this.#facades.set(opts.id, raw);
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.id,
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
- /** Restore persisted broker entries on boot (best-effort per manager). */
76142
- async restore(entries) {
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
- /** Stop all managers and clear state. */
76154
- async shutdown() {
76155
- const ids = Array.from(this.#managers.keys());
76156
- await Promise.all(ids.map(async (id) => {
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
- await this.#managers.get(id)?.stop();
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("DreameBrokerRegistry: shutdown stop failed", {
76161
- tags: { brokerId: id },
76137
+ this.#logger.warn("DreameClientRegistry: applyConnection failed", {
76138
+ tags: { integrationId },
76162
76139
  meta: { error: errMsg(err) }
76163
76140
  });
76164
76141
  }
76165
- }));
76166
- this.#managers.clear();
76167
- this.#facades.clear();
76168
- this.#integrationToBroker.clear();
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
- ...opts.integrationId !== void 0 ? { integrationId: opts.integrationId } : {}
76184
- };
76185
- await this.#startManager(entry);
76186
- if (opts.integrationId !== void 0) this.#integrationToBroker.set(opts.integrationId, id);
76187
- return entry;
76188
- }
76189
- async removeEntry(id) {
76190
- const mgr = this.#managers.get(id);
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 mgr.stop();
76157
+ await manager.start();
76198
76158
  } catch (err) {
76199
- this.#logger.warn("DreameBrokerRegistry: removeEntry stop failed", {
76200
- tags: { brokerId: id },
76159
+ this.#logger.warn("DreameClientRegistry: manager start failed", {
76160
+ tags: { integrationId },
76201
76161
  meta: { error: errMsg(err) }
76202
76162
  });
76203
76163
  }
76204
76164
  }
76205
- async updateEntry(id, connection) {
76206
- const mgr = this.#managers.get(id);
76207
- if (!mgr) throw new Error(`DreameBrokerRegistry: unknown broker id "${id}"`);
76208
- if (this.#useDefaultFactory) {
76209
- const raw = new Nodreame(toNodreameOptions(connection));
76210
- this.#facades.set(id, raw);
76211
- dreameFacades.set(id, raw);
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
- linkIntegration(integrationId, brokerId) {
76221
- this.#integrationToBroker.set(integrationId, brokerId);
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
- getBrokerIdByIntegrationId(integrationId) {
76224
- const brokerId = this.#integrationToBroker.get(integrationId);
76225
- if (brokerId === void 0) throw new Error(`DreameBrokerRegistry: no broker linked for integration id "${integrationId}"`);
76226
- return brokerId;
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
- getFacade(id) {
76229
- return this.#facades.get(id) ?? null;
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 Array.from(this.#managers.values()).map((m) => m.getInfo());
76207
+ return [...this.#entries.keys()];
76233
76208
  }
76234
- get(id) {
76235
- return this.#managers.get(id)?.getInfo() ?? null;
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
- getConnection(id) {
76238
- return this.#managers.get(id)?.getConnection() ?? null;
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.#managers.size;
76218
+ return this.#entries.size;
76242
76219
  }
76243
76220
  connectedCount() {
76244
76221
  let count = 0;
76245
- for (const mgr of this.#managers.values()) if (mgr.getInfo().status === "connected") count++;
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 broker. */
76458
- async function adoptedMapForBroker(brokerId, listAdoptedDreame) {
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"] === 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. Mirrors `buildHmAdoptionProvider`
76470
- * but with a single `devices` granularity (one Container per cloud device).
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 { registry, getBrokerIdForIntegration, listAdoptedDreame, adoptDevice, removeDevice, findDeviceConfig, logger } = deps;
76474
- async function allCandidatesForBroker(brokerId) {
76295
+ const { getFacade, hasIntegration, listIntegrations, listAdoptedDreame, adoptDevice, removeDevice, findDeviceConfig, logger } = deps;
76296
+ async function allCandidatesForIntegration(integrationId) {
76475
76297
  return buildDreameCandidates({
76476
- devices: registry.getFacade(brokerId)?.devices ?? [],
76477
- adopted: await adoptedMapForBroker(brokerId, listAdoptedDreame)
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 allCandidatesForBroker(await getBrokerIdForIntegration(integrationId)), filterText);
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 allCandidatesForBroker(await getBrokerIdForIntegration(integrationId))).find((c) => c.childNativeId === childNativeId) ?? null;
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 broker of brokers) candidateCount += (await allCandidatesForBroker(broker.id)).length;
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 brokerId = await getBrokerIdForIntegration(integrationId);
76531
- const candidateCount = (await allCandidatesForBroker(brokerId)).length;
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 brokerId = await getBrokerIdForIntegration(integrationId);
76542
- const facade = registry.getFacade(brokerId);
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 broker — skipping", { meta: {
76369
+ logger.warn("dreame adopt: device not found on integration — skipping", { meta: {
76551
76370
  dreameDeviceId,
76552
- brokerId
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
- brokerId,
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
- brokerId,
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 brokerId = String(cfg["brokerId"]);
76420
+ const integrationId = String(cfg["brokerId"]);
76603
76421
  const dreameDeviceId = String(cfg["dreameDeviceId"]);
76604
- if (!(registry.getFacade(brokerId)?.devices.some((d) => d.deviceId === dreameDeviceId) ?? false)) throw new Error(`dreame resync: device ${dreameDeviceId} no longer present on broker ${brokerId}`);
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-broker-device-cascade.ts
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 }`. Used when a
76617
- * broker is removed (broker.remove) and when its spawning integration is
76618
- * deleted. Best-effort per device; returns the count removed. Mirrors the
76619
- * Homematic cascade helper.
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 cascadeRemoveDevicesForBroker(input) {
76622
- const { reg, devices, addonId, brokerId, logger } = input;
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"] !== brokerId) continue;
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: broker cascade-remove failed", {
76453
+ logger.warn("dreame: integration cascade-remove failed", {
76633
76454
  tags: { deviceId: d.id },
76634
76455
  meta: {
76635
- brokerId,
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-broker-offline.ts
76465
+ //#region src/dreame-integration-offline.ts
76645
76466
  /**
76646
- * Set every Dreame device belonging to `brokerId` to the requested online
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 config blob.
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. Mirrors the Homematic offline helper.
76474
+ * of devices actually transitioned.
76652
76475
  */
76653
- function setBrokerDevicesOnline(devices, brokerId, online) {
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") !== brokerId) continue;
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
- * Wraps the `@apocaliss92/nodedreame` Dreamehome cloud client. One registered
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
- registry = null;
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({ ...DEFAULTS });
79286
+ super({});
79458
79287
  }
79459
79288
  async onInitialize() {
79460
79289
  const regs = await super.onInitialize();
79461
- this.registry = new DreameBrokerRegistry(this.ctx.logger);
79462
- this.registry.setOnBrokerConnected((brokerId) => {
79463
- this.ctx.logger.info("Dreame: broker connected", { meta: { brokerId } });
79464
- this.setBrokerDevicesOnline(brokerId, true);
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.registry.setOnBrokerDisconnected((brokerId) => {
79467
- this.setBrokerDevicesOnline(brokerId, false);
79295
+ this.clients.setOnDisconnected((integrationId) => {
79296
+ this.setIntegrationDevicesOnline(integrationId, false);
79468
79297
  });
79469
- await this.registry.restore(this.config.brokers);
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
- return [
79475
- ...regs,
79476
- {
79477
- capability: brokerCapability,
79478
- provider: this.buildBrokerProvider()
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.registry?.shutdown();
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.registry = null;
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
- /** Stable id, broker-scoped so two accounts exposing the same device id don't
79579
- * collide (mirrors the HA/Homematic broker-scoping). */
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
- * Link each surviving integration (carrying `{ brokerId }` in its settings) to
79607
- * its broker so the generic `device-adoption` cap resolves the account, and
79608
- * cascade-clean brokers (+ their adopted devices) whose spawning integration
79609
- * was deleted. Idempotent; runs on boot + on every integration lifecycle event.
79610
- * Guarded so a failure never fails init.
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 reconcileIntegrationsToBrokers() {
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(mine.map((i) => i.id));
79616
- for (const integration of mine) {
79617
- const settings = await this.ctx.api.integrations.getSettings.query({ id: integration.id });
79618
- const brokerId = typeof settings["brokerId"] === "string" ? settings["brokerId"] : void 0;
79619
- if (brokerId === void 0) continue;
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
- this.requireRegistry().linkIntegration(integration.id, brokerId);
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 integration→broker: broker cleanup failed", { meta: {
79636
- brokerId: id,
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 nextBrokers = this.config.brokers.filter((b) => !toRemove.includes(b.id));
79641
- await this.updateGlobalSettings({ brokers: nextBrokers });
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→broker reconcile failed", { meta: { error: errMsg(err) } });
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.reconcileIntegrationsToBrokers();
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
- getBrokerIdForIntegration: async (id) => {
79661
- const brokerId = (await this.ctx.api.integrations.getSettings.query({ id }))["brokerId"];
79662
- if (typeof brokerId !== "string") throw new Error(`integration ${id} has no linked brokerId`);
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, brokerId, integrationId, name, model }) => {
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 this.schema({ sections: [{
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
- setBrokerDevicesOnline(brokerId, online) {
79487
+ setIntegrationDevicesOnline(integrationId, online) {
79737
79488
  const reg = this.ctx.kernel.deviceRegistry;
79738
79489
  if (!reg) return;
79739
- const n = setBrokerDevicesOnline(reg.getAllForAddon(this.addonId), brokerId, online);
79740
- if (n > 0) this.ctx.logger.info("Dreame: broker devices " + (online ? "online" : "offline"), { meta: {
79741
- brokerId,
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, dreameAddonConfigSchema as u };
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 };