@camstack/addon-provider-dreame 0.1.14 → 0.1.16

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 CHANGED
@@ -4680,7 +4680,7 @@ function preprocess(fn, schema) {
4680
4680
  });
4681
4681
  }
4682
4682
  //#endregion
4683
- //#region ../types/dist/sleep-B3AOslwX.mjs
4683
+ //#region ../types/dist/sleep-C2M2zF7x.mjs
4684
4684
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4685
4685
  EventCategory["SystemBoot"] = "system.boot";
4686
4686
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -6248,6 +6248,12 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
6248
6248
  DeviceType["Switch"] = "switch";
6249
6249
  DeviceType["Sensor"] = "sensor";
6250
6250
  DeviceType["Thermostat"] = "thermostat";
6251
+ /** Air-conditioner / heat-pump climate device (HVAC) — shares the
6252
+ * `climate-control` cap surface with `Thermostat` but renders a
6253
+ * dedicated AC-appropriate control UI (mode chips, fan speed,
6254
+ * independent vertical/horizontal swing). Sources: native Gree, and
6255
+ * reusable by other AC integrations. */
6256
+ DeviceType["Climate"] = "climate";
6251
6257
  DeviceType["Button"] = "button";
6252
6258
  /** Generic stateless event emitter — carries a device's EXACT declared
6253
6259
  * event vocabulary verbatim (no normalization). Installed with the
@@ -9043,7 +9049,7 @@ var climateControlCapability = {
9043
9049
  scope: "device",
9044
9050
  deviceNative: true,
9045
9051
  mode: "singleton",
9046
- deviceTypes: [DeviceType.Thermostat],
9052
+ deviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
9047
9053
  methods: {
9048
9054
  setMode: method(object({
9049
9055
  deviceId: number().int().nonnegative(),
@@ -13642,10 +13648,30 @@ var deviceProviderCapability = {
13642
13648
  type: string()
13643
13649
  }))),
13644
13650
  supportsDiscovery: method(object({}), boolean()),
13645
- discoverDevices: method(object({}), array(DiscoveryCandidateSchema), {
13651
+ /**
13652
+ * Run a network scan. `params` carries optional provider-specific scan
13653
+ * inputs (e.g. a broadcast address / subnet for cross-subnet discovery),
13654
+ * shaped by `getDiscoveryParamsSchema`. Omitted for the generic scan
13655
+ * (provider uses its local-network default).
13656
+ */
13657
+ discoverDevices: method(object({ params: record(string(), unknown()).optional() }), array(DiscoveryCandidateSchema), {
13646
13658
  kind: "mutation",
13647
13659
  auth: "admin"
13648
13660
  }),
13661
+ /**
13662
+ * Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
13663
+ * provider accepts (e.g. Gree's broadcast address for a different subnet).
13664
+ * `null` when the provider takes no extra scan params — the generic
13665
+ * aggregated scan never renders this; the per-integration scan does.
13666
+ */
13667
+ getDiscoveryParamsSchema: method(object({}), CreationSchemaOutputSchema),
13668
+ /**
13669
+ * The DeviceType this provider creates via manual add (Camera for
13670
+ * Reolink/ONVIF, Container for Gree, Hub for Ecowitt). `null` when the
13671
+ * provider does not support manual creation. Lets the Add-Device dialog
13672
+ * pick the right type instead of assuming Camera.
13673
+ */
13674
+ getManualCreationType: method(object({}), object({ deviceType: _enum(DeviceType).nullable() })),
13649
13675
  adoptDiscoveredDevice: method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
13650
13676
  kind: "mutation",
13651
13677
  auth: "admin"
@@ -13769,9 +13795,23 @@ var BaseDeviceProvider = class extends BaseAddon {
13769
13795
  async supportsDiscovery() {
13770
13796
  return false;
13771
13797
  }
13772
- async discoverDevices() {
13798
+ async discoverDevices(_input) {
13773
13799
  return [];
13774
13800
  }
13801
+ /** Extra per-scan input form (e.g. a broadcast address for another subnet).
13802
+ * Null = no extra params. Override in providers that support scoped scans. */
13803
+ async getDiscoveryParamsSchema() {
13804
+ return null;
13805
+ }
13806
+ /**
13807
+ * The DeviceType this provider creates via manual add — derived from the
13808
+ * `deviceClasses` map (first registered type). `null` when manual creation is
13809
+ * unsupported. Lets the Add-Device dialog pick the right type per provider.
13810
+ */
13811
+ async getManualCreationType() {
13812
+ if (!await this.supportsManualCreation()) return { deviceType: null };
13813
+ return { deviceType: Object.values(DeviceType).find((t) => this.deviceClasses[t] !== void 0) ?? null };
13814
+ }
13775
13815
  async adoptDiscoveredDevice(_input) {
13776
13816
  throw new Error(`${this.providerName} provider does not support discovery-based adoption`);
13777
13817
  }
@@ -14818,83 +14858,34 @@ var GetStateInputSchema = object({
14818
14858
  * HA: entity_id (returns the cached entity state). */
14819
14859
  key: string()
14820
14860
  });
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
- };
14861
+ method(ListInputSchema, array(BrokerInfoSchema$1)), method(GetInputSchema, BrokerInfoSchema$1.nullable()), method(_void(), array(BrokerProviderInfoSchema), { auth: "admin" }), method(AddInputSchema, AddResultSchema, {
14862
+ kind: "mutation",
14863
+ auth: "admin"
14864
+ }), method(RemoveInputSchema, _void(), {
14865
+ kind: "mutation",
14866
+ auth: "admin"
14867
+ }), method(GetInputSchema, TestConnectionResultSchema, {
14868
+ kind: "mutation",
14869
+ auth: "admin"
14870
+ }), method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }), method(object({
14871
+ id: string(),
14872
+ settings: SettingsRecordSchema$1
14873
+ }), _void(), {
14874
+ kind: "mutation",
14875
+ auth: "admin"
14876
+ }), method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }), method(SettingsSchemaInputSchema, SettingsSchemaResultSchema, { auth: "admin" }), method(TestSettingsInputSchema, TestSettingsResultSchema, {
14877
+ kind: "mutation",
14878
+ auth: "admin"
14879
+ }), method(PublishInputSchema, unknown(), {
14880
+ kind: "mutation",
14881
+ auth: "admin"
14882
+ }), method(SubscribeInputSchema, SubscribeResultSchema, {
14883
+ kind: "mutation",
14884
+ auth: "admin"
14885
+ }), method(UnsubscribeInputSchema, _void(), {
14886
+ kind: "mutation",
14887
+ auth: "admin"
14888
+ }), method(GetStateInputSchema, unknown().nullable()), method(_void(), RegistryStatusSchema);
14898
14889
  DeviceType.Camera;
14899
14890
  /**
14900
14891
  * `custom-model-registry` — collection cap exposing operator-registered
@@ -15682,7 +15673,10 @@ method(object({
15682
15673
  }), FieldProbeResultSchema, {
15683
15674
  kind: "mutation",
15684
15675
  auth: "admin"
15685
- }), method(ListCandidatesInputSchema.extend({ addonId: string() }), ListCandidatesOutputSchema, { auth: "admin" }), method(object({
15676
+ }), method(object({
15677
+ addonId: string(),
15678
+ integrationId: string()
15679
+ }), object({ filters: array(AdoptionFilterSchema) }), { auth: "admin" }), method(ListCandidatesInputSchema.extend({ addonId: string() }), ListCandidatesOutputSchema, { auth: "admin" }), method(object({
15686
15680
  addonId: string(),
15687
15681
  integrationId: string()
15688
15682
  }), AdoptionStatusSchema, {
@@ -15697,7 +15691,24 @@ method(object({
15697
15691
  }), method(ResyncInputSchema, ResyncResultSchema, {
15698
15692
  kind: "mutation",
15699
15693
  auth: "admin"
15694
+ }), method(object({}), object({ providers: array(object({
15695
+ addonId: string(),
15696
+ label: string()
15697
+ })).readonly() }), { auth: "admin" }), method(object({}), object({ groups: array(object({
15698
+ addonId: string(),
15699
+ label: string(),
15700
+ candidates: array(DiscoveryCandidateSchema).readonly(),
15701
+ error: string().nullable()
15702
+ })).readonly() }), {
15703
+ kind: "mutation",
15704
+ auth: "admin"
15700
15705
  }), method(object({
15706
+ addonId: string(),
15707
+ params: record(string(), unknown()).optional()
15708
+ }), object({ candidates: array(DiscoveryCandidateSchema).readonly() }), {
15709
+ kind: "mutation",
15710
+ auth: "admin"
15711
+ }), method(object({ addonId: string() }), object({ deviceType: _enum(DeviceType).nullable() }), { auth: "admin" }), method(object({ addonId: string() }), unknown(), { auth: "admin" }), method(object({
15701
15712
  deviceId: number(),
15702
15713
  key: string(),
15703
15714
  value: unknown()
@@ -18366,6 +18377,17 @@ var AvailableIntegrationTypeSchema = object({
18366
18377
  iconUrl: string().nullable(),
18367
18378
  color: string(),
18368
18379
  instanceMode: string(),
18380
+ /**
18381
+ * Integration wizard `mode` (LOCKED MODEL): `standalone` (create
18382
+ * immediately then add devices, no config step/button), `account` (config
18383
+ * step), or `broker` (broker step). Derived server-side by
18384
+ * `getAvailableTypes` when the addon manifest omits an explicit `mode`.
18385
+ */
18386
+ mode: _enum([
18387
+ "standalone",
18388
+ "account",
18389
+ "broker"
18390
+ ]),
18369
18391
  discoveryMode: string(),
18370
18392
  /**
18371
18393
  * Which integration-marker cap the addon declared, so the wizard can
@@ -20826,6 +20848,12 @@ Object.freeze({
20826
20848
  addonId: null,
20827
20849
  access: "create"
20828
20850
  },
20851
+ "deviceManager.adoptionListCandidateFilters": {
20852
+ capName: "device-manager",
20853
+ capScope: "system",
20854
+ addonId: null,
20855
+ access: "view"
20856
+ },
20829
20857
  "deviceManager.adoptionListCandidates": {
20830
20858
  capName: "device-manager",
20831
20859
  capScope: "system",
@@ -20874,12 +20902,30 @@ Object.freeze({
20874
20902
  addonId: null,
20875
20903
  access: "create"
20876
20904
  },
20905
+ "deviceManager.discoverAllProviders": {
20906
+ capName: "device-manager",
20907
+ capScope: "system",
20908
+ addonId: null,
20909
+ access: "create"
20910
+ },
20877
20911
  "deviceManager.discoverDevices": {
20878
20912
  capName: "device-manager",
20879
20913
  capScope: "system",
20880
20914
  addonId: null,
20881
20915
  access: "create"
20882
20916
  },
20917
+ "deviceManager.discoverProvider": {
20918
+ capName: "device-manager",
20919
+ capScope: "system",
20920
+ addonId: null,
20921
+ access: "create"
20922
+ },
20923
+ "deviceManager.discoveryProviders": {
20924
+ capName: "device-manager",
20925
+ capScope: "system",
20926
+ addonId: null,
20927
+ access: "view"
20928
+ },
20883
20929
  "deviceManager.enable": {
20884
20930
  capName: "device-manager",
20885
20931
  capScope: "system",
@@ -21030,6 +21076,18 @@ Object.freeze({
21030
21076
  addonId: null,
21031
21077
  access: "create"
21032
21078
  },
21079
+ "deviceManager.providerCreationType": {
21080
+ capName: "device-manager",
21081
+ capScope: "system",
21082
+ addonId: null,
21083
+ access: "view"
21084
+ },
21085
+ "deviceManager.providerDiscoveryParamsSchema": {
21086
+ capName: "device-manager",
21087
+ capScope: "system",
21088
+ addonId: null,
21089
+ access: "view"
21090
+ },
21033
21091
  "deviceManager.registerDevice": {
21034
21092
  capName: "device-manager",
21035
21093
  capScope: "system",
@@ -21246,6 +21304,18 @@ Object.freeze({
21246
21304
  addonId: null,
21247
21305
  access: "view"
21248
21306
  },
21307
+ "deviceProvider.getDiscoveryParamsSchema": {
21308
+ capName: "device-provider",
21309
+ capScope: "system",
21310
+ addonId: null,
21311
+ access: "view"
21312
+ },
21313
+ "deviceProvider.getManualCreationType": {
21314
+ capName: "device-provider",
21315
+ capScope: "system",
21316
+ addonId: null,
21317
+ access: "view"
21318
+ },
21249
21319
  "deviceProvider.getStatus": {
21250
21320
  capName: "device-provider",
21251
21321
  capScope: "system",
@@ -24084,6 +24154,124 @@ object({
24084
24154
  schemaVersion: literal(1)
24085
24155
  });
24086
24156
  //#endregion
24157
+ //#region src/config.ts
24158
+ /**
24159
+ * The Dreame cloud regions the wrapped `@apocaliss92/nodedreame` client
24160
+ * supports. Mirrors the library's `DreameRegion` union — kept local so the
24161
+ * addon validates the operator-supplied value at the system boundary without
24162
+ * importing a runtime value the library does not export.
24163
+ */
24164
+ var DREAME_REGIONS = [
24165
+ "eu",
24166
+ "us",
24167
+ "cn",
24168
+ "ru",
24169
+ "sg",
24170
+ "in",
24171
+ "de",
24172
+ "tw"
24173
+ ];
24174
+ var DreameRegionSchema = _enum(DREAME_REGIONS);
24175
+ /**
24176
+ * Operator-supplied Dreamehome account settings for ONE broker (= one cloud
24177
+ * account). The Dreame integration is cloud-only (outbound HTTPS + per-device
24178
+ * MQTT push), so the connection is just account credentials plus a region.
24179
+ *
24180
+ * `pollIntervalMs` is the fallback poll cadence the library uses while a
24181
+ * device's MQTT push is down; `0` disables poll fallback.
24182
+ *
24183
+ * The diagnostic catalog (enum/numeric sensors, switches, selects) is seeded
24184
+ * from the cloud SHADOW once at broker start and then tracked by the library's
24185
+ * MQTT push. Live `get_properties` is NOT polled: a docked/asleep robot times it
24186
+ * out (Home Assistant shows the same diagnostics as `unavailable` then), and
24187
+ * hammering it only adds cloud load — so we rely on the shadow seed + push.
24188
+ */
24189
+ var dreameConfigSchema = object({
24190
+ username: string().min(1).describe("Dreamehome account email / username"),
24191
+ password: string().min(1).describe("Dreamehome account password"),
24192
+ region: DreameRegionSchema.default("eu").describe("Dreame cloud region"),
24193
+ 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")
24194
+ });
24195
+ /**
24196
+ * Build the `NodreameOptions` the wrapped client constructor expects from the
24197
+ * validated addon config. Pure: same config in → same options out.
24198
+ */
24199
+ function toNodreameOptions(config) {
24200
+ return {
24201
+ username: config.username,
24202
+ password: config.password,
24203
+ region: config.region,
24204
+ fetchInitialValues: true,
24205
+ pollIntervalMs: config.pollIntervalMs
24206
+ };
24207
+ }
24208
+ /**
24209
+ * Parse an integration's settings into a connection, or null when the mandatory
24210
+ * credentials (username + password) are missing. Used by the boot/lifecycle
24211
+ * reconcile to skip an integration whose account form was not completed.
24212
+ */
24213
+ function connectionFromSettings(settings) {
24214
+ const parsed = dreameConfigSchema.safeParse(settings);
24215
+ return parsed.success ? parsed.data : null;
24216
+ }
24217
+ /**
24218
+ * Hand-written connection form for the account/integration creation UI — the
24219
+ * wizard's `account` config step renders this via `getGlobalSettings`. A flat
24220
+ * set of sections the admin UI renders into the "Add Dreame account" modal.
24221
+ */
24222
+ function buildConnectionFormSchema() {
24223
+ return { sections: [
24224
+ {
24225
+ id: "credentials",
24226
+ title: "Dreamehome account",
24227
+ description: "Sign in with your Dreamehome (Dreame app) account credentials.",
24228
+ columns: 1,
24229
+ fields: [{
24230
+ type: "text",
24231
+ key: "username",
24232
+ label: "Email / username",
24233
+ required: true,
24234
+ placeholder: "you@example.com"
24235
+ }, {
24236
+ type: "password",
24237
+ key: "password",
24238
+ label: "Password",
24239
+ required: true,
24240
+ showToggle: true
24241
+ }]
24242
+ },
24243
+ {
24244
+ id: "region",
24245
+ title: "Region",
24246
+ description: "Select the Dreame cloud region your account is registered in.",
24247
+ columns: 1,
24248
+ fields: [{
24249
+ type: "select",
24250
+ key: "region",
24251
+ label: "Region",
24252
+ default: "eu",
24253
+ options: DREAME_REGIONS.map((r) => ({
24254
+ value: r,
24255
+ label: r.toUpperCase()
24256
+ }))
24257
+ }]
24258
+ },
24259
+ {
24260
+ id: "advanced",
24261
+ title: "Advanced",
24262
+ columns: 1,
24263
+ fields: [{
24264
+ type: "number",
24265
+ key: "pollIntervalMs",
24266
+ label: "Poll fallback interval (ms, 0 = disabled)",
24267
+ min: 0,
24268
+ max: 36e5,
24269
+ default: 3e4
24270
+ }]
24271
+ }
24272
+ ] };
24273
+ }
24274
+ //#endregion
24087
24275
  //#region node_modules/undici/lib/core/symbols.js
24088
24276
  var require_symbols$4 = /* @__PURE__ */ __commonJSMin(((exports, module) => {
24089
24277
  module.exports = {
@@ -74386,134 +74574,6 @@ objectType({
74386
74574
  })
74387
74575
  });
74388
74576
  //#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
74577
  //#region src/dreame-entity-catalog.ts
74518
74578
  var STATS = "Statistics";
74519
74579
  var MAINT = "Maintenance";
@@ -75160,6 +75220,34 @@ function enumOptions(e) {
75160
75220
  label: humanizeEnumName(name)
75161
75221
  }));
75162
75222
  }
75223
+ /**
75224
+ * MiotError codes that are BENIGN completion / reminder states, not active
75225
+ * faults. HA does not surface these as errors — a fresh cloud-shadow read of an
75226
+ * idle docked robot legitimately returns `TaskComplete` (68). We relabel them to
75227
+ * the `Clear` label so the Error sensor only shows real faults (HA parity).
75228
+ * Extend this set if other non-fault codes surface as spurious "errors".
75229
+ */
75230
+ var BENIGN_ERROR_CODES = new Set([MiotError.TaskComplete]);
75231
+ /** `MiotError` options with the benign completion codes remapped to the `Clear`
75232
+ * label, so the Error sensor reads as no-error for them (matches HA). */
75233
+ function errorEnumOptions() {
75234
+ const clearLabel = humanizeEnumName(MiotError[MiotError.Clear]);
75235
+ return enumOptions(MiotError).map((o) => BENIGN_ERROR_CODES.has(o.value) ? {
75236
+ ...o,
75237
+ label: clearLabel
75238
+ } : o);
75239
+ }
75240
+ /**
75241
+ * Resolve the CURRENT catalog options for an enum-sensor by its MIoT coordinates
75242
+ * (across both the vacuum and mower enum-sensor catalogs). The enum-sensor child
75243
+ * persists its option map at adoption; preferring the live catalog here lets a
75244
+ * label change (e.g. the benign-error remap) take effect for already-adopted
75245
+ * children on the next boot — without a re-adopt. Null when no catalog entry
75246
+ * matches (persisted config is then the only source).
75247
+ */
75248
+ function enumSensorOptionsFor(siid, piid) {
75249
+ return [...DREAME_ENUM_SENSORS, ...DREAME_MOWER_ENUM_SENSORS].find((es) => es.siid === siid && es.piid === piid)?.options ?? null;
75250
+ }
75163
75251
  /** Humanise a Tasshack SNAKE_CASE enum name → spaced Title Case
75164
75252
  * ('BACK_HOME' → 'Back Home'). */
75165
75253
  function humanizeSnake(name) {
@@ -75285,7 +75373,7 @@ var DREAME_ENUM_SENSORS = [
75285
75373
  label: "Error",
75286
75374
  siid: 2,
75287
75375
  piid: 2,
75288
- options: enumOptions(MiotError),
75376
+ options: errorEnumOptions(),
75289
75377
  section: DIAG
75290
75378
  },
75291
75379
  {
@@ -76100,35 +76188,51 @@ var DreameFacadeResolver = class {
76100
76188
  * and device instances. */
76101
76189
  var dreameFacades = new DreameFacadeResolver();
76102
76190
  //#endregion
76103
- //#region src/dreame-broker-registry.ts
76191
+ //#region src/dreame-client-registry.ts
76192
+ /**
76193
+ * Per-integration Dreame connection registry (account mode).
76194
+ *
76195
+ * Under the LOCKED integration/adoption model (design §7.1), Dreame is a
76196
+ * `mode: account` addon: each Dreame integration carries its own Dreamehome
76197
+ * account credentials in its `integration.settings`, and this registry holds one
76198
+ * {@link DreameIntegrationManager} per `integrationId`
76199
+ * (`Map<integrationId, DreameIntegrationManager>`). Multi-account = multiple
76200
+ * integrations; there is NO shared broker.
76201
+ *
76202
+ * The live `Nodreame` facade each manager owns is published on the in-process
76203
+ * {@link dreameFacades} resolver KEYED BY `integrationId`, so the device classes
76204
+ * resolve their live handle by the `brokerId` field they persist — which now
76205
+ * holds the integrationId (unchanged device-side code, see the addon migration
76206
+ * note). MQTT session/socket lifecycle stays entirely inside the manager +
76207
+ * library (no reintroduced socket-leak): the registry only starts/stops managers.
76208
+ */
76104
76209
  /**
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`.
76210
+ * Owns the `Map<integrationId, DreameIntegrationManager>`. Callers reconcile the
76211
+ * map against the live integration list on boot + on every integration lifecycle
76212
+ * event; device classes + the adoption provider resolve their facade by
76213
+ * `integrationId` through {@link dreameFacades}.
76214
+ *
76215
+ * Structural twin of `WyzeClientRegistry` — `upsert` is session-preserving when
76216
+ * credentials are unchanged, `retain` drops vanished integrations.
76109
76217
  */
76110
- var DreameBrokerRegistry = class {
76218
+ var DreameClientRegistry = class {
76111
76219
  #logger;
76112
- #onBrokerConnected;
76113
- #onBrokerDisconnected;
76220
+ #onConnected;
76221
+ #onDisconnected;
76114
76222
  #makeManager;
76115
- #managers = /* @__PURE__ */ new Map();
76116
- #facades = /* @__PURE__ */ new Map();
76117
- #integrationToBroker = /* @__PURE__ */ new Map();
76118
- #nextId = 1;
76119
76223
  #useDefaultFactory;
76224
+ #entries = /* @__PURE__ */ new Map();
76120
76225
  constructor(logger, deps = {}) {
76121
76226
  this.#logger = logger;
76122
- this.#onBrokerConnected = deps.onBrokerConnected ?? (() => void 0);
76123
- this.#onBrokerDisconnected = deps.onBrokerDisconnected ?? (() => void 0);
76227
+ this.#onConnected = deps.onConnected ?? (() => void 0);
76228
+ this.#onDisconnected = deps.onDisconnected ?? (() => void 0);
76124
76229
  this.#useDefaultFactory = deps.makeManager === void 0;
76125
76230
  this.#makeManager = deps.makeManager ?? ((opts) => {
76126
76231
  const raw = new Nodreame(toNodreameOptions(opts.connection));
76127
- this.#facades.set(opts.id, raw);
76128
- dreameFacades.set(opts.id, raw);
76232
+ dreameFacades.set(opts.integrationId, raw);
76129
76233
  const wrapped = raw;
76130
76234
  return new DreameIntegrationManager({
76131
- id: opts.id,
76235
+ id: opts.integrationId,
76132
76236
  name: opts.name,
76133
76237
  connection: opts.connection,
76134
76238
  logger: opts.logger,
@@ -76138,270 +76242,124 @@ var DreameBrokerRegistry = class {
76138
76242
  });
76139
76243
  });
76140
76244
  }
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
- }
76245
+ setOnConnected(cb) {
76246
+ this.#onConnected = cb;
76152
76247
  }
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) => {
76248
+ setOnDisconnected(cb) {
76249
+ this.#onDisconnected = cb;
76250
+ }
76251
+ /**
76252
+ * Ensure a manager exists for `integrationId` with the given credentials.
76253
+ * Idempotent: an existing entry with identical credentials is preserved
76254
+ * (keeps its live MQTT session); a credentials change re-applies the
76255
+ * connection atomically (stop + start). Best-effort start — a failed login
76256
+ * leaves the entry registered so a later reconcile / lifecycle event retries.
76257
+ */
76258
+ async upsert(integrationId, name, connection) {
76259
+ const existing = this.#entries.get(integrationId);
76260
+ if (existing) {
76261
+ if (sameConnection(existing.connection, connection)) return;
76157
76262
  try {
76158
- await this.#managers.get(id)?.stop();
76263
+ if (this.#useDefaultFactory) {
76264
+ const raw = new Nodreame(toNodreameOptions(connection));
76265
+ dreameFacades.set(integrationId, raw);
76266
+ }
76267
+ await existing.manager.applyConnection(connection);
76268
+ this.#entries.set(integrationId, {
76269
+ manager: existing.manager,
76270
+ connection
76271
+ });
76159
76272
  } catch (err) {
76160
- this.#logger.warn("DreameBrokerRegistry: shutdown stop failed", {
76161
- tags: { brokerId: id },
76273
+ this.#logger.warn("DreameClientRegistry: applyConnection failed", {
76274
+ tags: { integrationId },
76162
76275
  meta: { error: errMsg(err) }
76163
76276
  });
76164
76277
  }
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,
76278
+ return;
76279
+ }
76280
+ const manager = this.#makeManager({
76281
+ integrationId,
76181
76282
  name,
76182
76283
  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);
76284
+ logger: this.#logger,
76285
+ onConnected: (id) => this.#onConnected(id),
76286
+ onDisconnected: (id) => this.#onDisconnected(id)
76287
+ });
76288
+ this.#entries.set(integrationId, {
76289
+ manager,
76290
+ connection
76291
+ });
76196
76292
  try {
76197
- await mgr.stop();
76293
+ await manager.start();
76198
76294
  } catch (err) {
76199
- this.#logger.warn("DreameBrokerRegistry: removeEntry stop failed", {
76200
- tags: { brokerId: id },
76295
+ this.#logger.warn("DreameClientRegistry: manager start failed", {
76296
+ tags: { integrationId },
76201
76297
  meta: { error: errMsg(err) }
76202
76298
  });
76203
76299
  }
76204
76300
  }
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);
76301
+ /** Stop + drop the manager for an integration that no longer exists. */
76302
+ async remove(integrationId) {
76303
+ const entry = this.#entries.get(integrationId);
76304
+ if (!entry) return;
76305
+ this.#entries.delete(integrationId);
76306
+ dreameFacades.set(integrationId, null);
76307
+ try {
76308
+ await entry.manager.stop();
76309
+ } catch (err) {
76310
+ this.#logger.warn("DreameClientRegistry: remove stop failed", {
76311
+ tags: { integrationId },
76312
+ meta: { error: errMsg(err) }
76313
+ });
76212
76314
  }
76213
- await mgr.applyConnection(connection);
76214
- return {
76215
- id,
76216
- name: mgr.getInfo().name,
76217
- connection
76218
- };
76219
76315
  }
76220
- linkIntegration(integrationId, brokerId) {
76221
- this.#integrationToBroker.set(integrationId, brokerId);
76316
+ /** Stop + drop every manager whose integrationId is not in `keep`. */
76317
+ async retain(keep) {
76318
+ const stale = [...this.#entries.keys()].filter((id) => !keep.has(id));
76319
+ for (const id of stale) await this.remove(id);
76222
76320
  }
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;
76321
+ /** Stop every manager and clear all state (full shutdown). */
76322
+ async shutdown() {
76323
+ const ids = [...this.#entries.keys()];
76324
+ await Promise.all(ids.map(async (id) => {
76325
+ try {
76326
+ await this.#entries.get(id)?.manager.stop();
76327
+ } catch (err) {
76328
+ this.#logger.warn("DreameClientRegistry: shutdown stop failed", {
76329
+ tags: { integrationId: id },
76330
+ meta: { error: errMsg(err) }
76331
+ });
76332
+ }
76333
+ }));
76334
+ this.#entries.clear();
76335
+ dreameFacades.clear();
76227
76336
  }
76228
- getFacade(id) {
76229
- return this.#facades.get(id) ?? null;
76337
+ /** True when a manager is registered for the integration. */
76338
+ has(integrationId) {
76339
+ return this.#entries.has(integrationId);
76230
76340
  }
76341
+ /** The registered integration ids (one per account). */
76231
76342
  list() {
76232
- return Array.from(this.#managers.values()).map((m) => m.getInfo());
76343
+ return [...this.#entries.keys()];
76233
76344
  }
76234
- get(id) {
76235
- return this.#managers.get(id)?.getInfo() ?? null;
76345
+ /** The current connection an integration's manager is configured with. */
76346
+ getConnection(integrationId) {
76347
+ return this.#entries.get(integrationId)?.connection ?? null;
76236
76348
  }
76237
- getConnection(id) {
76238
- return this.#managers.get(id)?.getConnection() ?? null;
76349
+ /** Immutable status snapshot for an integration's manager. */
76350
+ getInfo(integrationId) {
76351
+ return this.#entries.get(integrationId)?.manager.getInfo() ?? null;
76239
76352
  }
76240
76353
  size() {
76241
- return this.#managers.size;
76354
+ return this.#entries.size;
76242
76355
  }
76243
76356
  connectedCount() {
76244
76357
  let count = 0;
76245
- for (const mgr of this.#managers.values()) if (mgr.getInfo().status === "connected") count++;
76358
+ for (const entry of this.#entries.values()) if (entry.manager.getInfo().status === "connected") count++;
76246
76359
  return count;
76247
76360
  }
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
76361
  };
76277
76362
  //#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
76363
  //#region src/dreame-mapping.ts
76406
76364
  /** Classify a Dreame cloud `model` string into a supported device kind. Pure. */
76407
76365
  function classifyDreameModel(model) {
@@ -76454,11 +76412,11 @@ var DEVICES_FILTER = {
76454
76412
  label: "Devices",
76455
76413
  isDefault: true
76456
76414
  };
76457
- /** Build a `dreameDeviceId → CamStack deviceId` map for a single broker. */
76458
- async function adoptedMapForBroker(brokerId, listAdoptedDreame) {
76415
+ /** Build a `dreameDeviceId → CamStack deviceId` map for a single integration. */
76416
+ async function adoptedMapForIntegration(integrationId, listAdoptedDreame) {
76459
76417
  const all = await listAdoptedDreame();
76460
76418
  const map = /* @__PURE__ */ new Map();
76461
- for (const device of all) if (device.config["system"] === "dreame" && device.config["brokerId"] === brokerId) {
76419
+ for (const device of all) if (device.config["system"] === "dreame" && device.config["brokerId"] === integrationId) {
76462
76420
  const dreameId = device.config["dreameDeviceId"];
76463
76421
  if (typeof dreameId === "string") map.set(dreameId, device.id);
76464
76422
  }
@@ -76466,15 +76424,15 @@ async function adoptedMapForBroker(brokerId, listAdoptedDreame) {
76466
76424
  }
76467
76425
  /**
76468
76426
  * 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).
76427
+ * builder: all side-effecting deps are injected. Single `devices` granularity
76428
+ * (one Container per cloud device); resolves each account by `integrationId`.
76471
76429
  */
76472
76430
  function buildDreameAdoptionProvider(deps) {
76473
- const { registry, getBrokerIdForIntegration, listAdoptedDreame, adoptDevice, removeDevice, findDeviceConfig, logger } = deps;
76474
- async function allCandidatesForBroker(brokerId) {
76431
+ const { getFacade, hasIntegration, listIntegrations, listAdoptedDreame, adoptDevice, removeDevice, findDeviceConfig, logger } = deps;
76432
+ async function allCandidatesForIntegration(integrationId) {
76475
76433
  return buildDreameCandidates({
76476
- devices: registry.getFacade(brokerId)?.devices ?? [],
76477
- adopted: await adoptedMapForBroker(brokerId, listAdoptedDreame)
76434
+ devices: getFacade(integrationId)?.devices ?? [],
76435
+ adopted: await adoptedMapForIntegration(integrationId, listAdoptedDreame)
76478
76436
  });
76479
76437
  }
76480
76438
  function applyCandidateTextFilter(cands, filterText) {
@@ -76492,7 +76450,7 @@ function buildDreameAdoptionProvider(deps) {
76492
76450
  return {
76493
76451
  listCandidateFilters: async () => ({ filters: [DEVICES_FILTER] }),
76494
76452
  listCandidates: async ({ integrationId, page, pageSize, filterText }) => {
76495
- const filtered = applyCandidateTextFilter(await allCandidatesForBroker(await getBrokerIdForIntegration(integrationId)), filterText);
76453
+ const filtered = applyCandidateTextFilter(await allCandidatesForIntegration(integrationId), filterText);
76496
76454
  const start = (page - 1) * pageSize;
76497
76455
  return {
76498
76456
  candidates: filtered.slice(start, start + pageSize),
@@ -76502,13 +76460,12 @@ function buildDreameAdoptionProvider(deps) {
76502
76460
  };
76503
76461
  },
76504
76462
  getCandidate: async ({ integrationId, childNativeId }) => {
76505
- return (await allCandidatesForBroker(await getBrokerIdForIntegration(integrationId))).find((c) => c.childNativeId === childNativeId) ?? null;
76463
+ return (await allCandidatesForIntegration(integrationId)).find((c) => c.childNativeId === childNativeId) ?? null;
76506
76464
  },
76507
76465
  getStatus: async () => {
76508
76466
  try {
76509
- const brokers = registry.list();
76510
76467
  let candidateCount = 0;
76511
- for (const broker of brokers) candidateCount += (await allCandidatesForBroker(broker.id)).length;
76468
+ for (const integrationId of listIntegrations()) candidateCount += (await allCandidatesForIntegration(integrationId)).length;
76512
76469
  const adoptedCount = (await listAdoptedDreame()).filter((d) => d.config["system"] === "dreame").length;
76513
76470
  return {
76514
76471
  lastDiscoveryAt: Date.now(),
@@ -76527,9 +76484,8 @@ function buildDreameAdoptionProvider(deps) {
76527
76484
  }
76528
76485
  },
76529
76486
  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;
76487
+ const candidateCount = (await allCandidatesForIntegration(integrationId)).length;
76488
+ const adoptedCount = (await adoptedMapForIntegration(integrationId, listAdoptedDreame)).size;
76533
76489
  return {
76534
76490
  lastDiscoveryAt: Date.now(),
76535
76491
  candidateCount,
@@ -76538,18 +76494,17 @@ function buildDreameAdoptionProvider(deps) {
76538
76494
  };
76539
76495
  },
76540
76496
  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`);
76497
+ const facade = getFacade(integrationId);
76498
+ if (facade === null) throw new Error(`dreame adopt: integration ${integrationId} not connected`);
76544
76499
  const devices = facade.devices;
76545
76500
  const adopted = [];
76546
76501
  let failures = 0;
76547
76502
  for (const dreameDeviceId of childNativeIds) try {
76548
76503
  const dev = devices.find((d) => d.deviceId === dreameDeviceId);
76549
76504
  if (dev === void 0) {
76550
- logger.warn("dreame adopt: device not found on broker — skipping", { meta: {
76505
+ logger.warn("dreame adopt: device not found on integration — skipping", { meta: {
76551
76506
  dreameDeviceId,
76552
- brokerId
76507
+ integrationId
76553
76508
  } });
76554
76509
  failures++;
76555
76510
  continue;
@@ -76561,7 +76516,7 @@ function buildDreameAdoptionProvider(deps) {
76561
76516
  if (candidate === void 0) {
76562
76517
  logger.warn("dreame adopt: device model unsupported — skipping", { meta: {
76563
76518
  dreameDeviceId,
76564
- brokerId,
76519
+ integrationId,
76565
76520
  model: dev.model
76566
76521
  } });
76567
76522
  failures++;
@@ -76570,7 +76525,6 @@ function buildDreameAdoptionProvider(deps) {
76570
76525
  const name = perCandidate?.[dreameDeviceId]?.name ?? dev.name ?? dreameDeviceId;
76571
76526
  const { deviceId, accessoryDeviceIds } = await adoptDevice({
76572
76527
  dreameDeviceId,
76573
- brokerId,
76574
76528
  integrationId,
76575
76529
  type: candidate.type,
76576
76530
  name,
@@ -76584,7 +76538,7 @@ function buildDreameAdoptionProvider(deps) {
76584
76538
  } catch (err) {
76585
76539
  logger.warn("dreame adopt: failed to adopt device", { meta: {
76586
76540
  dreameDeviceId,
76587
- brokerId,
76541
+ integrationId,
76588
76542
  error: errMsg(err)
76589
76543
  } });
76590
76544
  failures++;
@@ -76599,9 +76553,10 @@ function buildDreameAdoptionProvider(deps) {
76599
76553
  const cfg = await findDeviceConfig(camDeviceId);
76600
76554
  if (cfg === null) throw new Error(`dreame resync: device ${camDeviceId} not found`);
76601
76555
  if (cfg["system"] !== "dreame") throw new Error(`dreame resync: device ${camDeviceId} is not a Dreame device`);
76602
- const brokerId = String(cfg["brokerId"]);
76556
+ const integrationId = String(cfg["brokerId"]);
76603
76557
  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}`);
76558
+ if (!hasIntegration(integrationId)) throw new Error(`dreame resync: integration ${integrationId} not connected`);
76559
+ if (!(getFacade(integrationId)?.devices.some((d) => d.deviceId === dreameDeviceId) ?? false)) throw new Error(`dreame resync: device ${dreameDeviceId} no longer present on integration ${integrationId}`);
76605
76560
  return {
76606
76561
  changed: false,
76607
76562
  rebuiltChildren: 0
@@ -76610,29 +76565,31 @@ function buildDreameAdoptionProvider(deps) {
76610
76565
  };
76611
76566
  }
76612
76567
  //#endregion
76613
- //#region src/dreame-broker-device-cascade.ts
76568
+ //#region src/dreame-integration-device-cascade.ts
76614
76569
  /**
76615
76570
  * 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.
76571
+ * whose persisted config carries `{ system: 'dreame', brokerId: integrationId }`.
76572
+ * Used when an integration is deleted. Best-effort per device; returns the count
76573
+ * removed.
76574
+ *
76575
+ * NOTE: the persisted config field is still named `brokerId` (unchanged
76576
+ * device-side schema) but under the account model it holds the `integrationId`.
76620
76577
  */
76621
- async function cascadeRemoveDevicesForBroker(input) {
76622
- const { reg, devices, addonId, brokerId, logger } = input;
76578
+ async function cascadeRemoveDevicesForIntegration(input) {
76579
+ const { reg, devices, addonId, integrationId, logger } = input;
76623
76580
  let removed = 0;
76624
76581
  for (const d of reg.getAllForAddon(addonId)) {
76625
76582
  if (d.parentDeviceId !== null) continue;
76626
76583
  const cfg = await devices.loadConfig(d.id).catch(() => ({}));
76627
- if (cfg["system"] !== "dreame" || cfg["brokerId"] !== brokerId) continue;
76584
+ if (cfg["system"] !== "dreame" || cfg["brokerId"] !== integrationId) continue;
76628
76585
  try {
76629
76586
  await devices.remove(d.id);
76630
76587
  removed += 1;
76631
76588
  } catch (err) {
76632
- logger.warn("dreame: broker cascade-remove failed", {
76589
+ logger.warn("dreame: integration cascade-remove failed", {
76633
76590
  tags: { deviceId: d.id },
76634
76591
  meta: {
76635
- brokerId,
76592
+ integrationId,
76636
76593
  error: errMsg(err)
76637
76594
  }
76638
76595
  });
@@ -76641,20 +76598,22 @@ async function cascadeRemoveDevicesForBroker(input) {
76641
76598
  return removed;
76642
76599
  }
76643
76600
  //#endregion
76644
- //#region src/dreame-broker-offline.ts
76601
+ //#region src/dreame-integration-offline.ts
76645
76602
  /**
76646
- * Set every Dreame device belonging to `brokerId` to the requested online
76603
+ * Set every Dreame device belonging to `integrationId` to the requested online
76647
76604
  * state. `devices` is the addon-scoped device list; every Dreame top-level
76648
- * Container carries `{ system: 'dreame', brokerId }` in its config blob.
76605
+ * Container carries `{ system: 'dreame', brokerId: integrationId }` in its
76606
+ * config blob (the `brokerId` field name is unchanged; it now holds the
76607
+ * integrationId).
76649
76608
  *
76650
76609
  * Churn-free: skips any device already in the target state. Returns the count
76651
- * of devices actually transitioned. Mirrors the Homematic offline helper.
76610
+ * of devices actually transitioned.
76652
76611
  */
76653
- function setBrokerDevicesOnline(devices, brokerId, online) {
76612
+ function setIntegrationDevicesOnline(devices, integrationId, online) {
76654
76613
  let count = 0;
76655
76614
  for (const dev of devices) {
76656
76615
  if (dev.config.get("system") !== "dreame") continue;
76657
- if (dev.config.get("brokerId") !== brokerId) continue;
76616
+ if (dev.config.get("brokerId") !== integrationId) continue;
76658
76617
  if (dev.online === online) continue;
76659
76618
  dev.markOnline(online);
76660
76619
  count += 1;
@@ -77141,7 +77100,7 @@ var DreameEnumSensorDevice = class extends DreameChildDevice {
77141
77100
  const cfg = dreameEnumSensorSchema.parse(ctx.persistedConfig ?? {});
77142
77101
  this.#siid = cfg.siid;
77143
77102
  this.#piid = cfg.piid;
77144
- this.#options = cfg.options;
77103
+ this.#options = enumSensorOptionsFor(cfg.siid, cfg.piid) ?? cfg.options;
77145
77104
  this.updateSourceInfo({
77146
77105
  id: `${this.dreameDeviceId}:enum:${cfg.siid}-${cfg.piid}`,
77147
77106
  system: "dreame"
@@ -77391,6 +77350,15 @@ var CONSUMABLES_CAP_NAME$1 = "consumables";
77391
77350
  var CONSUMABLES_WAKE_RETRIES = 2;
77392
77351
  /** Spacing between wake-window retries. */
77393
77352
  var CONSUMABLES_RETRY_DELAY_MS = 1500;
77353
+ /**
77354
+ * Cloud-shadow (no-wake) refresh cadence for the mower's battery / charging /
77355
+ * status props. The shadow read (`refreshFromCache` → `getCachedProperties`)
77356
+ * hits the cloud SHADOW, not the device — so it succeeds even while the mower
77357
+ * sleeps at base, UNLIKE the synchronous CMS action above (which 80001s). The
77358
+ * mower never PUSHES battery over MQTT, so without this poll `batteryLevel`
77359
+ * stays null. A gentle interval keeps the level current as it charges.
77360
+ */
77361
+ var SHADOW_POLL_MS = 12e4;
77394
77362
  var COLD_START = {
77395
77363
  activity: "idle",
77396
77364
  batteryLevel: null,
@@ -77430,6 +77398,8 @@ var DreameMowerDevice = class extends DreameChildDevice {
77430
77398
  #prevStatus = null;
77431
77399
  /** Last seen `chargingStatus` value — second awake-and-charging trigger. */
77432
77400
  #prevCharging = null;
77401
+ /** Periodic cloud-shadow refresh timer (battery / charging / status). */
77402
+ #shadowTimer = null;
77433
77403
  constructor(ctx) {
77434
77404
  super(ctx);
77435
77405
  }
@@ -77465,6 +77435,35 @@ var DreameMowerDevice = class extends DreameChildDevice {
77465
77435
  async onActivate() {
77466
77436
  await super.onActivate();
77467
77437
  this.refreshConsumables(CONSUMABLES_WAKE_RETRIES);
77438
+ this.#refreshShadow();
77439
+ this.#shadowTimer = setInterval(() => {
77440
+ this.#refreshShadow();
77441
+ }, SHADOW_POLL_MS);
77442
+ }
77443
+ async removeDevice() {
77444
+ if (this.#shadowTimer) {
77445
+ clearInterval(this.#shadowTimer);
77446
+ this.#shadowTimer = null;
77447
+ }
77448
+ await super.removeDevice();
77449
+ }
77450
+ /**
77451
+ * Seed the cached MIoT props (battery / charging / status …) from the CLOUD
77452
+ * SHADOW — a no-wake read that succeeds even while the mower sleeps at base.
77453
+ * `refreshFromCache` emits `stateChanged`, which drives `recomputeSlice`, so
77454
+ * the battery level lands in the slice. Best-effort; failures log at debug.
77455
+ */
77456
+ async #refreshShadow() {
77457
+ const mower = this.resolveMower();
77458
+ if (mower === null) return;
77459
+ try {
77460
+ await mower.refreshFromCache();
77461
+ } catch (err) {
77462
+ this.ctx.logger.debug("dreame mower: cloud-shadow refresh failed", { meta: {
77463
+ dreameDeviceId: this.dreameDeviceId,
77464
+ error: errMsg(err)
77465
+ } });
77466
+ }
77468
77467
  }
77469
77468
  recomputeSlice() {
77470
77469
  const mower = this.resolveMower();
@@ -79433,101 +79432,53 @@ var DreameContainerDevice = class extends BaseDevice$1 {
79433
79432
  };
79434
79433
  //#endregion
79435
79434
  //#region src/addon.ts
79436
- /** Default multi-broker config — a fresh install starts with no accounts. */
79437
- var DEFAULTS = { brokers: [] };
79438
79435
  /**
79439
- * Dreame device-provider addon (multi-account).
79436
+ * Dreame device-provider addon — `mode: account` (multi-account, broker-less).
79437
+ *
79438
+ * Wraps the `@apocaliss92/nodedreame` Dreamehome cloud client. Each Dreame
79439
+ * integration carries its own account credentials in its `integration.settings`;
79440
+ * the addon holds one {@link DreameIntegrationManager} per `integrationId` in
79441
+ * {@link DreameClientRegistry} (`Map<integrationId, manager>`). There is NO
79442
+ * shared broker. The live `Nodreame` facade each manager owns is published on
79443
+ * the in-process `dreameFacades` resolver KEYED BY `integrationId`.
79440
79444
  *
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.
79445
+ * A `device-adoption` cap provider enumerates each account's cloud devices and
79446
+ * adopts them; each adopted cloud device becomes a {@link DeviceType.Container}
79447
+ * parent that fans out a single typed accessory child (vacuum or mower) and its
79448
+ * entity children. The device classes resolve their live handle by the
79449
+ * `brokerId` config field — UNCHANGED device-side code; that field now holds the
79450
+ * integrationId (see the migration note in the addon docs).
79451
+ *
79452
+ * `hub-only`: the map data-plane binds 127.0.0.1.
79448
79453
  */
79449
79454
  var DreameProviderAddon = class extends BaseDeviceProvider {
79450
79455
  addonId = "provider-dreame";
79451
79456
  providerName = "Dreame";
79452
79457
  deviceClasses = { [DeviceType.Container]: DreameContainerDevice };
79453
- registry = null;
79458
+ clients = null;
79454
79459
  /** Teardown for the `/map` data-plane listener (vacuum-map PNG serving). */
79455
79460
  mapDataPlaneDispose = null;
79456
79461
  constructor() {
79457
- super({ ...DEFAULTS });
79462
+ super({});
79458
79463
  }
79459
79464
  async onInitialize() {
79460
79465
  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);
79466
+ this.clients = new DreameClientRegistry(this.ctx.logger);
79467
+ this.clients.setOnConnected((integrationId) => {
79468
+ this.ctx.logger.info("Dreame: integration connected", { meta: { integrationId } });
79469
+ this.setIntegrationDevicesOnline(integrationId, true);
79465
79470
  });
79466
- this.registry.setOnBrokerDisconnected((brokerId) => {
79467
- this.setBrokerDevicesOnline(brokerId, false);
79471
+ this.clients.setOnDisconnected((integrationId) => {
79472
+ this.setIntegrationDevicesOnline(integrationId, false);
79468
79473
  });
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();
79474
+ await this.reconcileIntegrations();
79472
79475
  this.subscribeIntegrationLifecycle();
79473
79476
  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
- }
79477
+ this.ctx.logger.info("Dreame: provider initialised", { meta: { integrationCount: this.requireClients().size() } });
79478
+ return [...regs, {
79479
+ capability: deviceAdoptionCapability,
79480
+ provider: this.buildAdoptionProvider()
79481
+ }];
79531
79482
  }
79532
79483
  async onShutdown() {
79533
79484
  try {
@@ -79538,13 +79489,17 @@ var DreameProviderAddon = class extends BaseDeviceProvider {
79538
79489
  this.mapDataPlaneDispose = null;
79539
79490
  setMapBaseUrl(null);
79540
79491
  try {
79541
- await this.registry?.shutdown();
79492
+ await this.clients?.shutdown();
79542
79493
  } catch (err) {
79543
79494
  this.ctx.logger.warn("Dreame: provider shutdown error", { meta: { error: errMsg(err) } });
79544
79495
  }
79545
- this.registry = null;
79496
+ this.clients = null;
79546
79497
  await super.onShutdown();
79547
79498
  }
79499
+ requireClients() {
79500
+ if (!this.clients) throw new Error("Dreame provider not initialised");
79501
+ return this.clients;
79502
+ }
79548
79503
  /**
79549
79504
  * Register the `/map` data-plane route that streams rendered vacuum-map PNGs
79550
79505
  * to the browser (`<img>` loads `/addon/<addonId>/map?did=…&t=…`). The map
@@ -79575,93 +79530,76 @@ var DreameProviderAddon = class extends BaseDeviceProvider {
79575
79530
  this.ctx.logger.warn("Dreame: map data-plane setup failed", { meta: { error: errMsg(err) } });
79576
79531
  }
79577
79532
  }
79578
- /** Stable id, broker-scoped so two accounts exposing the same device id don't
79579
- * collide (mirrors the HA/Homematic broker-scoping). */
79533
+ /**
79534
+ * Stable id, integration-scoped so two accounts exposing the same cloud device
79535
+ * id don't collide. `brokerId` in the config is the integrationId (unchanged
79536
+ * field name).
79537
+ */
79580
79538
  generateStableId(_type, config) {
79581
79539
  return `dreame:${String(config?.["brokerId"] ?? "unknown")}:${String(config?.["dreameDeviceId"] ?? Date.now())}`;
79582
79540
  }
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
79541
  /**
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.
79542
+ * Rebuild the `Map<integrationId, manager>` from the live integration list:
79543
+ * for each surviving Dreame integration read its settings and upsert a manager
79544
+ * (session-preserving if credentials are unchanged); drop managers whose
79545
+ * integration was deleted/disabled cascade-removing their adopted devices.
79546
+ * Idempotent; runs on boot + on every integration lifecycle event. Guarded so
79547
+ * a failure never fails init.
79611
79548
  */
79612
- async reconcileIntegrationsToBrokers() {
79549
+ async reconcileIntegrations() {
79550
+ const reg = this.requireClients();
79613
79551
  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
- } });
79552
+ const mine = (await this.ctx.api.integrations.list.query()).filter((i) => i.addonId === this.ctx.id && i.enabled);
79553
+ const surviving = /* @__PURE__ */ new Set();
79554
+ for (const integration of mine) try {
79555
+ const connection = connectionFromSettings(await this.ctx.api.integrations.getSettings.query({ id: integration.id }));
79556
+ if (!connection) {
79557
+ this.ctx.logger.warn("Dreame integration has no complete credentials — skipping", { meta: { integrationId: integration.id } });
79625
79558
  continue;
79626
79559
  }
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);
79560
+ await reg.upsert(integration.id, integration.name, connection);
79561
+ surviving.add(integration.id);
79634
79562
  } catch (err) {
79635
- this.ctx.logger.warn("Dreame integration→broker: broker cleanup failed", { meta: {
79636
- brokerId: id,
79563
+ this.ctx.logger.warn("Dreame reconcile: failed to read integration settings", { meta: {
79564
+ integrationId: integration.id,
79637
79565
  error: errMsg(err)
79638
79566
  } });
79639
79567
  }
79640
- const nextBrokers = this.config.brokers.filter((b) => !toRemove.includes(b.id));
79641
- await this.updateGlobalSettings({ brokers: nextBrokers });
79568
+ const vanished = reg.list().filter((id) => !surviving.has(id));
79569
+ await reg.retain(surviving);
79570
+ for (const integrationId of vanished) await this.cascadeRemoveDevicesForIntegration(integrationId);
79642
79571
  } catch (err) {
79643
- this.ctx.logger.warn("Dreame integration→broker reconcile failed", { meta: { error: errMsg(err) } });
79572
+ this.ctx.logger.warn("Dreame integration reconcile failed", { meta: { error: errMsg(err) } });
79644
79573
  }
79645
79574
  }
79646
79575
  subscribeIntegrationLifecycle() {
79647
79576
  const handler = (event) => {
79648
79577
  const addonId = event.data["addonId"];
79649
79578
  if (typeof addonId === "string" && addonId !== this.ctx.id) return;
79650
- this.reconcileIntegrationsToBrokers();
79579
+ this.reconcileIntegrations();
79651
79580
  };
79652
79581
  this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationEnabled }, handler);
79653
79582
  this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDisabled }, handler);
79654
79583
  this.ctx.eventBus.subscribe({ category: EventCategory.IntegrationDeleted }, handler);
79655
79584
  }
79585
+ async cascadeRemoveDevicesForIntegration(integrationId) {
79586
+ const reg = this.ctx.kernel.deviceRegistry;
79587
+ const devices = this.ctx.kernel.devices;
79588
+ if (!reg || !devices) return;
79589
+ await cascadeRemoveDevicesForIntegration({
79590
+ reg,
79591
+ devices,
79592
+ addonId: this.addonId,
79593
+ integrationId,
79594
+ logger: this.ctx.logger
79595
+ });
79596
+ }
79656
79597
  buildAdoptionProvider() {
79657
79598
  return buildDreameAdoptionProvider({
79658
- registry: this.requireRegistry(),
79659
79599
  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
- },
79600
+ getFacade: (integrationId) => dreameFacades.get(integrationId),
79601
+ hasIntegration: (integrationId) => this.requireClients().has(integrationId),
79602
+ listIntegrations: () => this.requireClients().list(),
79665
79603
  listAdoptedDreame: async () => {
79666
79604
  const reg = this.ctx.kernel.deviceRegistry;
79667
79605
  const devices = this.ctx.kernel.devices;
@@ -79677,12 +79615,12 @@ var DreameProviderAddon = class extends BaseDeviceProvider {
79677
79615
  }
79678
79616
  return out;
79679
79617
  },
79680
- adoptDevice: async ({ dreameDeviceId, brokerId, integrationId, name, model }) => {
79618
+ adoptDevice: async ({ dreameDeviceId, integrationId, name, model }) => {
79681
79619
  const devices = this.ctx.kernel.devices;
79682
79620
  if (!devices) throw new Error("dreame adopt: kernel.devices unavailable");
79683
79621
  const config = {
79684
79622
  dreameDeviceId,
79685
- brokerId,
79623
+ brokerId: integrationId,
79686
79624
  model,
79687
79625
  system: "dreame",
79688
79626
  integrationId,
@@ -79711,18 +79649,7 @@ var DreameProviderAddon = class extends BaseDeviceProvider {
79711
79649
  });
79712
79650
  }
79713
79651
  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
- }] });
79652
+ return buildConnectionFormSchema();
79726
79653
  }
79727
79654
  async supportsManualCreation() {
79728
79655
  return false;
@@ -79733,28 +79660,24 @@ var DreameProviderAddon = class extends BaseDeviceProvider {
79733
79660
  async onCreateDevice(_type, _config) {
79734
79661
  throw new Error("Dreame devices are adopted from the cloud, not created manually");
79735
79662
  }
79736
- setBrokerDevicesOnline(brokerId, online) {
79663
+ setIntegrationDevicesOnline(integrationId, online) {
79737
79664
  const reg = this.ctx.kernel.deviceRegistry;
79738
79665
  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,
79666
+ const n = setIntegrationDevicesOnline(reg.getAllForAddon(this.addonId), integrationId, online);
79667
+ if (n > 0) this.ctx.logger.info("Dreame: integration devices " + (online ? "online" : "offline"), { meta: {
79668
+ integrationId,
79742
79669
  count: n
79743
79670
  } });
79744
79671
  }
79745
- requireRegistry() {
79746
- if (!this.registry) throw new Error("Dreame provider not initialised");
79747
- return this.registry;
79748
- }
79749
79672
  };
79750
79673
  //#endregion
79751
79674
  exports.DreameProviderAddon = DreameProviderAddon;
79752
79675
  exports.__toCommonJS = __toCommonJS;
79753
79676
  exports.buildConnectionFormSchema = buildConnectionFormSchema;
79754
79677
  exports.classifyDreameModel = classifyDreameModel;
79678
+ exports.connectionFromSettings = connectionFromSettings;
79755
79679
  exports.controlCapForKind = controlCapForKind;
79756
79680
  exports.deviceTypeForKind = deviceTypeForKind;
79757
- exports.dreameAddonConfigSchema = dreameAddonConfigSchema;
79758
79681
  exports.dreameConfigSchema = dreameConfigSchema;
79759
79682
  exports.mapMiotStateToVacuumState = mapMiotStateToVacuumState;
79760
79683
  exports.mapMowerStatusToActivity = mapMowerStatusToActivity;