@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 +590 -667
- package/dist/addon.mjs +590 -667
- package/dist/index.js +1 -1
- package/dist/index.mjs +2 -2
- package/package.json +2 -5
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-
|
|
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
|
-
|
|
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
|
-
|
|
14822
|
-
|
|
14823
|
-
|
|
14824
|
-
|
|
14825
|
-
|
|
14826
|
-
|
|
14827
|
-
|
|
14828
|
-
|
|
14829
|
-
|
|
14830
|
-
|
|
14831
|
-
|
|
14832
|
-
|
|
14833
|
-
|
|
14834
|
-
|
|
14835
|
-
|
|
14836
|
-
|
|
14837
|
-
|
|
14838
|
-
|
|
14839
|
-
|
|
14840
|
-
|
|
14841
|
-
|
|
14842
|
-
|
|
14843
|
-
|
|
14844
|
-
|
|
14845
|
-
|
|
14846
|
-
|
|
14847
|
-
|
|
14848
|
-
|
|
14849
|
-
}),
|
|
14850
|
-
/** Read the persisted settings record for a broker (kind-specific
|
|
14851
|
-
* shape). Admin-only — settings may contain secrets. Returns `null`
|
|
14852
|
-
* when the broker id is unknown to the provider (the collection
|
|
14853
|
-
* fallback may route a foreign id to the first provider). */
|
|
14854
|
-
getSettings: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
|
|
14855
|
-
/** Overwrite the persisted settings record. The kind-specific
|
|
14856
|
-
* provider validates the shape and applies the change (reconnects
|
|
14857
|
-
* if credentials changed). */
|
|
14858
|
-
setSettings: method(object({
|
|
14859
|
-
id: string(),
|
|
14860
|
-
settings: SettingsRecordSchema$1
|
|
14861
|
-
}), _void(), {
|
|
14862
|
-
kind: "mutation",
|
|
14863
|
-
auth: "admin"
|
|
14864
|
-
}),
|
|
14865
|
-
/** Returns the kind-specific connection config the consumer needs
|
|
14866
|
-
* to open its own client (MQTT pattern: `{url, username, password,
|
|
14867
|
-
* clientIdPrefix}`). HA providers MAY return the auth envelope
|
|
14868
|
-
* but typical HA consumers use `publish` / `subscribe` instead.
|
|
14869
|
-
* Returns `null` when the broker id is unknown to the provider. */
|
|
14870
|
-
getBrokerConfig: method(GetInputSchema, SettingsRecordSchema$1.nullable(), { auth: "admin" }),
|
|
14871
|
-
getSettingsSchema: method(SettingsSchemaInputSchema, SettingsSchemaResultSchema, { auth: "admin" }),
|
|
14872
|
-
testSettings: method(TestSettingsInputSchema, TestSettingsResultSchema, {
|
|
14873
|
-
kind: "mutation",
|
|
14874
|
-
auth: "admin"
|
|
14875
|
-
}),
|
|
14876
|
-
publish: method(PublishInputSchema, unknown(), {
|
|
14877
|
-
kind: "mutation",
|
|
14878
|
-
auth: "admin"
|
|
14879
|
-
}),
|
|
14880
|
-
subscribe: method(SubscribeInputSchema, SubscribeResultSchema, {
|
|
14881
|
-
kind: "mutation",
|
|
14882
|
-
auth: "admin"
|
|
14883
|
-
}),
|
|
14884
|
-
unsubscribe: method(UnsubscribeInputSchema, _void(), {
|
|
14885
|
-
kind: "mutation",
|
|
14886
|
-
auth: "admin"
|
|
14887
|
-
}),
|
|
14888
|
-
/** Read the broker's cached state for a key. Returns `null` when
|
|
14889
|
-
* unknown to the broker (never published / unknown entity). */
|
|
14890
|
-
getState: method(GetStateInputSchema, unknown().nullable()),
|
|
14891
|
-
/** Status method — explicit registration with a `z.void()` input so
|
|
14892
|
-
* the codegen-generated tRPC router types its input as
|
|
14893
|
-
* `{addonId?: string, nodeId?: string}` (system-scoped collection
|
|
14894
|
-
* shape) instead of the device-scoped `{deviceId}` fallback. */
|
|
14895
|
-
getStatus: method(_void(), RegistryStatusSchema)
|
|
14896
|
-
}
|
|
14897
|
-
};
|
|
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(
|
|
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:
|
|
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-
|
|
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
|
-
*
|
|
76106
|
-
*
|
|
76107
|
-
*
|
|
76108
|
-
*
|
|
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
|
|
76218
|
+
var DreameClientRegistry = class {
|
|
76111
76219
|
#logger;
|
|
76112
|
-
#
|
|
76113
|
-
#
|
|
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.#
|
|
76123
|
-
this.#
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
76142
|
-
|
|
76143
|
-
for (const entry of entries) this.#seedCounter(entry.id);
|
|
76144
|
-
for (const entry of entries) try {
|
|
76145
|
-
await this.#startManager(entry);
|
|
76146
|
-
} catch (err) {
|
|
76147
|
-
this.#logger.warn("DreameBrokerRegistry: failed to restore manager", {
|
|
76148
|
-
tags: { brokerId: entry.id },
|
|
76149
|
-
meta: { error: errMsg(err) }
|
|
76150
|
-
});
|
|
76151
|
-
}
|
|
76245
|
+
setOnConnected(cb) {
|
|
76246
|
+
this.#onConnected = cb;
|
|
76152
76247
|
}
|
|
76153
|
-
|
|
76154
|
-
|
|
76155
|
-
|
|
76156
|
-
|
|
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
|
-
|
|
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("
|
|
76161
|
-
tags: {
|
|
76273
|
+
this.#logger.warn("DreameClientRegistry: applyConnection failed", {
|
|
76274
|
+
tags: { integrationId },
|
|
76162
76275
|
meta: { error: errMsg(err) }
|
|
76163
76276
|
});
|
|
76164
76277
|
}
|
|
76165
|
-
|
|
76166
|
-
|
|
76167
|
-
this.#
|
|
76168
|
-
|
|
76169
|
-
dreameFacades.clear();
|
|
76170
|
-
}
|
|
76171
|
-
setOnBrokerConnected(cb) {
|
|
76172
|
-
this.#onBrokerConnected = cb;
|
|
76173
|
-
}
|
|
76174
|
-
setOnBrokerDisconnected(cb) {
|
|
76175
|
-
this.#onBrokerDisconnected = cb;
|
|
76176
|
-
}
|
|
76177
|
-
async createEntry(name, connection, opts = {}) {
|
|
76178
|
-
const id = this.#allocateId();
|
|
76179
|
-
const entry = {
|
|
76180
|
-
id,
|
|
76278
|
+
return;
|
|
76279
|
+
}
|
|
76280
|
+
const manager = this.#makeManager({
|
|
76281
|
+
integrationId,
|
|
76181
76282
|
name,
|
|
76182
76283
|
connection,
|
|
76183
|
-
|
|
76184
|
-
|
|
76185
|
-
|
|
76186
|
-
|
|
76187
|
-
|
|
76188
|
-
|
|
76189
|
-
|
|
76190
|
-
|
|
76191
|
-
if (!mgr) throw new Error(`DreameBrokerRegistry: unknown broker id "${id}"`);
|
|
76192
|
-
for (const [integrationId, brokerId] of this.#integrationToBroker.entries()) if (brokerId === id) this.#integrationToBroker.delete(integrationId);
|
|
76193
|
-
this.#managers.delete(id);
|
|
76194
|
-
this.#facades.delete(id);
|
|
76195
|
-
dreameFacades.set(id, null);
|
|
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
|
|
76293
|
+
await manager.start();
|
|
76198
76294
|
} catch (err) {
|
|
76199
|
-
this.#logger.warn("
|
|
76200
|
-
tags: {
|
|
76295
|
+
this.#logger.warn("DreameClientRegistry: manager start failed", {
|
|
76296
|
+
tags: { integrationId },
|
|
76201
76297
|
meta: { error: errMsg(err) }
|
|
76202
76298
|
});
|
|
76203
76299
|
}
|
|
76204
76300
|
}
|
|
76205
|
-
|
|
76206
|
-
|
|
76207
|
-
|
|
76208
|
-
if (
|
|
76209
|
-
|
|
76210
|
-
|
|
76211
|
-
|
|
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
|
-
|
|
76221
|
-
|
|
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
|
-
|
|
76224
|
-
|
|
76225
|
-
|
|
76226
|
-
|
|
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
|
-
|
|
76229
|
-
|
|
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
|
|
76343
|
+
return [...this.#entries.keys()];
|
|
76233
76344
|
}
|
|
76234
|
-
|
|
76235
|
-
|
|
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
|
-
|
|
76238
|
-
|
|
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.#
|
|
76354
|
+
return this.#entries.size;
|
|
76242
76355
|
}
|
|
76243
76356
|
connectedCount() {
|
|
76244
76357
|
let count = 0;
|
|
76245
|
-
for (const
|
|
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
|
|
76458
|
-
async function
|
|
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"] ===
|
|
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.
|
|
76470
|
-
*
|
|
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 {
|
|
76474
|
-
async function
|
|
76431
|
+
const { getFacade, hasIntegration, listIntegrations, listAdoptedDreame, adoptDevice, removeDevice, findDeviceConfig, logger } = deps;
|
|
76432
|
+
async function allCandidatesForIntegration(integrationId) {
|
|
76475
76433
|
return buildDreameCandidates({
|
|
76476
|
-
devices:
|
|
76477
|
-
adopted: await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
76531
|
-
const
|
|
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
|
|
76542
|
-
|
|
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
|
|
76505
|
+
logger.warn("dreame adopt: device not found on integration — skipping", { meta: {
|
|
76551
76506
|
dreameDeviceId,
|
|
76552
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
76556
|
+
const integrationId = String(cfg["brokerId"]);
|
|
76603
76557
|
const dreameDeviceId = String(cfg["dreameDeviceId"]);
|
|
76604
|
-
if (!(
|
|
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-
|
|
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 }`.
|
|
76617
|
-
*
|
|
76618
|
-
*
|
|
76619
|
-
*
|
|
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
|
|
76622
|
-
const { reg, devices, addonId,
|
|
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"] !==
|
|
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:
|
|
76589
|
+
logger.warn("dreame: integration cascade-remove failed", {
|
|
76633
76590
|
tags: { deviceId: d.id },
|
|
76634
76591
|
meta: {
|
|
76635
|
-
|
|
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-
|
|
76601
|
+
//#region src/dreame-integration-offline.ts
|
|
76645
76602
|
/**
|
|
76646
|
-
* Set every Dreame device belonging to `
|
|
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
|
|
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.
|
|
76610
|
+
* of devices actually transitioned.
|
|
76652
76611
|
*/
|
|
76653
|
-
function
|
|
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") !==
|
|
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
|
-
*
|
|
79442
|
-
*
|
|
79443
|
-
* parent that fans out a single typed accessory child (vacuum or mower)
|
|
79444
|
-
*
|
|
79445
|
-
* `
|
|
79446
|
-
*
|
|
79447
|
-
*
|
|
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
|
-
|
|
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({
|
|
79462
|
+
super({});
|
|
79458
79463
|
}
|
|
79459
79464
|
async onInitialize() {
|
|
79460
79465
|
const regs = await super.onInitialize();
|
|
79461
|
-
this.
|
|
79462
|
-
this.
|
|
79463
|
-
this.ctx.logger.info("Dreame:
|
|
79464
|
-
this.
|
|
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.
|
|
79467
|
-
this.
|
|
79471
|
+
this.clients.setOnDisconnected((integrationId) => {
|
|
79472
|
+
this.setIntegrationDevicesOnline(integrationId, false);
|
|
79468
79473
|
});
|
|
79469
|
-
await this.
|
|
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
|
-
|
|
79475
|
-
|
|
79476
|
-
|
|
79477
|
-
|
|
79478
|
-
|
|
79479
|
-
},
|
|
79480
|
-
{
|
|
79481
|
-
capability: deviceAdoptionCapability,
|
|
79482
|
-
provider: this.buildAdoptionProvider()
|
|
79483
|
-
}
|
|
79484
|
-
];
|
|
79485
|
-
}
|
|
79486
|
-
/** Reconcile the live registry against the persisted `brokers` array after a
|
|
79487
|
-
* settings write (UI save or `broker.*` cap). Mirrors the HA / Homematic addon. */
|
|
79488
|
-
async onConfigChanged() {
|
|
79489
|
-
const reg = this.registry;
|
|
79490
|
-
if (!reg) return;
|
|
79491
|
-
const persisted = this.config.brokers;
|
|
79492
|
-
const liveIds = new Set(reg.list().map((b) => b.id));
|
|
79493
|
-
const persistedIds = new Set(persisted.map((e) => e.id));
|
|
79494
|
-
for (const liveId of liveIds) if (!persistedIds.has(liveId)) try {
|
|
79495
|
-
await reg.removeEntry(liveId);
|
|
79496
|
-
} catch (err) {
|
|
79497
|
-
this.ctx.logger.warn("Dreame onConfigChanged: removeEntry failed", { meta: {
|
|
79498
|
-
brokerId: liveId,
|
|
79499
|
-
error: errMsg(err)
|
|
79500
|
-
} });
|
|
79501
|
-
}
|
|
79502
|
-
for (const entry of persisted) {
|
|
79503
|
-
if (entry.id && liveIds.has(entry.id)) {
|
|
79504
|
-
if (sameConnection(entry.connection, reg.getConnection(entry.id))) continue;
|
|
79505
|
-
try {
|
|
79506
|
-
await reg.updateEntry(entry.id, entry.connection);
|
|
79507
|
-
} catch (err) {
|
|
79508
|
-
this.ctx.logger.warn("Dreame onConfigChanged: updateEntry failed", { meta: {
|
|
79509
|
-
brokerId: entry.id,
|
|
79510
|
-
error: errMsg(err)
|
|
79511
|
-
} });
|
|
79512
|
-
}
|
|
79513
|
-
continue;
|
|
79514
|
-
}
|
|
79515
|
-
try {
|
|
79516
|
-
const created = await reg.createEntry(entry.name, entry.connection);
|
|
79517
|
-
if (created.id !== entry.id) {
|
|
79518
|
-
const next = persisted.map((b) => b === entry ? {
|
|
79519
|
-
...b,
|
|
79520
|
-
id: created.id
|
|
79521
|
-
} : b);
|
|
79522
|
-
await this.updateGlobalSettings({ brokers: next });
|
|
79523
|
-
}
|
|
79524
|
-
} catch (err) {
|
|
79525
|
-
this.ctx.logger.warn("Dreame onConfigChanged: failed to start new broker", { meta: {
|
|
79526
|
-
brokerName: entry.name,
|
|
79527
|
-
error: errMsg(err)
|
|
79528
|
-
} });
|
|
79529
|
-
}
|
|
79530
|
-
}
|
|
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.
|
|
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.
|
|
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
|
-
/**
|
|
79579
|
-
*
|
|
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
|
-
*
|
|
79607
|
-
*
|
|
79608
|
-
*
|
|
79609
|
-
* was deleted
|
|
79610
|
-
*
|
|
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
|
|
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(
|
|
79616
|
-
for (const integration of mine) {
|
|
79617
|
-
const
|
|
79618
|
-
|
|
79619
|
-
|
|
79620
|
-
if (!this.config.brokers.some((b) => b.id === brokerId)) {
|
|
79621
|
-
this.ctx.logger.warn("Dreame integration→broker: linked broker not found", { meta: {
|
|
79622
|
-
integrationId: integration.id,
|
|
79623
|
-
brokerId
|
|
79624
|
-
} });
|
|
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
|
-
|
|
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
|
|
79636
|
-
|
|
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
|
|
79641
|
-
await
|
|
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
|
|
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.
|
|
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
|
-
|
|
79661
|
-
|
|
79662
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
79663
|
+
setIntegrationDevicesOnline(integrationId, online) {
|
|
79737
79664
|
const reg = this.ctx.kernel.deviceRegistry;
|
|
79738
79665
|
if (!reg) return;
|
|
79739
|
-
const n =
|
|
79740
|
-
if (n > 0) this.ctx.logger.info("Dreame:
|
|
79741
|
-
|
|
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;
|